In this tutorial, we'll build a complete backend API for a social media application using Spring Boot 3.3, Java 22, MongoDB, and AWS S3. This application allows users to create posts with text and media content, view posts from other users, and like posts. We'll cover everything from setting up the project to implementing advanced features like pagination, search, and secure media file storage.
The final application will have the following features:
Our tech stack includes:
First, let's run MongoDB using Docker:
# Pull MongoDB image docker pull mongo:latest # Run MongoDB container docker run -d -p 27017:27017 --name mongo-server mongo:latest # Connect to MongoDB shell to execute queries docker exec -it mongo-server mongosh
If you don't have an AWS account, please register first and login to AWS console.
Then create an S3 bucket to store media files.
Create an IAM user and give access to S3. Create an access key and secret key for the user.
Now let us create a new Spring Boot project using Spring Initializr with the following dependencies:
Also add AWS Java SDK (S3) dependency:
Contents of pom.xml
:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>software.amazon.awssdk</groupId> <artifactId>s3</artifactId> <version>2.25.27</version> </dependency> </dependencies>
Also let us add mongo db database name in application.properties
:
spring.data.mongodb.database=social-media
We don't need to create database or collection in MongoDB manually since this will get created automatically when we run the application and call the endpoints.
Create the Post model:
@Data @Document(collection = "posts") public class Post { @Id private String id; private String title; private String text; private String tags; private PostCreator creator; private Integer likes; private LocalDateTime createdAt; private String mediaUrl; private MediaType mediaType; } @Data public class PostCreator { private String id; private String name; } public enum MediaType { VIDEO, IMAGE, NONE }
public interface PostRepository extends MongoRepository<Post, String> { @Query("{ '$text': { '$search': ?0 } }") Page<Post> searchByText(String searchTerm, Pageable pageable); @Query("{ '_id': ?0 }") @Update("{ '$inc': { 'likes': 1 } }") void incrementLikes(String postId); }
We will add a couple of methods in the repository to search and increment likes for a post.
The searchByText
method uses MongoDB's full-text search to find posts based on the search term. We need to add a text index on the fields we want to search. Run the following command in the MongoDB shell to create the index on fields title
, text
, and tags
.
db.post.createIndex( { title: "text", text: "text", tags: "text" }, { name: "title_text_tags_index" } )
The incrementLikes
method increments the likes count for a post using the $inc
operator.
Now let us configure the AWS S3 client in our Spring Boot application. Set the following properties in application.properties
:
aws.s3.accessKey=your-access-key aws.s3.secretKey=your-secret-key
@Configuration public class S3Config { public static final String BUCKET_NAME = "social-media-app-codewiz"; @Bean public S3Client s3Client( @Value("${aws.s3.accessKey}") String accessKey, @Value("${aws.s3.secretKey}") String secretKey ) { AwsBasicCredentials credentials = AwsBasicCredentials.builder() .accessKeyId(accessKey) .secretAccessKey(secretKey) .build(); return S3Client.builder() .credentialsProvider(() -> credentials) .region(Region.AP_SOUTHEAST_2) .build(); } }
We want to restrict access to media files in S3 to only authenticated users. To achieve this, we'll generate pre-signed URLs for media files that expire after a certain time.
@Service @AllArgsConstructor public class S3PresignedUrlService { public String generatePresignedUrl(String key) { S3Presigner presigner = S3Presigner.builder().region(Region.AP_SOUTHEAST_2).build(); GetObjectRequest getObjectRequest = GetObjectRequest.builder() .bucket(AWSConfig.BUCKET_NAME) .key(key) .build(); GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder() .signatureDuration(Duration.ofHours(48)) .getObjectRequest(getObjectRequest) .build(); PresignedGetObjectRequest presignedGetObjectRequest = presigner.presignGetObject(getObjectPresignRequest); return presignedGetObjectRequest.url().toExternalForm(); } }
The PostService class handles all business logic including post management and S3 file operations:
@Service @AllArgsConstructor public class PostService { private final PostRepository postRepository; private final S3Client s3Client; private final S3PresignedUrlService s3PresignedUrlService; // Stores the post in MongoDB and media file in S3 public Post createPost(String title, String text, List<String> tags, MultipartFile mediaFile) throws IOException { String fileName = storeFileInS3(mediaFile); PostCreator creator = PostCreator.builder() .id("1") .name("John Doe") .build(); Post post = new Post(); post.setTitle(title); post.setText(text); post.setTags(tags); post.setLikes(0); post.setCreator(creator); post.setCreatedAt(java.time.LocalDateTime.now()); post.setMediaUrl(fileName); MediaType mediaType = getMediaType(mediaFile); post.setMediaType(mediaType); return postRepository.save(post); } private static MediaType getMediaType(MultipartFile mediaFile) { return Objects.requireNonNull(mediaFile.getContentType()).startsWith("video/") ? MediaType.VIDEO : (mediaFile.getContentType().startsWith("image/") ? MediaType.IMAGE : null); } // Uses the S3 client to store the media file private String storeFileInS3(MultipartFile mediaFile) throws IOException { String fileName = UUID.randomUUID().toString()+" - "+ mediaFile.getOriginalFilename(); if(mediaFile !=null && !mediaFile.isEmpty()){ PutObjectRequest putObjectRequest = PutObjectRequest.builder() .bucket(AWSConfig.BUCKET_NAME) .key(fileName) .build(); s3Client.putObject(putObjectRequest, RequestBody.fromBytes(mediaFile.getBytes())); } return fileName; } public Page<Post> getAllPosts(int page, int size,String searchCriteria) { Sort sort = Sort.by(Sort.Direction.DESC, "id"); var postList = StringUtils.hasText(searchCriteria)? postRepository.searchByText(searchCriteria,PageRequest.of(page, size, sort)) :postRepository.findAll(PageRequest.of(page, size, sort)); postList.forEach(post -> { if(post.getMediaUrl()!=null) { post.setPresignedUrl(s3PresignedUrlService.generatePresignedUrl(post.getMediaUrl())); } }); return postList; } public Post getPostById(String id) { var post = postRepository.findById(id).orElseThrow(() -> new RuntimeException("Post not found")); if(post.getMediaUrl()!=null) { post.setPresignedUrl(s3PresignedUrlService.generatePresignedUrl(post.getMediaUrl())); } return post; } public Post updatePost(String id, String title, String text, List<String> tags, MultipartFile mediaFile) throws IOException { Post post = getPostById(id); if(post.getMediaUrl()!=null){ s3Client.deleteObject(builder -> builder.bucket(AWSConfig.BUCKET_NAME).key(post.getMediaUrl())); } String fileName = storeFileInS3(mediaFile); post.setTitle(title); post.setText(text); post.setTags(tags); post.setMediaUrl(fileName); MediaType mediaType = getMediaType(mediaFile); post.setMediaType(mediaType); return postRepository.save(post); } public void deletePost(String id) { Post post = getPostById(id); if(post.getMediaUrl()!=null){ s3Client.deleteObject(builder -> builder.bucket(AWSConfig.BUCKET_NAME).key(post.getMediaUrl())); } postRepository.deleteById(id); } public void likePost(String id) { postRepository.incrementLikes(id); } }
@RestController @RequestMapping("/posts") @AllArgsConstructor public class PostController { private final PostService postService; @PostMapping public Post createPost(@RequestParam String title, @RequestParam String text, @RequestParam List<String> tags, @RequestParam(value = "mediaFile", required = false) MultipartFile mediaFile) throws IOException { return postService.createPost(title, text, tags,mediaFile); } @GetMapping public Page<Post> getAllPosts( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(defaultValue = "") String searchCriteria ) { return postService.getAllPosts(page, size,searchCriteria); } @GetMapping("/{id}") public Post getPostById(@PathVariable String id) { return postService.getPostById(id); } @PutMapping("/{id}") public Post updatePost(@PathVariable String id, @RequestParam String title, @RequestParam String text, @RequestParam List<String> tags, @RequestParam(value = "mediaFile", required = false) MultipartFile mediaFile) throws IOException { return postService.updatePost(id, title, text, tags,mediaFile); } @DeleteMapping("/{id}") public void deletePost(@PathVariable String id) { postService.deletePost(id); } @PostMapping("/{id}/like") public void likePost(@PathVariable String id) { postService.likePost(id); } }
curl -X POST "http://localhost:8080/posts" \ -F "title=Test Post" \ -F "text=This is a test post" \ -F "tags=test" \ -F "mediaFile=@image.jpg"
curl -X GET "http://localhost:8080/posts?page=0&size=10&searchCriteria=test"
In this tutorial, we built a social media backend API using Spring Boot, MongoDB, and AWS S3. We covered:
To stay updated with the latest updates in Java and Spring follow us on youtube, linked in and medium.
You can find the code used in this blog here
Watch the detailed complete video tutorial below:
Learn how to implement JWT authentication with Spring 6 Security following best practices recommended in Spring docs and without custom filters.
Learn how to build an AI-powered stock portfolio advisor using Java, Spring Boot, LangChain4j, and OpenAI/Ollama. This guide walks you through integrating AI into your application to provide real-time investment advice based on the latest stock data.
Get instant AI-powered summaries of YouTube videos and websites. Save time while enhancing your learning experience.