CodeWiz Logo

    CodeWiz

    REST API with Spring Boot, MongoDB, and AWS S3: Build a Social Media App Backend

    REST API with Spring Boot, MongoDB, and AWS S3: Build a Social Media App Backend

    26/02/2025

    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

    Social Media API Components

    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: