REST API for managing a fruit shop inventory developed with Spring Boot and H2 in-memory database. Project created following TDD (Test-Driven Development) methodology with MVC architecture.
- Description
- Technologies
- Architecture
- Endpoints
- Installation
- Execution
- Tests
- Docker
- Project Structure
- Validations
- Error Handling
This backend application allows managing a fruit shop inventory, recording for each entry:
- Product name
- Weight in kilograms
The project implements a complete CRUD (Create, Read, Update, Delete) following development best practices:
β
Outside-In TDD with integration tests
β
MVC Architecture (Model-View-Controller)
β
DTO separation (Request/Response)
β
Bean Validation for data validation
β
Global exception handling
β
H2 in-memory database
β
Dockerized with multi-stage build
- Java 21 (LTS)
- Spring Boot 3.x
- Maven - Dependency management
- Spring Web - REST API
- Spring Data JPA - Persistence
- Spring Boot Actuator - Health checks
- Spring Boot DevTools - Development
- H2 Database - In-memory SQL database
- JUnit 5 - Testing framework
- Spring Boot Test - Integration tests
- MockMvc - Controller tests
- Mockito - Mocking
- Jakarta Validation - Data validation
- Lombok - Boilerplate reduction
- Docker - Containerization
- Docker Multi-stage Build - Image optimization
The project follows the MVC (Model-View-Controller) pattern with the following layers:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CLIENT (Postman, cURL) β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β HTTP Requests
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CONTROLLER LAYER β
β β’ FruitController β
β β’ Handles HTTP requests β
β β’ Validates input data (@Valid) β
β β’ Returns ResponseEntity with proper HTTP codes β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β SERVICE LAYER β
β β’ FruitService β
β β’ Business logic β
β β’ Transactions (@Transactional) β
β β’ Entity β DTO mapping β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β REPOSITORY LAYER β
β β’ FruitRepository (JpaRepository) β
β β’ Data access β
β β’ Automatically implemented by Spring Data JPA β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DATABASE LAYER β
β β’ H2 Database (in-memory) β
β β’ Table: fruits β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EXCEPTION HANDLING β
β β’ GlobalExceptionHandler (@RestControllerAdvice) β
β β’ Centralized error handling β
β β’ Returns consistent ErrorResponse β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
DTOs (Data Transfer Objects)
FruitRequestDTO: Input data (validated)FruitResponseDTO: Output data
-
Mapper
FruitMapper: Entity β DTO conversion
-
Exceptions
FruitNotFoundException: Fruit not foundGlobalExceptionHandler: Centralized error handling
Base URL: http://localhost:9000
Endpoint: POST /fruits
Request Body:
{
"name": "Apple",
"weightInKilos": 5
}Response: 201 Created
{
"id": 1,
"name": "Apple",
"weightInKilos": 5
}Errors:
400 Bad Request- Invalid data (empty name, negative weight)
Endpoint: GET /fruits
Response: 200 OK
[
{
"id": 1,
"name": "Apple",
"weightInKilos": 5
},
{
"id": 2,
"name": "Banana",
"weightInKilos": 3
}
]If no fruits: 200 OK with empty array []
Endpoint: GET /fruits/{id}
Response: 200 OK
{
"id": 1,
"name": "Apple",
"weightInKilos": 5
}Errors:
404 Not Found- Fruit doesn't exist
{
"status": 404,
"message": "Fruit not found with id: 999",
"timestamp": "2025-12-11T10:30:00"
}Endpoint: PUT /fruits/{id}
Request Body:
{
"name": "Green Apple",
"weightInKilos": 10
}Response: 200 OK
{
"id": 1,
"name": "Green Apple",
"weightInKilos": 10
}Errors:
404 Not Found- Fruit doesn't exist400 Bad Request- Invalid data
Endpoint: DELETE /fruits/{id}
Response: 204 No Content
Errors:
404 Not Found- Fruit doesn't exist
Endpoint: GET /actuator/health
Response: 200 OK
{
"status": "UP"
}git clone https://github.com/your-username/04.01_fruit-api-h2.git
cd 04.01_fruit-api-h2# With Maven Wrapper (recommended)
./mvnw clean install
# Or with Maven installed
mvn clean install./mvnw spring-boot:run# Build
./mvnw clean package
# Run
java -jar target/fruit-api-h2-0.0.1-SNAPSHOT.jarRun main class:
cat.itacademy.s04.t02.n01.FruitApiH2Application
curl http://localhost:9000/actuator/healthYou should see:
{"status":"UP"}The project is developed with TDD (Test-Driven Development) following the Outside-In approach.
./mvnw test./mvnw test jacoco:reportReport will be at: target/site/jacoco/index.html
- Test the complete application flow
- Use
@SpringBootTest+@AutoConfigureMockMvc - Real H2 database
- Automatic rollback with
@Transactional
Example:
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class FruitControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void testCreateFruit_Success() throws Exception {
mockMvc.perform(post("/fruits")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Apple\",\"weightInKilos\":5}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("Apple"));
}
}- Test isolated business logic
- Use
@ExtendWith(MockitoExtension.class) - Mock Repository
- Verify Entity β DTO conversions
Development followed the cycle:
1. π΄ RED β Write test (fails)
2. π’ GREEN β Implement minimal code (passes)
3. π΅ REFACTOR β Improve code (tests still pass)
4. πΎ COMMIT β Save progress
Implementation order:
- POST /fruits
- GET /fruits
- GET /fruits/{id}
- PUT /fruits/{id}
- DELETE /fruits/{id}
The project includes an optimized Dockerfile with two-stage build:
Stage 1 - BUILD:
- Uses
maven:3.9.6-amazoncorretto-21 - Compiles the application
- Generates the
.jarfile
Stage 2 - RUNTIME:
- Uses
amazoncorretto:21-alpine(lightweight) - Only copies the
.jar - Runs with non-root user
- Includes health check
Benefit: Final image ~180MB vs ~850MB (78% reduction)
docker build -t fruit-api:1.0 .# Foreground
docker run -p 9000:9000 fruit-api:1.0
# Background
docker run -d -p 9000:9000 --name fruit-api fruit-api:1.0docker run -d \
-p 9000:9000 \
-e JAVA_OPTS="-Xms512m -Xmx1024m" \
-e SPRING_PROFILES_ACTIVE=prod \
--name fruit-api \
fruit-api:1.0# View logs
docker logs -f fruit-api
# Health check
docker inspect --format='{{json .State.Health}}' fruit-api
# Access container
docker exec -it fruit-api shfruit-api-h2/
βββ src/
β βββ main/
β β βββ java/
β β β βββ cat/itacademy/s04/t02/n01/
β β β βββ FruitApiH2Application.java # Main class
β β β βββ controllers/
β β β β βββ FruitController.java # REST Controller
β β β βββ services/
β β β β βββ FruitService.java # Business logic
β β β βββ repository/
β β β β βββ FruitRepository.java # Data access
β β β βββ model/
β β β β βββ Fruit.java # JPA Entity
β β β βββ dto/
β β β β βββ FruitRequestDTO.java # Input DTO
β β β β βββ FruitResponseDTO.java # Output DTO
β β β βββ mapper/
β β β β βββ FruitMapper.java # Entity β DTO
β β β βββ exceptions/
β β β βββ FruitNotFoundException.java # Custom exception
β β β βββ ErrorResponse.java # Error response
β β β βββ GlobalExceptionHandler.java # Global handling
β β βββ resources/
β β βββ application.properties # Configuration
β β βββ application-prod.properties # Prod config
β βββ test/
β βββ java/
β βββ cat/itacademy/s04/t02/n01/
β βββ controllers/
β βββ FruitControllerIntegrationTest.java
βββ Dockerfile # Multi-stage build
βββ .dockerignore # Docker exclusions
βββ pom.xml # Maven dependencies
βββ README.md # This file
The project implements validations with Bean Validation:
public class FruitRequestDTO {
@NotBlank(message = "Name cannot be empty")
private String name;
@Positive(message = "Weight must be greater than 0")
private int weightInKilos;
}| Field | Validation | Error Message |
|---|---|---|
name |
Not empty | "Name cannot be empty" |
weightInKilos |
Greater than 0 | "Weight must be greater than 0" |
Request:
{
"name": "",
"weightInKilos": -5
}Response: 400 Bad Request
{
"status": 400,
"message": "Name cannot be empty, Weight must be greater than 0",
"timestamp": "2025-12-11T10:30:00"
}Handles all exceptions centrally:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(FruitNotFoundException.class)
public ResponseEntity<ErrorResponse> handleFruitNotFound(...) {
// Returns 404 Not Found
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(...) {
// Returns 400 Bad Request
}
}| Code | Situation |
|---|---|
| 200 OK | Successful operation (GET, PUT) |
| 201 Created | Resource created (POST) |
| 204 No Content | Successful deletion (DELETE) |
| 400 Bad Request | Validation failed |
| 404 Not Found | Resource not found |
| 500 Internal Server Error | Server error |
{
"status": 404,
"message": "Fruit not found with id: 999",
"timestamp": "2025-12-11T10:30:00"
}# H2 Database
spring.datasource.url=jdbc:h2:mem:fruitdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# JPA
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
# H2 Console (for debugging)
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# Actuator
management.endpoints.web.exposure.include=health,info
management.endpoint.health.show-details=when-authorized- Run the application
- Go to: http://localhost:8080/h2-console
- JDBC URL:
jdbc:h2:mem:fruitdb - Username:
sa - Password: (empty)
# 1. Create fruit
curl -X POST http://localhost:9000/fruits \
-H "Content-Type: application/json" \
-d '{"name":"Apple","weightInKilos":5}'
# 2. Get all
curl http://localhost:9000/fruits
# 3. Get by ID
curl http://localhost:9000/fruits/1
# 4. Update
curl -X PUT http://localhost:9000/fruits/1 \
-H "Content-Type: application/json" \
-d '{"name":"Green Apple","weightInKilos":10}'
# 5. Delete
curl -X DELETE http://localhost:9000/fruits/1- Import collection from:
docs/Fruit-API.postman_collection.json(if you create it) - Execute requests from the interface
This project allows learning and applying:
β
REST API with Spring Boot
β
Complete CRUD with proper HTTP verbs
β
MVC Architecture in layers
β
Persistence with Spring Data JPA
β
H2 in-memory database
β
DTOs to separate layers
β
Validations with Bean Validation
β
Global exception handling
β
Outside-In TDD with integration tests
β
Docker with multi-stage build
β
Health checks with Actuator
β
Security (non-root user in Docker)
β
Development best practices