
REST API with Spring Boot, MongoDB, and AWS S3: Build a Social Media App Backend
Introduction
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.
Project Overview
The final application will have the following features:
- Create, read, update, and delete posts
- Upload and store media files (images and videos)
- Search posts by title, content, or tags
- Pagination for post listing
- Like functionality for posts
- Secure media access using pre-signed URLs
Our tech stack includes:
- Spring Boot 3.3: For building the REST API
- MongoDB: As the document database
- AWS S3: For storing media files
- Java 22: As the programming language
Project Setup
MongoDB Setup
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
S3 Bucket Setup
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.
Spring Boot Dependencies
Now let us create a new Spring Boot project using Spring Initializr with the following dependencies:
- Spring Web
- Spring Data MongoDB
- Spring Boot Actuator
- Spring Boot DevTools
- Lombok
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.
Model Implementation
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 }
Repository Layer
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.
AWS S3 Integration
Configuration
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(); } }
Pre-signed URL Service
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(); } }
Service Implementation
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); } }
REST Controller
@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); } }
Testing
Create a Post
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"
Get Posts with Pagination
curl -X GET "http://localhost:8080/posts?page=0&size=10&searchCriteria=test"
Conclusion
In this tutorial, we built a social media backend API using Spring Boot, MongoDB, and AWS S3. We covered:
- Setting up MongoDB with Docker
- Implementing CRUD operations
- Handling file uploads with AWS S3
- Implementing pagination and search
- Securing file access with pre-signed URLs
- Adding social features like post liking
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
Video Tutorial
Watch the detailed complete video tutorial below:
Related Posts
JWT Authentication with Spring 6 Security
Learn how to implement JWT authentication with Spring 6 Security following best practices recommended in Spring docs and without custom filters.
Building a Smart Investment Portfolio Advisor with Java, Spring Boot, and LangChain4j
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.