Lab: REST API for 'Book Catalog' & Monitoring


Lab 1: Building a Basic REST API for “Book Catalog”

Objective

Create a REST API to manage a catalog of books, handling operations like retrieving all books, retrieving a single book by ID, adding a new book, updating book details, and deleting a book.

Prerequisites

  • Complete the Student Portal application from Module 2
  • Basic understanding of Spring Boot and HTTP methods

Step 1: Create a new Spring Boot project

For this lab, we’ll create a separate project for our Book Catalog API.

  1. Create a new Spring Boot project with the following details:
  • Group: com.example
  • Artifact: bookcatalog
  • Dependencies: Spring Web, Spring Boot DevTools, Spring Validation
  1. Open the project in your IDE

Step 2: Define the Book model

Create a new package com.example.bookcatalog.model and add the Book class:

package com.example.bookcatalog.model;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PastOrPresent;

import java.time.LocalDate;
import java.util.Objects;

public class Book {
    private Long id;

    @NotBlank(message = "Title is required")
    private String title;

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

    @NotBlank(message = "ISBN is required")
    private String isbn;

    @NotNull(message = "Publication date is required")
    @PastOrPresent(message = "Publication date cannot be in the future")
    private LocalDate publicationDate;

    @Min(value = 1, message = "Page count must be positive")
    private int pageCount;

    private String genre;

    private String description;

    // Default constructor
    public Book() {
    }

    // Constructor with all fields
    public Book(Long id, String title, String author, String isbn,
                LocalDate publicationDate, int pageCount, String genre,
                String description) {
        this.id = id;
        this.title = title;
        this.author = author;
        this.isbn = isbn;
        this.publicationDate = publicationDate;
        this.pageCount = pageCount;
        this.genre = genre;
        this.description = description;
    }

    // Getters and setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public String getIsbn() {
        return isbn;
    }

    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }

    public LocalDate getPublicationDate() {
        return publicationDate;
    }

    public void setPublicationDate(LocalDate publicationDate) {
        this.publicationDate = publicationDate;
    }

    public int getPageCount() {
        return pageCount;
    }

    public void setPageCount(int pageCount) {
        this.pageCount = pageCount;
    }

    public String getGenre() {
        return genre;
    }

    public void setGenre(String genre) {
        this.genre = genre;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return Objects.equals(id, book.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", author='" + author + '\'' +
                ", isbn='" + isbn + '\'' +
                ", publicationDate=" + publicationDate +
                ", pageCount=" + pageCount +
                ", genre='" + genre + '\'' +
                '}';
    }
}

Step 3: Create a Repository layer

Create a new package com.example.bookcatalog.repository and add the BookRepository interface:

package com.example.bookcatalog.repository;

import com.example.bookcatalog.model.Book;

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

public interface BookRepository {
    List<Book> findAll();
    Optional<Book> findById(Long id);
    Book save(Book book);
    void delete(Long id);
    boolean existsById(Long id);
}

Now implement the repository with an in-memory data store:

package com.example.bookcatalog.repository;

import com.example.bookcatalog.model.Book;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

@Repository
public class InMemoryBookRepository implements BookRepository {

    private final Map<Long, Book> books = new ConcurrentHashMap<>();
    private final AtomicLong idGenerator = new AtomicLong(1);

    public InMemoryBookRepository() {
        // Add some sample data
        Book book1 = new Book(
            idGenerator.getAndIncrement(),
            "Clean Code",
            "Robert C. Martin",
            "9780132350884",
            LocalDate.of(2008, 8, 1),
            464,
            "Programming",
            "A handbook of agile software craftsmanship"
        );

        Book book2 = new Book(
            idGenerator.getAndIncrement(),
            "Design Patterns",
            "Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides",
            "9780201633610",
            LocalDate.of(1994, 11, 10),
            416,
            "Programming",
            "Elements of Reusable Object-Oriented Software"
        );

        Book book3 = new Book(
            idGenerator.getAndIncrement(),
            "The Pragmatic Programmer",
            "Andrew Hunt, David Thomas",
            "9780201616224",
            LocalDate.of(1999, 10, 30),
            352,
            "Programming",
            "From Journeyman to Master"
        );

        books.put(book1.getId(), book1);
        books.put(book2.getId(), book2);
        books.put(book3.getId(), book3);
    }

    @Override
    public List<Book> findAll() {
        return new ArrayList<>(books.values());
    }

    @Override
    public Optional<Book> findById(Long id) {
        return Optional.ofNullable(books.get(id));
    }

    @Override
    public Book save(Book book) {
        if (book.getId() == null) {
            // New book
            book.setId(idGenerator.getAndIncrement());
        }
        books.put(book.getId(), book);
        return book;
    }

    @Override
    public void delete(Long id) {
        books.remove(id);
    }

    @Override
    public boolean existsById(Long id) {
        return books.containsKey(id);
    }
}

Step 4: Create a Service layer

Create a new package com.example.bookcatalog.service and add the BookService interface:

package com.example.bookcatalog.service;

import com.example.bookcatalog.model.Book;

import java.util.List;

public interface BookService {
    List<Book> findAllBooks();
    Book findBookById(Long id);
    Book createBook(Book book);
    Book updateBook(Long id, Book book);
    void deleteBook(Long id);
}

Now implement the service:

package com.example.bookcatalog.service;

import com.example.bookcatalog.exception.ResourceNotFoundException;
import com.example.bookcatalog.model.Book;
import com.example.bookcatalog.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class BookServiceImpl implements BookService {

    private final BookRepository bookRepository;

    @Autowired
    public BookServiceImpl(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    @Override
    public List<Book> findAllBooks() {
        return bookRepository.findAll();
    }

    @Override
    public Book findBookById(Long id) {
        return bookRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Book not found with id: " + id));
    }

    @Override
    public Book createBook(Book book) {
        // Ensure the book has no ID (it will be generated)
        book.setId(null);
        return bookRepository.save(book);
    }

    @Override
    public Book updateBook(Long id, Book bookDetails) {
        Book book = findBookById(id);

        // Update the book properties
        book.setTitle(bookDetails.getTitle());
        book.setAuthor(bookDetails.getAuthor());
        book.setIsbn(bookDetails.getIsbn());
        book.setPublicationDate(bookDetails.getPublicationDate());
        book.setPageCount(bookDetails.getPageCount());
        book.setGenre(bookDetails.getGenre());
        book.setDescription(bookDetails.getDescription());

        return bookRepository.save(book);
    }

    @Override
    public void deleteBook(Long id) {
        if (!bookRepository.existsById(id)) {
            throw new ResourceNotFoundException("Book not found with id: " + id);
        }
        bookRepository.delete(id);
    }
}

Step 5: Create the Exception handler

Create a new package com.example.bookcatalog.exception and add the ResourceNotFoundException class:

package com.example.bookcatalog.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {

    public ResourceNotFoundException(String message) {
        super(message);
    }
}

Add an ErrorResponse class to standardize error responses:

package com.example.bookcatalog.exception;

import java.util.ArrayList;
import java.util.List;

public class ErrorResponse {
    private int status;
    private String message;
    private long timestamp;
    private List<String> errors = new ArrayList<>();

    public ErrorResponse() {
    }

    public ErrorResponse(int status, String message, long timestamp) {
        this.status = status;
        this.message = message;
        this.timestamp = timestamp;
    }

    public ErrorResponse(int status, String message, long timestamp, List<String> errors) {
        this.status = status;
        this.message = message;
        this.timestamp = timestamp;
        this.errors = errors;
    }

    // Getters and setters
    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public long getTimestamp() {
        return timestamp;
    }

    public void setTimestamp(long timestamp) {
        this.timestamp = timestamp;
    }

    public List<String> getErrors() {
        return errors;
    }

    public void setErrors(List<String> errors) {
        this.errors = errors;
    }
}

Create a global exception handler:

package com.example.bookcatalog.exception;

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

import java.util.ArrayList;
import java.util.List;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.NOT_FOUND.value(),
            ex.getMessage(),
            System.currentTimeMillis()
        );

        return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> handleValidationExceptions(MethodArgumentNotValidException ex) {
        List<String> errors = new ArrayList<>();
        ex.getBindingResult().getAllErrors().forEach(error -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.add(fieldName + ": " + errorMessage);
        });

        ErrorResponse error = new ErrorResponse(
            HttpStatus.BAD_REQUEST.value(),
            "Validation failed",
            System.currentTimeMillis(),
            errors
        );

        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleGlobalException(Exception ex) {
        ErrorResponse error = new ErrorResponse(
            HttpStatus.INTERNAL_SERVER_ERROR.value(),
            "An unexpected error occurred: " + ex.getMessage(),
            System.currentTimeMillis()
        );

        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

Step 6: Create the REST Controller

Create a new package com.example.bookcatalog.controller and add the BookController class:

package com.example.bookcatalog.controller;

import com.example.bookcatalog.model.Book;
import com.example.bookcatalog.service.BookService;
import jakarta.validation.Valid;
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 {

    private final BookService bookService;

    @Autowired
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    @GetMapping
    public ResponseEntity<List<Book>> getAllBooks() {
        List<Book> books = bookService.findAllBooks();
        return ResponseEntity.ok(books);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Book> getBookById(@PathVariable Long id) {
        Book book = bookService.findBookById(id);
        return ResponseEntity.ok(book);
    }

    @PostMapping
    public ResponseEntity<Book> createBook(@Valid @RequestBody Book book) {
        Book createdBook = bookService.createBook(book);
        return new ResponseEntity<>(createdBook, HttpStatus.CREATED);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Book> updateBook(@PathVariable Long id, @Valid @RequestBody Book book) {
        Book updatedBook = bookService.updateBook(id, book);
        return ResponseEntity.ok(updatedBook);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
        bookService.deleteBook(id);
        return ResponseEntity.noContent().build();
    }
}

Step 7: Configure Application Properties

Update the application.properties file in src/main/resources:

# Server configuration
server.port=8081

# Jackson configuration
spring.jackson.date-format=yyyy-MM-dd
spring.jackson.serialization.write-dates-as-timestamps=false

# Logging configuration
logging.level.org.springframework.web=INFO
logging.level.com.example.bookcatalog=DEBUG

# Server error handling
server.error.include-message=always

Step 8: Run and Test the Application

  1. Run the application:
  • From IDE: Run the main application class BookcatalogApplication.java
  • With Maven: mvn spring-boot:run
  • With Gradle: ./gradlew bootRun
  1. Test the REST API using Postman, curl, or httpie:
  • Get all books:

    GET http://localhost:8081/api/books
  • Get a book by ID:

    GET http://localhost:8081/api/books/1
  • Create a new book:

    POST http://localhost:8081/api/books
    Content-Type: application/json
    
    {
     "title": "Effective Java",
     "author": "Joshua Bloch",
     "isbn": "9780134685991",
     "publicationDate": "2018-01-06",
     "pageCount": 416,
     "genre": "Programming",
     "description": "Best practices for the Java platform"
    }
  • Update a book:

    PUT http://localhost:8081/api/books/1
    Content-Type: application/json
    
    {
     "title": "Clean Code: A Handbook of Agile Software Craftsmanship",
     "author": "Robert C. Martin",
     "isbn": "9780132350884",
     "publicationDate": "2008-08-01",
     "pageCount": 464,
     "genre": "Programming",
     "description": "A handbook of agile software craftsmanship"
    }
  • Delete a book:

    DELETE http://localhost:8081/api/books/1

Lab 2: Spring Boot Actuator (Monitoring)

Objective

Enable and explore Actuator endpoints for monitoring and managing the Book Catalog application.

Step 1: Add Spring Boot Actuator dependency

Add the following dependency to your pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

If you’re using Gradle, add this to your build.gradle:

implementation 'org.springframework.boot:spring-boot-starter-actuator'

Step 2: Configure Actuator endpoints

Update the application.properties file:

# Actuator configuration
management.endpoints.web.exposure.include=health,info,metrics,mappings,beans,env
management.endpoint.health.show-details=always

# Custom info endpoint
info.app.name=Book Catalog API
info.app.description=A Spring Boot REST API for managing a book catalog
info.app.version=1.0.0
info.app.java.version=${java.version}

Step 3: Create a custom health indicator

Create a new package com.example.bookcatalog.actuator and add a custom health indicator:

package com.example.bookcatalog.actuator;

import com.example.bookcatalog.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class BookCatalogHealthIndicator implements HealthIndicator {

    private final BookService bookService;

    @Autowired
    public BookCatalogHealthIndicator(BookService bookService) {
        this.bookService = bookService;
    }

    @Override
    public Health health() {
        try {
            // Check if we can access the book repository
            int bookCount = bookService.findAllBooks().size();
            return Health.up()
                    .withDetail("bookCount", bookCount)
                    .withDetail("message", "Book catalog service is running normally")
                    .build();
        } catch (Exception e) {
            return Health.down()
                    .withDetail("error", e.getMessage())
                    .withDetail("message", "Book catalog service is not available")
                    .build();
        }
    }
}

Step 4: Create a custom metrics endpoint

Add a custom metrics component to track book statistics:

package com.example.bookcatalog.actuator;

import com.example.bookcatalog.model.Book;
import com.example.bookcatalog.service.BookService;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.time.LocalDate;
import java.util.List;
import java.util.function.Supplier;

@Component
public class BookCatalogMetrics {

    private final BookService bookService;
    private final MeterRegistry meterRegistry;

    @Autowired
    public BookCatalogMetrics(BookService bookService, MeterRegistry meterRegistry) {
        this.bookService = bookService;
        this.meterRegistry = meterRegistry;
    }

    @PostConstruct
    public void registerMetrics() {
        // Total number of books
        Gauge.builder("bookcatalog.books.count",
                      () -> bookService.findAllBooks().size())
             .description("Total number of books in the catalog")
             .register(meterRegistry);

        // Number of books published in the last 10 years
        Gauge.builder("bookcatalog.books.recent",
                      () -> getRecentBooksCount())
             .description("Number of books published in the last 10 years")
             .register(meterRegistry);

        // Average page count
        Gauge.builder("bookcatalog.books.avgpages",
                      () -> getAveragePageCount())
             .description("Average number of pages per book")
             .register(meterRegistry);
    }

    private int getRecentBooksCount() {
        List<Book> allBooks = bookService.findAllBooks();
        LocalDate tenYearsAgo = LocalDate.now().minusYears(10);
        return (int) allBooks.stream()
                .filter(book -> book.getPublicationDate().isAfter(tenYearsAgo))
                .count();
    }

    private double getAveragePageCount() {
        List<Book> allBooks = bookService.findAllBooks();
        if (allBooks.isEmpty()) {
            return 0;
        }
        return allBooks.stream()
                .mapToInt(Book::getPageCount)
                .average()
                .orElse(0);
    }
}

Step 5: Configure an info contributor

Create a custom info contributor to provide additional application information:

package com.example.bookcatalog.actuator;

import com.example.bookcatalog.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class BookCatalogInfoContributor implements InfoContributor {

    private final BookService bookService;

    @Autowired
    public BookCatalogInfoContributor(BookService bookService) {
        this.bookService = bookService;
    }

    @Override
    public void contribute(Info.Builder builder) {
        Map<String, Object> bookDetails = new HashMap<>();
        bookDetails.put("totalBooks", bookService.findAllBooks().size());

        // Group books by genre
        Map<String, Long> booksByGenre = new HashMap<>();
        bookService.findAllBooks().forEach(book -> {
            String genre = book.getGenre() != null ? book.getGenre() : "Uncategorized";
            booksByGenre.put(genre, booksByGenre.getOrDefault(genre, 0L) + 1);
        });
        bookDetails.put("booksByGenre", booksByGenre);

        builder.withDetail("bookCatalog", bookDetails);
    }
}

Step 6: Run and explore Actuator endpoints

  1. Start the application

  2. Access the following Actuator endpoints:

  • Health endpoint: http://localhost:8081/actuator/health
  • Info endpoint: http://localhost:8081/actuator/info
  • Metrics endpoint: http://localhost:8081/actuator/metrics
  • Specific metric: http://localhost:8081/actuator/metrics/bookcatalog.books.count
  • Mappings endpoint: http://localhost:8081/actuator/mappings
  • Beans endpoint: http://localhost:8081/actuator/beans
  • Environment endpoint: http://localhost:8081/actuator/env
  1. Observe the output of each endpoint and understand the information provided

Step 7: Secure Actuator endpoints (Optional)

Add Spring Security to restrict access to Actuator endpoints:

  1. Add the Spring Security dependency to your pom.xml:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  1. Create a security configuration class:
package com.example.bookcatalog.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorizeRequests ->
                authorizeRequests
                    .requestMatchers("/api/**").permitAll()
                    .requestMatchers("/actuator/health", "/actuator/info").permitAll()
                    .requestMatchers("/actuator/**").hasRole("ADMIN")
                    .anyRequest().authenticated()
            )
            .httpBasic();

        return http.build();
    }
}
  1. Add user credentials in application.properties:
# Security configuration
spring.security.user.name=admin
spring.security.user.password=admin123
spring.security.user.roles=ADMIN

Step 8: Add API documentation with SpringDoc OpenAPI

  1. Add the SpringDoc OpenAPI dependency to your pom.xml:
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.2.0</version>
</dependency>
  1. Create an OpenAPI configuration class:
package com.example.bookcatalog.config;

import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI bookCatalogOpenAPI() {
        return new OpenAPI()
                .info(new Info().title("Book Catalog API")
                        .description("Spring Boot REST API for managing a book catalog")
                        .version("v1.0.0")
                        .contact(new Contact()
                                .name("Your Name")
                                .url("https://github.com/yourusername")
                                .email("[email protected]"))
                        .license(new License().name("Apache 2.0").url("http://springdoc.org")))
                .externalDocs(new ExternalDocumentation()
                        .description("Book Catalog API Documentation")
                        .url("https://github.com/yourusername/bookcatalog"));
    }
}
  1. Enhance your controller with OpenAPI annotations:
package com.example.bookcatalog.controller;

import com.example.bookcatalog.model.Book;
import com.example.bookcatalog.service.BookService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
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")
@Tag(name = "Book Controller", description = "Operations pertaining to books in the catalog")
public class BookController {

    private final BookService bookService;

    @Autowired
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    @Operation(summary = "Get all books", description = "Returns a list of all books in the catalog")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "Successfully retrieved books",
                    content = { @Content(mediaType = "application/json",
                                schema = @Schema(implementation = Book.class)) })
    })
    @GetMapping
    public ResponseEntity<List<Book>> getAllBooks() {
        List<Book> books = bookService.findAllBooks();
        return ResponseEntity.ok(books);
    }

    @Operation(summary = "Get a book by ID", description = "Returns a single book identified by its ID")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "Successfully retrieved book",
                    content = { @Content(mediaType = "application/json",
                                schema = @Schema(implementation = Book.class)) }),
        @ApiResponse(responseCode = "404", description = "Book not found",
                    content = @Content)
    })
    @GetMapping("/{id}")
    public ResponseEntity<Book> getBookById(
            @Parameter(description = "ID of the book to be retrieved") @PathVariable Long id) {
        Book book = bookService.findBookById(id);
        return ResponseEntity.ok(book);
    }

    @Operation(summary = "Create a new book", description = "Adds a new book to the catalog")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "201", description = "Book successfully created",
                    content = { @Content(mediaType = "application/json",
                                schema = @Schema(implementation = Book.class)) }),
        @ApiResponse(responseCode = "400", description = "Invalid input",
                    content = @Content)
    })
    @PostMapping
    public ResponseEntity<Book> createBook(
            @Parameter(description = "Book object to be added") @Valid @RequestBody Book book) {
        Book createdBook = bookService.createBook(book);
        return new ResponseEntity<>(createdBook, HttpStatus.CREATED);
    }

    @Operation(summary = "Update a book", description = "Updates an existing book in the catalog")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "Book successfully updated",
                    content = { @Content(mediaType = "application/json",
                                schema = @Schema(implementation = Book.class)) }),
        @ApiResponse(responseCode = "400", description = "Invalid input",
                    content = @Content),
        @ApiResponse(responseCode = "404", description = "Book not found",
                    content = @Content)
    })
    @PutMapping("/{id}")
    public ResponseEntity<Book> updateBook(
            @Parameter(description = "ID of the book to be updated") @PathVariable Long id,
            @Parameter(description = "Updated book object") @Valid @RequestBody Book book) {
        Book updatedBook = bookService.updateBook(id, book);
        return ResponseEntity.ok(updatedBook);
    }

    @Operation(summary = "Delete a book", description = "Removes a book from the catalog")
    @ApiResponses(value = {
        @ApiResponse(responseCode = "204", description = "Book successfully deleted",
                    content = @Content),
        @ApiResponse(responseCode = "404", description = "Book not found",
                    content = @Content)
    })
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteBook(
            @Parameter(description = "ID of the book to be deleted") @PathVariable Long id) {
        bookService.deleteBook(id);
        return ResponseEntity.noContent().build();
    }
}
  1. Configure OpenAPI in application.properties:
# SpringDoc OpenAPI configuration
springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui.html
springdoc.swagger-ui.operationsSorter=method
  1. Access the Swagger UI:
  • Open your browser and navigate to http://localhost:8081/swagger-ui.html

Step 9: Reflection and Additional Tasks

  1. Explore the Actuator endpoints and document the information each provides
  2. Try adding custom metrics for different aspects of your application
  3. Experiment with different Actuator configurations
  4. Practice using the Swagger UI to test your API endpoints

Summary

In this module, we’ve built a comprehensive RESTful API for a Book Catalog application using Spring Boot. Here’s what you’ve learned:

  1. REST Principles: You’ve gained an understanding of the core REST principles and how they apply to API design, including resource naming, HTTP methods, and status codes.

  2. Spring Boot REST Components: You’ve implemented a complete API using Spring Boot’s REST support, including controllers, services, repositories, and exception handling.

  3. Request Validation: You’ve learned how to validate incoming requests using Bean Validation annotations to ensure data integrity.

  4. Exception Handling: You’ve created a global exception handler to provide consistent error responses across your API.

  5. API Documentation: You’ve implemented OpenAPI documentation using SpringDoc to make your API discoverable and usable by clients.

  6. Application Monitoring: You’ve added Spring Boot Actuator to expose health, metrics, and other information about your application.

  7. Custom Metrics and Health Indicators: You’ve extended the basic monitoring capabilities with custom metrics and health checks.

  8. API Security (Optional): You’ve learned how to secure sensitive endpoints using Spring Security.


By Wahid Hamdi