Mastering RESTful APIs with Spring Boot: A Step-by-Step Guide with Swagger Integration

Photo by Goran Ivos on Unsplash

Mastering RESTful APIs with Spring Boot: A Step-by-Step Guide with Swagger Integration


Building a Robust REST API with Spring Boot

In this guide, we will walk through the process of building a robust REST API using Spring Boot. We will cover various topics such as setting up controllers, mapping requests to the appropriate handlers, adding validation, handling exceptions, formatting responses, and integrating Swagger for API documentation.

1. Setting Up the Spring Boot Project

Before we dive into the code, let's set up our Spring Boot project.

Create a Spring Boot Application

You can create a Spring Boot application from Spring Initializr by selecting dependencies like Spring Web, Spring Boot DevTools, and Spring Data JPA (if you plan to interact with a database).

Alternatively, you can use your IDE to create a new Spring Boot project.

Maven/Gradle Dependencies

Add the following dependencies to your pom.xml (if you're using Maven) for Swagger integration:

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>

For Gradle, add this to your build.gradle:

implementation 'io.springfox:springfox-boot-starter:3.0.0'

2. Defining the Model

Models represent the structure of the data that will be transferred between the client and server. For this example, we will define a Book model.

Book.java (Model)

package com.example.demo.model;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;
import java.time.LocalDateTime;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotEmpty(message = "Title is required")
    @Size(min = 1, max = 100, message = "Title must be between 1 and 100 characters")
    private String title;

    @NotEmpty(message = "Author is required")
    private String author;

    @Column(nullable = false, updatable = false)
    @CreationTimestamp
    private LocalDateTime createdOn;

    @Column(nullable = false)
    @UpdateTimestamp
    private LocalDateTime modifiedOn;

    // Getters and Setters
}

Explanation:

  • The Book model represents a book entity with properties like id, title, author, publishedDate, and timestamp fields (createdOn, modifiedOn).
  • Validation:

    • @NotEmpty ensures the title and author are not empty.

    • @Size ensures the title is between 1 and 100 characters.

  • Timestamps:

    • @CreationTimestamp automatically sets the createdOn field when the book is created.

    • @UpdateTimestamp updates the modifiedOn field whenever the book is modified.

  • ID Generation:

    • The id is automatically generated using @GeneratedValue(strategy = GenerationType.IDENTITY).

This model ensures that the book has the necessary data and timestamp tracking for creation and updates.

3. Defining the DTO (Data Transfer Object)

DTOs are used to transfer data between the client and the server. They often contain only the necessary information required for specific operations.

BookDTO.java (DTO)

package com.example.demo.dto;

public class BookDTO {

    private Long id;
    private String title;
    private String author;

    // Getters and Setters
}

Explanation:

  • The BookDTO class is a simplified version of the Book model. It doesn't include any validation annotations and is used for transferring data between layers.

  • Using DTOs can help decouple the internal data representation from the external API representation.

4. Setting Up the Controller

Controllers in Spring Boot handle the requests coming from clients (such as browsers or mobile apps). Let's define a simple Book resource.

BookController.java

package com.example.demo.controller;

import com.example.demo.dto.BookDTO;
import com.example.demo.model.Book;
import com.example.demo.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/books")
public class BookController {

    @Autowired
    private BookService bookService;

    // Get all books
    @GetMapping
    public List<BookDTO> getAllBooks() {
        return bookService.getAllBooks();
    }

    // Get a single book by ID
    @GetMapping("/{id}")
    public ResponseEntity<BookDTO> getBookById(@PathVariable Long id) {
        return bookService.getBookById(id)
                .map(book -> ResponseEntity.ok().body(book))
                .orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND).build());
    }

    // Add a new book
    @PostMapping
    public ResponseEntity<BookDTO> createBook(@RequestBody BookDTO bookDTO) {
        BookDTO savedBook = bookService.createBook(bookDTO);
        return ResponseEntity.status(HttpStatus.CREATED).body(savedBook);
    }

    // Update an existing book
    @PutMapping("/{id}")
    public ResponseEntity<BookDTO> updateBook(@PathVariable Long id, @RequestBody BookDTO bookDTO) {
        return bookService.updateBook(id, bookDTO)
                .map(updatedBook -> ResponseEntity.ok(updatedBook))
                .orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND).build());
    }

    // Delete a book
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
        if (bookService.deleteBook(id)) {
            return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
        }
        return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
    }
}

Explanation:

  • @RestController: Marks the class as a REST controller, and the return type is automatically converted to JSON.

  • @RequestMapping: Sets the base URL path for all endpoints in the controller.

  • @GetMapping, @PostMapping, @PutMapping, @DeleteMapping: These annotations map HTTP requests (GET, POST, PUT, DELETE) to methods in your controller.

5. Defining the Interface

The interface allows abstraction for service implementations, defining methods for operations on books.

BookService.java (Interface)

package com.example.demo.service;

import com.example.demo.dto.BookDTO;

import java.util.List;
import java.util.Optional;

public interface BookService {

    List<BookDTO> getAllBooks();

    Optional<BookDTO> getBookById(Long id);

    BookDTO createBook(BookDTO bookDTO);

    Optional<BookDTO> updateBook(Long id, BookDTO bookDTO);

    boolean deleteBook(Long id);
}

6. Implementing the Service

The service layer contains the business logic of the application. It works as a bridge between the controller and the data layer.

BookServiceImpl.java (Service Implementation)

package com.example.demo.service;

import com.example.demo.dto.BookDTO;
import com.example.demo.model.Book;
import com.example.demo.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
public class BookServiceImpl implements BookService {

    @Autowired
    private BookRepository bookRepository;

    @Override
    public List<BookDTO> getAllBooks() {
        return bookRepository.findAll().stream()
                .map(book -> new BookDTO(book.getId(), book.getTitle(), book.getAuthor()))
                .collect(Collectors.toList());
    }

    @Override
    public Optional<BookDTO> getBookById(Long id) {
        return bookRepository.findById(id)
                .map(book -> new BookDTO(book.getId(), book.getTitle(), book.getAuthor()));
    }

    @Override
    public BookDTO createBook(BookDTO bookDTO) {
        Book book = new Book(bookDTO.getId(), bookDTO.getTitle(), bookDTO.getAuthor());
        book = bookRepository.save(book);
        return new BookDTO(book.getId(), book.getTitle(), book.getAuthor());
    }

    @Override
    public Optional<BookDTO> updateBook(Long id, BookDTO bookDTO) {
        return bookRepository.findById(id)
                .map(existingBook -> {
                    existingBook.setTitle(bookDTO.getTitle());
                    existingBook.setAuthor(bookDTO.getAuthor());
                    bookRepository.save(existingBook);
                    return new BookDTO(existingBook.getId(), existingBook.getTitle(), existingBook.getAuthor());
                });
    }

    @Override
    public boolean deleteBook(Long id) {
        if (bookRepository.existsById(id)) {
            bookRepository.deleteById(id);
            return true;
        }
        return false;
    }
}

7. Exception Handling

We need to handle exceptions properly to provide meaningful error messages to the clients.

Create a global exception handler using @ControllerAdvice.

GlobalExceptionHandler.java

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<String> handleResourceNotFound(ResourceNotFoundException ex) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGeneralException(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getMessage());
    }
}

In the above example, we handle ResourceNotFoundException with a custom error message, and we have a generic handler for all other exceptions.

8. Swagger Integration (OpenAPI 3.0)

Swagger helps in documenting and interacting with your REST APIs. With Spring Boot, you can integrate OpenAPI 3.0 to provide detailed API documentation.

Dependencies

To integrate Swagger 3.0 with Spring Boot, add the following dependencies in your pom.xml (for Maven):

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-ui</artifactId>
    <version>1.5.9</version>
</dependency>
<dependency>
    <groupId>io.swagger.core.v3</groupId>
    <artifactId>swagger-annotations</artifactId>
    <version>2.1.11</version>
</dependency>

For Gradle, add this to build.gradle:

implementation 'org.springdoc:springdoc-openapi-ui:1.5.9'
implementation 'io.swagger.core.v3:swagger-annotations:2.1.11'

Swagger Configuration

Here's how you can set up Swagger (OpenAPI 3.0) in your Spring Boot application:

package com.example.demo;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.servers.Server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@OpenAPIDefinition(info = @Info(
        title = "Book API", 
        description = "API for managing books", 
        version = "1.0.0", 
        contact = @Contact(name = "Demo App", email = "demo@example.com", url = "http://localhost:8080")),
        servers = @Server(url = "http://localhost:8080", description = "Development Server"))
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

Access Swagger UI

Once your Spring Boot application is up and running, you can access the Swagger UI documentation by navigating to:

http://localhost:8080/swagger-ui/index.html

Here, you will be able to see all your API endpoints, with detailed information and the ability to interact with them directly from the UI.


Conclusion

In this guide, we've covered the basics of building a REST API using Spring Boot, including setting up controllers, mapping requests, adding validation, exception handling, formatting responses, and integrating Swagger for API documentation. These practices will help you build robust and maintainable REST APIs that are easy to consume and test.