Tecnologías: Java 17 · Spring Boot 3 · Spring Web · Spring Data JPA · Bean Validation · PostgreSQL · Flyway · Maven Wrapper
API RESTful para la gestión de productos (CRUD completo) con persistencia en PostgreSQL, migraciones Flyway, validaciones y manejo uniforme de errores.
- Clonar el repositorio
git clone <URL_DEL_REPO>
cd <carpeta_del_repo>- Configurar base de datos PostgreSQL
Crea la base de datos y el usuario (puede ser postgres u otro usuario propio):
CREATE DATABASE productdb OWNER postgres;- Configurar credenciales (variables de entorno o application.yml)
Ejemplo con variables de entorno:
export SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/productdb
export SPRING_DATASOURCE_USERNAME=postgres
export SPRING_DATASOURCE_PASSWORD=TU_PASSWORD
export SERVER_PORT=8080- Construir y ejecutar la aplicación
./mvnw clean package
./mvnw spring-boot:run
# o bien
java -jar target/product-api-0.0.1-SNAPSHOT.jarLa API quedará disponible en:
http://localhost:8080
Endpoints principales:
GET /productsGET /products/{id}POST /productsPUT /products/{id}DELETE /products/{id}
En todos los ejemplos se asume que la API corre en http://localhost:8080.
Cada comando curl es independiente (no se usan variables tipo BASE_URL).
curl -i -X POST http://localhost:8080/products -H "Content-Type: application/json" -d '{"name":"Mate","description":"Acero inoxidable","price":190.43}'Respuesta esperada (201 Created):
{
"id": 1,
"name": "Mate",
"description": "Acero inoxidable",
"price": 190.43,
"createdAt": "2025-11-18T18:30:00Z",
"updatedAt": "2025-11-18T18:30:00Z"
}curl -i http://localhost:8080/products- Devuelve una lista de productos (
List<ProductResponse>). - Por defecto se ordenan cronológicamente, del más nuevo al más viejo (
createdAtdescendente).
curl -i http://localhost:8080/products/1- Si existe:
200 OKcon el producto. - Si no existe:
404 Not Foundcon un cuerpo JSON de error, por ejemplo:
{
"error": "PRODUCT_NOT_FOUND",
"message": "Product with id 999 not found",
"status": 404,
"path": "/products/999",
"timestamp": "2025-11-18T18:35:00Z"
}curl -i -X PUT http://localhost:8080/products/1 -H "Content-Type: application/json" -d '{"name":"Mate XL","description":"Acero doble pared","price":219.99}'- Si el producto existe:
200 OK(o204 No Content, según implementación) con el producto actualizado. - El campo
updatedAtse actualiza a la fecha/hora de la última modificación. createdAtse mantiene sin cambios.
curl -i -X DELETE http://localhost:8080/products/1- Si existe y se elimina correctamente:
204 No Content. - Si no existe:
404 Not Foundcon JSON de error.
A nivel de negocio, un producto tiene:
id: identificador numérico autogenerado.name: nombre del producto.description: descripción del producto.price: precio con 2 decimales (NUMERIC(15,2)).createdAt: fecha/hora de creación.updatedAt: fecha/hora de última actualización.
Ejemplo de JSON válido para crear/actualizar:
{
"name": "Cafetera Express",
"description": "Cafetera de 20 bares",
"price": 129999.99
}Reglas clave:
name- Obligatorio, no vacío.
- Longitud mínima 3 caracteres.
- Debe contener al menos una letra (no puede ser solo números).
description- Obligatoria, no vacía.
- Longitud mínima 3 caracteres.
- Debe contener al menos una letra.
price- Obligatorio.
- Mayor o igual que 0.
- Máximo 2 decimales.
- Se fuerza internamente a escala 2 con redondeo HALF_UP, por lo que
valores como
190.43097853056348075se guardan y devuelven como190.43.
El contrato de salida incluye:
{
"id": 1,
"name": "Cafetera Express",
"description": "Cafetera de 20 bares",
"price": 129999.99,
"createdAt": "2025-11-18T18:30:00Z",
"updatedAt": "2025-11-18T18:45:12Z"
}- Sistema operativo: Linux, macOS o Windows.
- Java: JDK 17 (OpenJDK recomendado).
- Base de datos: PostgreSQL 14+.
- Herramientas recomendadas:
- DBeaver / pgAdmin (cliente gráfico).
- curl o HTTPie para pruebas manuales.
- Docker (opcional) si se desea levantar PostgreSQL en contenedor.
Archivo principal: src/main/resources/application.yml:
spring:
datasource:
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/productdb}
username: ${SPRING_DATASOURCE_USERNAME:postgres}
password: ${SPRING_DATASOURCE_PASSWORD:postgres}
jpa:
hibernate:
ddl-auto: validate
open-in-view: false
flyway:
enabled: true
locations: classpath:db/migration
server:
port: ${SERVER_PORT:8080}Se recomienda configurar las credenciales mediante variables de entorno en lugar de hardcodearlas.
Ejemplo de V1__create_products.sql:
CREATE TABLE IF NOT EXISTS products (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL,
price NUMERIC(15,2) NOT NULL CHECK (price >= 0),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);- El tipo
NUMERIC(15,2)limita el precio a 2 decimales. created_atyupdated_atmantienen el tracking temporal.- Cualquier valor de
pricefuera de rango dispara un error coherente con las validaciones del modelo.
psql -h localhost -U postgres -d productdb -c "\d+ products"
psql -h localhost -U postgres -d productdb -c "SELECT * FROM products ORDER BY created_at DESC LIMIT 5;"curl -i -X POST http://localhost:8080/products -H "Content-Type: application/json" -d '{"name":"Mouse","description":"Inalámbrico","price":999.90}'Errores posibles:
400 Bad Requestcon detalles si:- Falta algún campo obligatorio.
nameodescriptionno cumplen con las reglas (solo números, muy corto, etc.).pricetiene más de 2 decimales o no es numérico.
curl -s http://localhost:8080/products | jq .- Devuelve lista ordenada por
createdAtdescendente. - Si se implementa paginación con
Pageable, se exponen campos comocontent,totalElements,totalPages, etc.
Ejemplo con parámetros de paginación:
curl -s "http://localhost:8080/products?page=0&size=5" | jq .curl -i http://localhost:8080/products/10200 OKsi existe.404 Not Foundcon body JSON si no existe.
curl -i -X PUT http://localhost:8080/products/10 -H "Content-Type: application/json" -d '{"name":"Mouse Pro","description":"Bluetooth 5.0","price":1299.00}'200 OKcon el producto actualizado.404 Not Foundsi el id no existe.400 Bad Requestsi la validación falla.
curl -i -X DELETE http://localhost:8080/products/10204 No Contentsi se elimina correctamente.404 Not Foundsi el id no existe.
La API utiliza Bean Validation (jakarta.validation) en los DTOs de entrada y un @ControllerAdvice para transformar las excepciones en respuestas JSON consistentes.
Ejemplo de error de validación:
{
"error": "VALIDATION_ERROR",
"status": 400,
"message": "Invalid request body",
"path": "/products",
"timestamp": "2025-11-18T18:40:00Z",
"details": [
{
"field": "name",
"message": "name must contain at least one letter"
},
{
"field": "price",
"message": "price must have up to 2 decimals"
}
]
}Ejemplo de error de recurso no encontrado:
{
"error": "PRODUCT_NOT_FOUND",
"status": 404,
"message": "Product with id 123 not found",
"path": "/products/123",
"timestamp": "2025-11-18T18:42:00Z"
}Este diseño evita mensajes genéricos tipo “malformación” y ayuda a que quien use la API entienda exactamente qué debe corregir.
- El dominio es relacional y sencillo (lista de productos), por lo que SQL encaja bien.
- PostgreSQL:
- Es open source, robusto y estándar en entornos productivos.
- Soporta tipos numéricos exactos (
NUMERIC) para precios. - Se integra muy bien con Spring Data JPA.
Para este tipo de API CRUD simple, una base NoSQL sería una sobreingeniería innecesaria.
ar.edu.challenge01.productapi
├── ProductApiApplication.java
├── entity
│ └── Product.java
├── dto
│ ├── CreateProductRequest.java
│ ├── UpdateProductRequest.java
│ └── ProductResponse.java
├── repository
│ └── ProductRepository.java
├── mapper
│ └── ProductMapper.java
├── web
│ └── ProductController.java
└── exception
├── ResourceNotFoundException.java
└── GlobalExceptionHandler.java // @ControllerAdvice
src/main/java: código fuente de la aplicación.src/main/resources:application.yml: configuración de Spring.db/migration: scripts Flyway.
src/test/java: pruebas unitarias e integración.pom.xml: dependencias, plugins y configuración de build.
| Tipo de test | Capa | Herramienta | Objetivo principal |
|---|---|---|---|
| Unit tests | Servicio | JUnit + Mockito | Regla de negocio y validaciones |
| Web slice tests | Controller | @WebMvcTest + MockMvc | Códigos HTTP, payloads, errores |
| Integration tests | Full stack | @SpringBootTest | Flujo completo con BD (idealmente PostgreSQL real) |
Con esta base, la API queda lista para ser probada por cualquier cliente HTTP (curl, Postman, Insomnia, Swagger UI, etc.) y para ser extendida con nuevas funcionalidades en el futuro.