Welcome to Chapter 10! Up until now, we’ve focused on building standalone, console-based applications in Java. While these are excellent for understanding core programming concepts, most real-world applications today involve a backend service that communicates with a user interface, mobile app, or other services. This chapter marks a significant pivot as we introduce you to building a robust, scalable, and production-ready RESTful API using the Spring Boot framework.
In this chapter, we will lay the foundation for our “Basic To-Do List Application” by creating its backend API. We’ll leverage Spring Web to define endpoints, Spring Data JPA for database interaction, and an in-memory H2 database for local development. By the end of this chapter, you will have a running Spring Boot application with basic API endpoints to create and retrieve To-Do items, complete with proper error handling, logging, and input validation. This step is crucial for understanding how modern Java applications expose their functionality to the outside world, setting the stage for more complex features and a user interface in future chapters.
Planning & Design
Before we dive into coding, let’s outline the architecture and API design for our To-Do List backend.
Component Architecture
We will adopt a layered architecture, which is a standard practice in enterprise applications:
- Controller Layer (
TodoController): Handles incoming HTTP requests, delegates to the service layer, and returns HTTP responses. - Service Layer (
TodoService): Contains the business logic, orchestrates data operations, and ensures data integrity. - Repository Layer (
TodoRepository): Provides an abstraction over the data storage, handling CRUD (Create, Read, Update, Delete) operations. We’ll use Spring Data JPA for this. - Entity Layer (
Todo): Represents the data structure in our database. - DTO Layer (
TodoDTO): Data Transfer Objects used for request and response bodies to decouple the API from the internal entity structure. - Database: For this chapter, we’ll use an in-memory H2 database, perfect for development and testing.
API Endpoints Design
We’ll start with two essential endpoints for our To-Do List:
POST /api/todos: Creates a new To-Do item.- Request Body:
TodoCreationRequestDTO(e.g.,{"description": "Learn Spring Boot"}). - Response:
TodoDTOof the newly created To-Do item, with HTTP Status201 Created. - Error Cases:
400 Bad Requestif input is invalid.
- Request Body:
GET /api/todos: Retrieves a list of all To-Do items.- Request Parameters: None for now.
- Response: A list of
TodoDTOobjects, with HTTP Status200 OK.
Database Schema (H2)
Our Todo entity will be straightforward:
id:BIGINT(Primary Key, auto-generated)description:VARCHAR(255)(Not Null)completed:BOOLEAN(Defaultfalse)createdAt:TIMESTAMP(Automatically set on creation)updatedAt:TIMESTAMP(Automatically set on update)
File Structure
We’ll adhere to the standard Spring Boot project structure:
src/main/java/com/simpleappcollection/todoapi/
├── TodoApiServiceApplication.java # Main entry point
├── config/ # Configuration classes
├── controller/ # REST Controllers
│ └── TodoController.java
├── dto/ # Data Transfer Objects
│ ├── TodoCreationRequestDTO.java
│ └── TodoDTO.java
├── entity/ # JPA Entities
│ └── Todo.java
├── exception/ # Custom Exception handling
│ └── GlobalExceptionHandler.java
├── repository/ # Spring Data JPA Repositories
│ └── TodoRepository.java
└── service/ # Business Logic Services
└── TodoService.java
src/main/resources/
├── application.properties # Spring Boot configuration
└── logback-spring.xml # Logging configuration
Step-by-Step Implementation
1. Setup/Configuration: Create a Spring Boot Project
We’ll start by generating a new Spring Boot project. We’ll use Java 25, the latest stable version as of December 2025, and the latest stable Spring Boot 3.x release (e.g., 3.4.x).
a) Generate Project with Spring Initializr
Navigate to start.spring.io. Configure the project with the following settings:
- Project: Maven Project
- Language: Java
- Spring Boot: Latest stable (e.g.,
3.4.0- adjust if a newer3.xis available) - Project Metadata:
- Group:
com.simpleappcollection - Artifact:
todo-api-service - Name:
todo-api-service - Description:
API for the Basic To-Do List Application - Package Name:
com.simpleappcollection.todoapi - Packaging: Jar
- Java: 25
- Group:
- Dependencies:
Spring Web(for building RESTful APIs)Spring Data JPA(for database interaction)H2 Database(in-memory database for development)Lombok(optional, but highly recommended for reducing boilerplate code)Validation(for input validation)Spring Boot Actuator(for production-ready features like monitoring)
Click “Generate” and download the todo-api-service.zip file. Extract it to your desired project directory.
b) Project Structure Overview
After extracting, open the project in your IDE (IntelliJ IDEA, VS Code, Eclipse). You should see a structure similar to what was planned.
c) Configure application.properties
Open src/main/resources/application.properties. We’ll configure our H2 database and enable the H2 console for easy inspection during development.
# src/main/resources/application.properties
# Server Port
server.port=8080
# H2 Database Configuration (in-memory for development)
spring.datasource.url=jdbc:h2:mem:tododb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.h2.console.settings.web-allow-others=true # Allow remote access for testing (be cautious in production)
# JPA/Hibernate Configuration
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update # Automatically create/update schema (use 'validate' or 'none' in production)
spring.jpa.show-sql=true # Log SQL queries to console
spring.jpa.properties.hibernate.format_sql=true # Format SQL for readability
# Actuator Endpoints
management.endpoints.web.exposure.include=* # Expose all actuator endpoints
management.endpoint.health.show-details=always # Show full health details
Why these configurations?
server.port: Specifies the port where our API will run.8080is standard.spring.datasource.*: Configures H2 to run in-memory, meaning data is lost when the application restarts. This is ideal for development.spring.h2.console.enabled=true: Enables a web-based console to view and manage the H2 database.spring.h2.console.path=/h2-console: Sets the access path for the H2 console.spring.jpa.hibernate.ddl-auto=update: Hibernate will automatically create or update database tables based on our JPA entities. Crucially, this should be set tovalidateornonein production to prevent unintended schema changes.spring.jpa.show-sql=trueandformat_sql=true: Useful for debugging database interactions during development.management.endpoints.web.exposure.include=*: Exposes all Spring Boot Actuator endpoints (e.g.,/actuator/health,/actuator/info) for monitoring our application.
d) Configure Logging with logback-spring.xml
For production-ready applications, detailed and configurable logging is essential. Spring Boot uses Logback by default. Let’s create a logback-spring.xml to customize our logging output.
Create a new file: src/main/resources/logback-spring.xml
<!-- src/main/resources/logback-spring.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}/logs/}}spring-boot-application.log}"/>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<file>${LOG_FILE}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
<!-- Suppress verbose logging from specific packages -->
<logger name="org.springframework" level="WARN"/>
<logger name="org.hibernate" level="WARN"/>
<logger name="org.apache.catalina" level="WARN"/>
<logger name="com.zaxxer.hikari" level="WARN"/>
<!-- Custom logger for our application package -->
<logger name="com.simpleappcollection.todoapi" level="DEBUG"/>
</configuration>
Why this logging configuration?
- It includes Spring Boot’s default console appender, but also adds a file appender.
RollingFileAppender: This ensures logs are written to a file (spring-boot-application.log) and rotated based on size (10MB) and time (daily), compressing old logs. This prevents log files from growing indefinitely, which is critical in production.root level="INFO": Sets the default logging level for the application to INFO.- Specific loggers for
org.springframework,org.hibernate, etc., are set toWARNto reduce console noise from framework internals, allowing us to focus on our application’s logs. com.simpleappcollection.todoapiis set toDEBUGto get detailed logs from our application code during development.
2. Core Implementation
Now, let’s build the core components of our To-Do API.
a) Create the Todo Entity
This class represents our To-Do item in the database.
File: src/main/java/com/simpleappcollection/todoapi/entity/Todo.java
package com.simpleappcollection.todoapi.entity;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
@Entity // Marks this class as a JPA entity
@Table(name = "todos") // Specifies the table name in the database
@Data // Lombok annotation to generate getters, setters, toString, equals, and hashCode
@NoArgsConstructor // Lombok annotation to generate a no-argument constructor
@AllArgsConstructor // Lombok annotation to generate an all-argument constructor
public class Todo {
@Id // Marks this field as the primary key
@GeneratedValue(strategy = GenerationType.IDENTITY) // Configures ID generation strategy (auto-increment)
private Long id;
@Column(nullable = false) // Specifies that the column cannot be null
private String description;
@Column(nullable = false)
private boolean completed = false; // Default value is false
@CreationTimestamp // Automatically sets the creation timestamp
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp // Automatically updates the timestamp on entity modification
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
}
Explanation:
@Entity,@Table: Standard JPA annotations to map the class to a database table.@Data,@NoArgsConstructor,@AllArgsConstructor: Lombok annotations that automatically generate boilerplate code (getters, setters, constructors) reducing verbosity.@Id,@GeneratedValue(strategy = GenerationType.IDENTITY): Defines the primary key and uses the database’s auto-increment feature for ID generation.@Column: Configures column properties likenullableandname.@CreationTimestamp,@UpdateTimestamp: Hibernate-specific annotations that automatically manage thecreatedAtandupdatedAtfields, crucial for auditing and tracking changes.
b) Create the TodoRepository Interface
This interface extends Spring Data JPA’s JpaRepository, providing ready-to-use CRUD operations without writing any implementation code.
File: src/main/java/com/simpleappcollection/todoapi/repository/TodoRepository.java
package com.simpleappcollection.todoapi.repository;
import com.simpleappcollection.todoapi.entity.Todo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository // Marks this interface as a Spring Data repository
public interface TodoRepository extends JpaRepository<Todo, Long> {
// Spring Data JPA automatically provides methods like save(), findById(), findAll(), deleteById()
// Example of a custom query method (not used in this chapter, but for future reference)
List<Todo> findByCompleted(boolean completed);
}
Explanation:
@Repository: A Spring stereotype annotation indicating that this is a repository component.extends JpaRepository<Todo, Long>: Inheriting fromJpaRepositoryprovides a wealth of methods forTodoentities, usingLongas the type for the primary key. Spring automatically generates the implementation at runtime.
c) Create Data Transfer Objects (DTOs)
DTOs are crucial for defining the contract of our API, separating the external representation from the internal entity. This helps with versioning, security, and preventing over-exposure of internal data.
File: src/main/java/com/simpleappcollection/todoapi/dto/TodoDTO.java
package com.simpleappcollection.todoapi.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDateTime;
@Data // Lombok: Getters, Setters, toString, equals, hashCode
@NoArgsConstructor // Lombok: No-arg constructor
@AllArgsConstructor // Lombok: All-arg constructor
public class TodoDTO {
private Long id;
private String description;
private boolean completed;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
File: src/main/java/com/simpleappcollection/todoapi/dto/TodoCreationRequestDTO.java
package com.simpleappcollection.todoapi.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Data // Lombok: Getters, Setters, toString, equals, hashCode
@NoArgsConstructor // Lombok: No-arg constructor
@AllArgsConstructor // Lombok: All-arg constructor
public class TodoCreationRequestDTO {
@NotBlank(message = "Description cannot be empty") // Ensures the description is not null or empty
@Size(min = 3, max = 255, message = "Description must be between 3 and 255 characters") // Length constraints
private String description;
}
Explanation:
TodoDTO: Represents the full To-Do item when returned by the API.TodoCreationRequestDTO: Used specifically for creating a new To-Do. It only contains thedescriptionasid,completed, and timestamps are managed by the backend.@NotBlank,@Size: Jakarta Validation (JSR 380) annotations to ensure that thedescriptionfield in the request is present and meets length requirements. This is critical for input validation and security.
d) Implement the TodoService
The service layer contains the business logic. It orchestrates interactions between the controller and the repository.
File: src/main/java/com/simpleappcollection/todoapi/service/TodoService.java
package com.simpleappcollection.todoapi.service;
import com.simpleappcollection.todoapi.dto.TodoCreationRequestDTO;
import com.simpleappcollection.todoapi.dto.TodoDTO;
import com.simpleappcollection.todoapi.entity.Todo;
import com.simpleappcollection.todoapi.repository.TodoRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Service // Marks this class as a Spring Service component
public class TodoService {
private static final Logger log = LoggerFactory.getLogger(TodoService.class);
private final TodoRepository todoRepository;
// Constructor Injection: Spring automatically injects TodoRepository
public TodoService(TodoRepository todoRepository) {
this.todoRepository = todoRepository;
}
@Transactional(readOnly = true) // Marks the method as read-only for transactions, optimizing performance
public List<TodoDTO> getAllTodos() {
log.debug("Fetching all To-Do items.");
List<Todo> todos = todoRepository.findAll();
// Convert entities to DTOs before returning
return todos.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
@Transactional // Marks the method as transactional for write operations
public TodoDTO createTodo(TodoCreationRequestDTO requestDTO) {
log.info("Attempting to create a new To-Do with description: {}", requestDTO.getDescription());
Todo todo = new Todo();
todo.setDescription(requestDTO.getDescription());
// 'completed' defaults to false, timestamps are handled by JPA annotations
Todo savedTodo = todoRepository.save(todo);
log.info("Successfully created To-Do with ID: {}", savedTodo.getId());
return convertToDto(savedTodo);
}
// Helper method to convert Entity to DTO
private TodoDTO convertToDto(Todo todo) {
return new TodoDTO(
todo.getId(),
todo.getDescription(),
todo.isCompleted(),
todo.getCreatedAt(),
todo.getUpdatedAt()
);
}
}
Explanation:
@Service: A Spring stereotype annotation indicating this is a service component.Logger log = LoggerFactory.getLogger(TodoService.class): Standard SLF4J logger for robust logging. We use{}for parameterized logging to avoid String concatenation overhead.- Constructor Injection: The
TodoRepositoryis injected via the constructor, which is the recommended practice for dependency injection in Spring. @Transactional(readOnly = true): ForgetAllTodos, this annotation ensures the method runs within a transaction and hints to the JPA provider that it’s a read-only operation, potentially optimizing performance.@Transactional: ForcreateTodo, this ensures that the entire operation (saving to the database) is atomic. If any part fails, the transaction is rolled back.- Entity to DTO Conversion: The
convertToDtohelper method maps the internalTodoentity to the externalTodoDTO, ensuring data encapsulation.
e) Implement the TodoController
This is our REST endpoint, handling HTTP requests and returning responses.
File: src/main/java/com/simpleappcollection/todoapi/controller/TodoController.java
package com.simpleappcollection.todoapi.controller;
import com.simpleappcollection.todoapi.dto.TodoCreationRequestDTO;
import com.simpleappcollection.todoapi.dto.TodoDTO;
import com.simpleappcollection.todoapi.service.TodoService;
import jakarta.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController // Marks this class as a REST controller, combining @Controller and @ResponseBody
@RequestMapping("/api/todos") // Base path for all endpoints in this controller
public class TodoController {
private static final Logger log = LoggerFactory.getLogger(TodoController.class);
private final TodoService todoService;
// Constructor Injection
public TodoController(TodoService todoService) {
this.todoService = todoService;
}
@GetMapping // Maps HTTP GET requests to /api/todos
public ResponseEntity<List<TodoDTO>> getAllTodos() {
log.debug("Received request to get all To-Do items.");
List<TodoDTO> todos = todoService.getAllTodos();
return ResponseEntity.ok(todos); // Returns 200 OK with the list of To-Dos
}
@PostMapping // Maps HTTP POST requests to /api/todos
public ResponseEntity<TodoDTO> createTodo(@Valid @RequestBody TodoCreationRequestDTO requestDTO) {
log.debug("Received request to create a To-Do: {}", requestDTO.getDescription());
TodoDTO newTodo = todoService.createTodo(requestDTO);
// Returns 201 Created with the newly created To-Do item
return new ResponseEntity<>(newTodo, HttpStatus.CREATED);
}
}
Explanation:
@RestController: A convenience annotation that combines@Controllerand@ResponseBody, meaning all methods’ return values are automatically serialized into the HTTP response body.@RequestMapping("/api/todos"): Sets the base URI path for all handler methods in this controller.@GetMapping,@PostMapping: Map specific HTTP methods and URI paths to controller methods.@Valid @RequestBody TodoCreationRequestDTO requestDTO:@RequestBody: Indicates that the method parameter should be bound to the body of the HTTP request.@Valid: Triggers the validation process for therequestDTObased on the annotations (@NotBlank,@Size) defined inTodoCreationRequestDTO. If validation fails, aMethodArgumentNotValidExceptionis thrown.
ResponseEntity: A Spring class that allows fine-grained control over the HTTP response, including status codes, headers, and body.HttpStatus.CREATED: Returns an HTTP 201 status code, which is the appropriate response for a successful resource creation.
f) Global Exception Handling
To provide consistent and informative error responses, we’ll implement a global exception handler. This catches exceptions thrown by our controllers and services and translates them into appropriate HTTP responses.
File: src/main/java/com/simpleappcollection/todoapi/exception/GlobalExceptionHandler.java
package com.simpleappcollection.todoapi.exception;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
@ControllerAdvice // Global exception handler for all controllers
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, WebRequest request) {
log.warn("Validation error: {}", ex.getMessage());
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.BAD_REQUEST.value());
body.put("error", "Bad Request");
// Get all validation errors and format them
String errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining("; "));
body.put("message", "Validation failed: " + errors);
body.put("path", request.getDescription(false).replace("uri=", ""));
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleAllUncaughtException(Exception ex, WebRequest request) {
log.error("An unexpected error occurred: {}", ex.getMessage(), ex); // Log the full stack trace for unexpected errors
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
body.put("error", "Internal Server Error");
body.put("message", "An unexpected error occurred. Please try again later.");
body.put("path", request.getDescription(false).replace("uri=", ""));
return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Explanation:
@ControllerAdvice: This annotation makes the class a global handler for exceptions across all@Controllercomponents.@ExceptionHandler(MethodArgumentNotValidException.class): This method specifically handlesMethodArgumentNotValidException, which is thrown when@Validannotation fails on a request body.- It constructs a custom error response including timestamp, status, a descriptive error message with specific field validation failures, and the request path.
- It returns
HttpStatus.BAD_REQUEST (400).
@ExceptionHandler(Exception.class): This is a catch-all handler for any other unhandled exceptions. It logs the full stack trace (important for debugging unexpected issues) and returns a generic500 Internal Server Errormessage to the client, avoiding exposure of internal details.
3. Testing This Component
a) Start the Spring Boot Application
Navigate to the root of your todo-api-service project in your terminal and run the following Maven command:
mvn spring-boot:run
You should see logs indicating that Spring Boot is starting up, including:
- H2 database initialized.
- Tomcat started on port 8080.
- Actuator endpoints exposed.
- H2 console available at
/h2-console.
Keep an eye on the console for any error messages during startup.
b) Manual Testing with curl or Postman
Once the application is running, you can test the API endpoints.
Access H2 Console:
Open your browser and go to http://localhost:8080/h2-console.
Use the credentials from application.properties:
- JDBC URL:
jdbc:h2:mem:tododb - User Name:
sa - Password:
passwordClick “Connect”. You should see an emptyTODOStable.
1. Create a To-Do (POST /api/todos)
Open a new terminal or use Postman/Insomnia.
curl -X POST \
http://localhost:8080/api/todos \
-H 'Content-Type: application/json' \
-d '{
"description": "Learn to deploy Java apps"
}'
Expected Response (HTTP Status: 201 Created):
{
"id": 1,
"description": "Learn to deploy Java apps",
"completed": false,
"createdAt": "2025-12-04T10:30:00.123456",
"updatedAt": "2025-12-04T10:30:00.123456"
}
(Timestamps will vary)
Test Validation (POST /api/todos with invalid input)
curl -X POST \
http://localhost:8080/api/todos \
-H 'Content-Type: application/json' \
-d '{
"description": "ab"
}'
Expected Response (HTTP Status: 400 Bad Request):
{
"timestamp": "2025-12-04T10:31:00.123456",
"status": 400,
"error": "Bad Request",
"message": "Validation failed: description: Description must be between 3 and 255 characters",
"path": "/api/todos"
}
2. Get All To-Dos (GET /api/todos)
curl -X GET \
http://localhost:8080/api/todos
Expected Response (HTTP Status: 200 OK):
[
{
"id": 1,
"description": "Learn to deploy Java apps",
"completed": false,
"createdAt": "2025-12-04T10:30:00.123456",
"updatedAt": "2025-12-04T10:30:00.123456"
}
]
c) Unit Testing (Example for TodoService)
While manual testing is good, automated unit tests are essential for production-ready code. Let’s add a basic unit test for our TodoService.
File: src/test/java/com/simpleappcollection/todoapi/service/TodoServiceTest.java
package com.simpleappcollection.todoapi.service;
import com.simpleappcollection.todoapi.dto.TodoCreationRequestDTO;
import com.simpleappcollection.todoapi.dto.TodoDTO;
import com.simpleappcollection.todoapi.entity.Todo;
import com.simpleappcollection.todoapi.repository.TodoRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class) // Enables Mockito for JUnit 5
class TodoServiceTest {
@Mock // Mocks the TodoRepository dependency
private TodoRepository todoRepository;
@InjectMocks // Injects the mocked repository into TodoService
private TodoService todoService;
private Todo todo1;
private Todo todo2;
@BeforeEach // Runs before each test method
void setUp() {
todo1 = new Todo(1L, "Test Todo 1", false, LocalDateTime.now(), LocalDateTime.now());
todo2 = new Todo(2L, "Test Todo 2", true, LocalDateTime.now(), LocalDateTime.now());
}
@Test
void getAllTodos_shouldReturnListOfTodos() {
// Given
when(todoRepository.findAll()).thenReturn(Arrays.asList(todo1, todo2));
// When
List<TodoDTO> result = todoService.getAllTodos();
// Then
assertNotNull(result);
assertEquals(2, result.size());
assertEquals("Test Todo 1", result.get(0).getDescription());
assertEquals("Test Todo 2", result.get(1).getDescription());
}
@Test
void createTodo_shouldReturnCreatedTodo() {
// Given
TodoCreationRequestDTO requestDTO = new TodoCreationRequestDTO("New Test Todo");
Todo newTodoEntity = new Todo(3L, "New Test Todo", false, LocalDateTime.now(), LocalDateTime.now());
when(todoRepository.save(any(Todo.class))).thenReturn(newTodoEntity);
// When
TodoDTO result = todoService.createTodo(requestDTO);
// Then
assertNotNull(result);
assertEquals(3L, result.getId());
assertEquals("New Test Todo", result.getDescription());
assertEquals(false, result.isCompleted());
}
}
Explanation:
@ExtendWith(MockitoExtension.class): Integrates Mockito with JUnit 5.@Mock: Creates a mock instance ofTodoRepository. This means we don’t hit a real database during unit tests.@InjectMocks: Injects the mockedTodoRepositoryintoTodoService.@BeforeEach: Sets up common test data before each test.when(...).thenReturn(...): Defines the behavior of the mockedtodoRepositorywhen its methods are called.assert*: JUnit assertions to verify the expected outcomes.
Run Tests: You can run these tests from your IDE or using Maven:
mvn test
All tests should pass.
Production Considerations
Building a production-ready API requires more than just functional code.
- Error Handling: Our
GlobalExceptionHandlerprovides a good start for consistent error responses, specifically for validation issues and unexpected server errors. For more complex applications, you might introduce custom exception classes (e.g.,ResourceNotFoundException) and dedicated handlers for them. - Performance Optimization:
- Database Indexing: Ensure appropriate indexes are added to frequently queried columns (e.g.,
descriptionif we add search functionality). - Caching: For read-heavy operations, consider implementing caching layers (e.g., Spring Cache with Redis or Caffeine) to reduce database load.
- Efficient Queries: Use optimized JPA queries, DTO projections, or even native queries when necessary, rather than fetching entire entities for simple data.
- Database Indexing: Ensure appropriate indexes are added to frequently queried columns (e.g.,
- Security Considerations:
- Input Validation: We’ve implemented basic validation with
@Valid. This is a first line of defense against injection attacks and malformed data. Always validate all incoming data. - Authentication & Authorization: For a real application, you’d integrate Spring Security to handle user authentication (e.g., JWT, OAuth2) and authorization (role-based access control). This will be covered in future chapters for more complex projects.
- HTTPS: Always deploy APIs over HTTPS to encrypt communication and prevent man-in-the-middle attacks.
- Dependency Security: Regularly scan your project dependencies for known vulnerabilities using tools like OWASP Dependency-Check or Snyk.
- Input Validation: We’ve implemented basic validation with
- Logging and Monitoring:
- Our
logback-spring.xmlprovides file-based rolling logs, crucial for production. - Spring Boot Actuator endpoints (
/actuator/health,/actuator/info,/actuator/metrics) are exposed, providing valuable insights into the application’s health and performance. These should be integrated with monitoring systems like Prometheus/Grafana or ELK stack in a production environment. - Ensure logs contain sufficient context (e.g., request IDs, user IDs) for easier debugging in distributed systems.
- Our
Code Review Checkpoint
At this point, you have successfully built the foundational RESTful API for our To-Do List application.
Files Created/Modified:
pom.xml: Updated with Spring Boot and other dependencies.src/main/java/com/simpleappcollection/todoapi/TodoApiServiceApplication.java: Main application class (generated by Initializr).src/main/resources/application.properties: Configured H2 database, JPA, and Actuator.src/main/resources/logback-spring.xml: Custom logging configuration.src/main/java/com/simpleappcollection/todoapi/entity/Todo.java: JPA Entity for To-Do items.src/main/java/com/simpleappcollection/todoapi/repository/TodoRepository.java: Spring Data JPA Repository.src/main/java/com/simpleappcollection/todoapi/dto/TodoDTO.java: DTO for API responses.src/main/java/com/simpleappcollection/todoapi/dto/TodoCreationRequestDTO.java: DTO for API requests with validation.src/main/java/com/simpleappcollection/todoapi/service/TodoService.java: Business logic for To-Do operations.src/main/java/com/simpleappcollection/todoapi/controller/TodoController.java: REST Controller withGET /api/todosandPOST /api/todosendpoints.src/main/java/com/simpleappcollection/todoapi/exception/GlobalExceptionHandler.java: Centralized exception handling.src/test/java/com/simpleappcollection/todoapi/service/TodoServiceTest.java: Unit tests forTodoService.
How it integrates:
- The
TodoApiServiceApplicationclass bootstraps the Spring Boot application. - Spring’s component scanning automatically discovers and registers our
@RestController,@Service, and@Repositorycomponents. - Dependency Injection wires
TodoRepositoryintoTodoService, andTodoServiceintoTodoController. - Incoming HTTP requests to
/api/todosare routed toTodoController, which delegates toTodoServicefor business logic, andTodoServiceinteracts withTodoRepositoryto persist or retrieve data from the H2 database. GlobalExceptionHandlerensures that any errors encountered during this flow are handled gracefully and returned as standardized error responses.
Common Issues & Solutions
Port 8080 already in use:
- Error:
Address already in use: bindorPort 8080 was already in use. - Cause: Another application (e.g., another Spring Boot app, a web server) is using port 8080.
- Solution:
- Find and stop the process using the port:
- Linux/macOS:
sudo lsof -i :8080thenkill -9 <PID> - Windows:
netstat -ano | findstr :8080thentaskkill /PID <PID> /F
- Linux/macOS:
- Alternatively, change the port for your Spring Boot application in
src/main/resources/application.properties:server.port=8081.
- Find and stop the process using the port:
- Error:
H2 Console not accessible or “Database not found”:
- Error: Browser shows 404 for
/h2-consoleor H2 console connects but says “Database not found”. - Cause:
spring.h2.console.enabledisfalseor missing.- Incorrect JDBC URL in
application.propertiesor in the H2 console login. spring.h2.console.settings.web-allow-others=truemight be missing, preventing external access (though usually for remote access).
- Solution:
- Ensure
spring.h2.console.enabled=trueandspring.h2.console.path=/h2-consoleare correctly set inapplication.properties. - Double-check the JDBC URL in the H2 console login screen matches
jdbc:h2:mem:tododb. - Restart the application after making changes.
- Ensure
- Error: Browser shows 404 for
Validation errors not handled (e.g., 500 instead of 400 for bad input):
- Error: Sending an invalid
TodoCreationRequestDTO(e.g., empty description) results in a generic 500 Internal Server Error instead of a 400 Bad Request. - Cause:
- Missing
@Validannotation on therequestDTOparameter inTodoController.createTodo(). GlobalExceptionHandleris not correctly configured orMethodArgumentNotValidExceptionis not handled.
- Missing
- Solution:
- Ensure
@Validis present on the method parameter:public ResponseEntity<TodoDTO> createTodo(@Valid @RequestBody TodoCreationRequestDTO requestDTO). - Verify
GlobalExceptionHandler.javais correctly placed in a package scanned by Spring and that the@ControllerAdviceannotation is present.
- Ensure
- Error: Sending an invalid
Testing & Verification
To ensure everything is working as expected:
- Start the application: Run
mvn spring-boot:runfrom your project root. - Verify H2 Console: Open
http://localhost:8080/h2-console, connect tojdbc:h2:mem:tododb(usersa, passpassword). Confirm theTODOStable exists and is empty initially. - Test
POST /api/todos(valid input):Expectcurl -X POST http://localhost:8080/api/todos -H 'Content-Type: application/json' -d '{"description": "Buy groceries"}'201 Createdwith the new To-Do item. Check the H2 console; runSELECT * FROM TODOS;to see the new entry. - Test
POST /api/todos(invalid input):Expectcurl -X POST http://localhost:8080/api/todos -H 'Content-Type: application/json' -d '{"description": ""}'400 Bad Requestwith a detailed error message from ourGlobalExceptionHandler. - Test
GET /api/todos:Expectcurl -X GET http://localhost:8080/api/todos200 OKwith a JSON array containing the To-Do items you’ve created. - Check Logs: Review your application console/logs (
target/logs/spring-boot-application.log) forINFOandDEBUGmessages fromTodoServiceandTodoControllerto confirm operations are being logged. Also, check for anyWARNorERRORmessages. - Run Unit Tests: Execute
mvn test. All tests should pass, indicating that your service layer logic is correct and robust.
Summary & Next Steps
Congratulations! In this chapter, you’ve successfully transitioned from console applications to building a professional RESTful API using Spring Boot. You’ve learned how to:
- Initialize a Spring Boot project with essential dependencies.
- Configure the application for development, including an in-memory H2 database and logging.
- Design and implement a layered architecture (Controller, Service, Repository, Entity, DTO).
- Create a JPA entity and use Spring Data JPA for effortless database interaction.
- Develop RESTful endpoints for creating and retrieving resources.
- Implement input validation and global exception handling for robust API responses.
- Write basic unit tests for your service layer.
- Consider production aspects like logging, security, and performance.
This robust foundation for our “Basic To-Do List Application” is now ready. In the next chapter, Chapter 11, we will expand our API to include full CRUD (Create, Read, Update, Delete) operations, allowing us to retrieve individual To-Do items, update their status, and delete them. We’ll also explore more advanced error handling and potentially introduce pagination for the list endpoint.