Skip to content

eariasvalor/04.02-Level1-spring-boot-fruit-api-h2

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

13 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

🍎 Fruit API - REST API with Spring Boot and H2

Java Spring Boot Maven H2 Docker

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.


πŸ“‹ Table of Contents


🎯 Description

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


πŸ› οΈ Technologies

Core

  • Java 21 (LTS)
  • Spring Boot 3.x
  • Maven - Dependency management

Spring Modules

  • Spring Web - REST API
  • Spring Data JPA - Persistence
  • Spring Boot Actuator - Health checks
  • Spring Boot DevTools - Development

Database

  • H2 Database - In-memory SQL database

Testing

  • JUnit 5 - Testing framework
  • Spring Boot Test - Integration tests
  • MockMvc - Controller tests
  • Mockito - Mocking

Validation

  • Jakarta Validation - Data validation

Utilities

  • Lombok - Boilerplate reduction

DevOps

  • Docker - Containerization
  • Docker Multi-stage Build - Image optimization

πŸ—οΈ Architecture

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                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Additional Components

  • DTOs (Data Transfer Objects)

    • FruitRequestDTO: Input data (validated)
    • FruitResponseDTO: Output data
  • Mapper

    • FruitMapper: Entity ↔ DTO conversion
  • Exceptions

    • FruitNotFoundException: Fruit not found
    • GlobalExceptionHandler: Centralized error handling

🌐 Endpoints

Base URL: http://localhost:9000

1. Create Fruit

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)

2. Get All Fruits

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 []


3. Get Fruit by ID

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"
}

4. Update Fruit

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 exist
  • 400 Bad Request - Invalid data

5. Delete Fruit

Endpoint: DELETE /fruits/{id}

Response: 204 No Content

Errors:

  • 404 Not Found - Fruit doesn't exist

6. Health Check (Actuator)

Endpoint: GET /actuator/health

Response: 200 OK

{
  "status": "UP"
}

πŸš€ Installation

Prerequisites

Clone Repository

git clone https://github.com/your-username/04.01_fruit-api-h2.git
cd 04.01_fruit-api-h2

Build Project

# With Maven Wrapper (recommended)
./mvnw clean install

# Or with Maven installed
mvn clean install

▢️ Execution

Option 1: Run with Maven

./mvnw spring-boot:run

Option 2: Run JAR

# Build
./mvnw clean package

# Run
java -jar target/fruit-api-h2-0.0.1-SNAPSHOT.jar

Option 3: From IDE

Run main class:

cat.itacademy.s04.t02.n01.FruitApiH2Application

Verify It Works

curl http://localhost:9000/actuator/health

You should see:

{"status":"UP"}

πŸ§ͺ Tests

The project is developed with TDD (Test-Driven Development) following the Outside-In approach.

Run All Tests

./mvnw test

Run Tests with Coverage

./mvnw test jacoco:report

Report will be at: target/site/jacoco/index.html

Test Types

1. Integration Tests (FruitControllerIntegrationTest)

  • 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"));
    }
}

2. Service Unit Tests (FruitServiceTest)

  • Test isolated business logic
  • Use @ExtendWith(MockitoExtension.class)
  • Mock Repository

3. Mapper Tests (FruitMapperTest)

  • Verify Entity ↔ DTO conversions

Outside-In TDD Methodology

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:

  1. POST /fruits
  2. GET /fruits
  3. GET /fruits/{id}
  4. PUT /fruits/{id}
  5. DELETE /fruits/{id}

🐳 Docker

Multi-Stage Dockerfile

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 .jar file

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)

Build Image

docker build -t fruit-api:1.0 .

Run Container

# Foreground
docker run -p 9000:9000 fruit-api:1.0

# Background
docker run -d -p 9000:9000 --name fruit-api fruit-api:1.0

Environment Variables

docker run -d \
  -p 9000:9000 \
  -e JAVA_OPTS="-Xms512m -Xmx1024m" \
  -e SPRING_PROFILES_ACTIVE=prod \
  --name fruit-api \
  fruit-api:1.0

Verify Container Health

# View logs
docker logs -f fruit-api

# Health check
docker inspect --format='{{json .State.Health}}' fruit-api

# Access container
docker exec -it fruit-api sh

πŸ“ Project Structure

fruit-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

βœ… Validations

The project implements validations with Bean Validation:

FruitRequestDTO

public class FruitRequestDTO {
    
    @NotBlank(message = "Name cannot be empty")
    private String name;
    
    @Positive(message = "Weight must be greater than 0")
    private int weightInKilos;
}

Validation Rules

Field Validation Error Message
name Not empty "Name cannot be empty"
weightInKilos Greater than 0 "Weight must be greater than 0"

Validation Error Example

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"
}

⚠️ Error Handling

GlobalExceptionHandler

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
    }
}

HTTP Status Codes

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

ErrorResponse Structure

{
  "status": 404,
  "message": "Fruit not found with id: 999",
  "timestamp": "2025-12-11T10:30:00"
}

πŸ”§ Configuration

application.properties

# 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

Access H2 Console (Development)

  1. Run the application
  2. Go to: http://localhost:8080/h2-console
  3. JDBC URL: jdbc:h2:mem:fruitdb
  4. Username: sa
  5. Password: (empty)

πŸ“Š Usage Examples

Using cURL

# 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

Using Postman

  1. Import collection from: docs/Fruit-API.postman_collection.json (if you create it)
  2. Execute requests from the interface

πŸŽ“ Project Learnings

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

About

RESTful API for fruit inventory management built with Java 21 and Spring Boot. Features an in-memory H2 database, clean MVC architecture, and a strict Test-Driven Development (TDD) approach.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors