A complete REST API implementation using Spring Boot 3.x, PostgreSQL, and Redis for caching, fully containerized with Docker.
- ✅ RESTful API with CRUD operations
- ✅ API Key authentication via header (
X-API-Key
) - ✅ PostgreSQL database with JPA/Hibernate
- ✅ Redis caching with 5-minute TTL
- ✅ HikariCP connection pooling
- ✅ Health check endpoint for monitoring
- ✅ Complete Docker setup with docker-compose
- ✅ Multi-stage Dockerfile for optimized builds
- ✅ Automatic API key generation on first run
- Backend: Java 21 with Spring Boot 3.5.6
- Database: PostgreSQL 15
- Cache: Redis 7
- Build Tool: Maven
- Containerization: Docker & Docker Compose
- Docker and Docker Compose installed
- Git (to clone the repository)
git clone https://github.com/AlesixDev/example-api-spring
cd example-api-spring
docker-compose up --build
This command will:
- Build the Spring Boot application
- Start PostgreSQL database
- Start Redis cache
- Start the API application
On the first run, the application automatically generates an API key. Check the logs:
docker-compose logs api | grep "API KEY GENERATED"
You should see output like:
================================================================================
NEW API KEY GENERATED: 12345678-90ab-cdef-1234-567890abcdef
Please save this API key. Use it in the X-API-Key header for all requests.
================================================================================
Save this API key - you'll need it for all API requests.
http://localhost:8080
No authentication required
GET /healthcheck
Example:
curl http://localhost:8080/healthcheck
Response (200 OK):
{
"status": "UP",
"timestamp": "2025-10-03T10:30:00",
"components": {
"database": {
"status": "UP",
"details": {
"database": "PostgreSQL",
"validationQuery": "SELECT 1",
"responseTime": "5ms"
}
},
"redis": {
"status": "UP",
"details": {
"responseTime": "2ms"
}
}
}
}
Requires API Key
POST /data
Headers:
X-API-Key
: [your-api-key]Content-Type
: application/json
Request Body:
{
"name": "Test Record",
"description": "This is a test description"
}
Example:
curl -X POST http://localhost:8080/data \
-H "X-API-Key: 12345678-90ab-cdef-1234-567890abcdef" \
-H "Content-Type: application/json" \
-d '{
"name": "Test Record",
"description": "This is a test description"
}'
Response (201 Created):
{
"id": 1,
"name": "Test Record",
"description": "This is a test description",
"created_at": "2025-10-03T10:30:00",
"updated_at": "2025-10-03T10:30:00"
}
Requires API Key
GET /data?sort=id&direction=ASC
Headers:
X-API-Key
: [your-api-key]
Query Parameters:
sort
(optional): Field to sort by (default:id
)direction
(optional):ASC
orDESC
(default:ASC
)
Example:
curl -X GET "http://localhost:8080/data?sort=name&direction=DESC" \
-H "X-API-Key: 12345678-90ab-cdef-1234-567890abcdef"
Response (200 OK):
{
"data": [
{
"id": 1,
"name": "Test Record",
"description": "This is a test description",
"created_at": "2025-10-03T10:30:00",
"updated_at": "2025-10-03T10:30:00"
}
],
"total": 1
}
Caching: Results are cached in Redis for 5 minutes. Subsequent requests return cached data.
Requires API Key
GET /data/{id}
Headers:
X-API-Key
: [your-api-key]
Example:
curl -X GET http://localhost:8080/data/1 \
-H "X-API-Key: 12345678-90ab-cdef-1234-567890abcdef"
Response (200 OK):
{
"id": 1,
"name": "Test Record",
"description": "This is a test description",
"created_at": "2025-10-03T10:30:00",
"updated_at": "2025-10-03T10:30:00"
}
Response (404 Not Found):
{
"error": "Not Found",
"message": "Registro con ID 999 no encontrado",
"timestamp": "2025-10-03T10:30:00",
"path": "/data/999"
}
Caching: Individual records are cached by ID.
{
"error": "Unauthorized",
"message": "API Key faltante o inválida"
}
{
"error": "Bad Request",
"message": "Name is required",
"timestamp": "2025-10-03T10:30:00",
"path": "/data"
}
{
"error": "Not Found",
"message": "Registro con ID 999 no encontrado",
"timestamp": "2025-10-03T10:30:00",
"path": "/data/999"
}
{
"error": "Internal Server Error",
"message": "An unexpected error occurred",
"timestamp": "2025-10-03T10:30:00",
"path": "/data"
}
{
"status": "DOWN",
"timestamp": "2025-10-03T10:30:00",
"components": {
"database": {
"status": "DOWN",
"error": "Connection refused"
},
"redis": {
"status": "DOWN",
"error": "Connection timeout"
}
}
}
Table: test_table
CREATE TABLE test_table (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Table: api_keys
CREATE TABLE api_keys (
id BIGSERIAL PRIMARY KEY,
key_value VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE
);
- Maximum pool size: 10 connections
- Minimum idle: 5 connections
- Connection timeout: 30 seconds
- Idle timeout: 10 minutes
- Max lifetime: 30 minutes
Connections are kept open and reused for optimal performance.
- Cache Type: Redis with Lettuce client
- TTL (Time To Live): 5 minutes (300 seconds)
- Serialization: JSON with Jackson
- Cache Keys:
- List queries:
testTableList::{sort}_{direction}
- Individual records:
testTableById::{id}
- List queries:
- Cache Invalidation: Automatically clears list cache when new records are created
- Method: API Key in HTTP header
- Header Name:
X-API-Key
- Generation: Automatic UUID generation on application startup
- Validation: Interceptor checks all requests (except
/healthcheck
)
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f api
docker-compose logs -f postgres
docker-compose logs -f redis
docker-compose down
docker-compose down -v
docker-compose up --build
docker exec -it api-postgres psql -U apiuser -d testdb
docker exec -it api-redis redis-cli
docker exec -it api-redis redis-cli
> KEYS *
> GET "testTableList::id_ASC"
example-api-spring/
├── src/
│ ├── main/
│ │ ├── java/dev/alesixdev/example_api_spring/
│ │ │ ├── config/ # Configuration classes
│ │ │ │ ├── ApiKeyInterceptor.java
│ │ │ │ ├── RedisCacheConfig.java
│ │ │ │ └── WebConfig.java
│ │ │ ├── controller/ # REST controllers
│ │ │ │ ├── HealthCheckController.java
│ │ │ │ └── TestTableController.java
│ │ │ ├── dto/ # Data Transfer Objects
│ │ │ │ ├── ErrorResponse.java
│ │ │ │ ├── HealthCheckResponse.java
│ │ │ │ ├── TestTableListResponse.java
│ │ │ │ ├── TestTableRequest.java
│ │ │ │ └── TestTableResponse.java
│ │ │ ├── entity/ # JPA Entities
│ │ │ │ ├── ApiKey.java
│ │ │ │ └── TestTable.java
│ │ │ ├── exception/ # Exception handlers
│ │ │ │ └── GlobalExceptionHandler.java
│ │ │ ├── repository/ # JPA Repositories
│ │ │ │ ├── ApiKeyRepository.java
│ │ │ │ └── TestTableRepository.java
│ │ │ ├── service/ # Business logic
│ │ │ │ ├── ApiKeyService.java
│ │ │ │ ├── HealthCheckService.java
│ │ │ │ └── TestTableService.java
│ │ │ └── ExampleApiSpringApplication.java
│ │ └── resources/
│ │ └── application.properties
│ └── test/
├── Dockerfile
├── docker-compose.yml
├── init.sql
├── pom.xml
└── README.md
Docker Compose sets these automatically:
Variable | Default | Description |
---|---|---|
SPRING_DATASOURCE_URL |
jdbc:postgresql://postgres:5432/testdb |
PostgreSQL connection URL |
SPRING_DATASOURCE_USERNAME |
apiuser |
Database username |
SPRING_DATASOURCE_PASSWORD |
apipass |
Database password |
SPRING_REDIS_HOST |
redis |
Redis host |
SPRING_REDIS_PORT |
6379 |
Redis port |
See src/main/resources/application.properties
for all configuration options.
- Check health:
curl http://localhost:8080/healthcheck
- Create a record:
curl -X POST http://localhost:8080/data \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"First Record","description":"My first test"}'
- Get all records (first call - from database):
curl -X GET http://localhost:8080/data \
-H "X-API-Key: YOUR_API_KEY"
- Get all records again (from Redis cache):
curl -X GET http://localhost:8080/data \
-H "X-API-Key: YOUR_API_KEY"
- Get specific record:
curl -X GET http://localhost:8080/data/1 \
-H "X-API-Key: YOUR_API_KEY"
Solution: Ensure PostgreSQL container is healthy:
docker-compose ps
docker-compose logs postgres
Solution: Check Redis container status:
docker-compose ps
docker-compose logs redis
Solution: Verify you're using the correct API key from the logs:
docker-compose logs api | grep "API KEY"
Solution: Rebuild the application:
docker-compose down
docker-compose up --build
Solution: Change ports in docker-compose.yml
or stop the conflicting service.
This project is provided as-is for educational and demonstration purposes.
For issues, questions, or contributions, please refer to the project repository or documentation.