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.
- Create a new Spring Boot project with the following details:
- Group: com.example
- Artifact: bookcatalog
- Dependencies: Spring Web, Spring Boot DevTools, Spring Validation
- 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
- Run the application:
- From IDE: Run the main application class
BookcatalogApplication.java
- With Maven:
mvn spring-boot:run
- With Gradle:
./gradlew bootRun
- 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
-
Start the application
-
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
- 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:
- Add the Spring Security dependency to your
pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 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();
}
}
- 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
- 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>
- 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"));
}
}
- 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();
}
}
- 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
- Access the Swagger UI:
- Open your browser and navigate to http://localhost:8081/swagger-ui.html
Step 9: Reflection and Additional Tasks
- Explore the Actuator endpoints and document the information each provides
- Try adding custom metrics for different aspects of your application
- Experiment with different Actuator configurations
- 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:
-
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.
-
Spring Boot REST Components: You’ve implemented a complete API using Spring Boot’s REST support, including controllers, services, repositories, and exception handling.
-
Request Validation: You’ve learned how to validate incoming requests using Bean Validation annotations to ensure data integrity.
-
Exception Handling: You’ve created a global exception handler to provide consistent error responses across your API.
-
API Documentation: You’ve implemented OpenAPI documentation using SpringDoc to make your API discoverable and usable by clients.
-
Application Monitoring: You’ve added Spring Boot Actuator to expose health, metrics, and other information about your application.
-
Custom Metrics and Health Indicators: You’ve extended the basic monitoring capabilities with custom metrics and health checks.
-
API Security (Optional): You’ve learned how to secure sensitive endpoints using Spring Security.
By Wahid Hamdi