Spring Boot es el framework más usado en el mundo empresarial Java para construir APIs REST. Combina la potencia del ecosistema Spring con una configuración mínima que te permite tener un servidor funcionando en minutos. En esta guía vas a construir una API REST completa con operaciones CRUD (Crear, Leer, Actualizar y Eliminar) conectada a una base de datos MySQL, desde cero y paso a paso.
Al terminar tendrás una API funcional para gestionar productos, con endpoints documentados y listos para ser consumidos desde cualquier frontend o herramienta de testing como Postman.
Requisitos previos
Antes de empezar, asegúrate de tener instalado en tu máquina:
- Java 21 o superior. Puedes verificarlo con
java -version. - Maven 3.9+ o usar el Maven Wrapper que genera Spring Initializr.
- MySQL 8.x ejecutándose localmente. Puedes instalarlo directamente o usar Docker:
docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root mysql:8. - Un IDE como IntelliJ IDEA (recomendado) o VS Code con la extensión de Java.
- Postman o cualquier cliente HTTP para probar los endpoints.
Paso 1: Crear el proyecto con Spring Initializr
La forma más rápida de empezar un proyecto Spring Boot es usando Spring Initializr, la herramienta oficial disponible en start.spring.io. Configúralo así:
- Project: Maven
- Language: Java
- Spring Boot: 3.3.x (la versión estable más reciente)
- Group:
com.ejemplo - Artifact:
tienda-api - Packaging: Jar
- Java: 21
En el apartado de dependencias, añade estas cuatro:
- Spring Web — para crear controladores REST y gestionar peticiones HTTP.
- Spring Data JPA — para acceder a la base de datos con el ORM Hibernate.
- MySQL Driver — el driver JDBC para conectar con MySQL.
- Validation — para validar los datos de entrada con anotaciones.
Haz clic en Generate, descarga el ZIP, descomprímelo y ábrelo en tu IDE.
Si prefieres la línea de comandos, el equivalente con Spring CLI es:
spring init \
--dependencies=web,data-jpa,mysql,validation \
--group-id=com.ejemplo \
--artifact-id=tienda-api \
--java-version=21 \
tienda-apiLa estructura del proyecto generado quedará así:
tienda-api/
├── src/
│ ├── main/
│ │ ├── java/com/ejemplo/tiendaapi/
│ │ │ └── TiendaApiApplication.java
│ │ └── resources/
│ │ └── application.properties
│ └── test/
│ └── java/com/ejemplo/tiendaapi/
│ └── TiendaApiApplicationTests.java
├── pom.xml
└── mvnwPaso 2: Configurar la conexión a MySQL
Abre el archivo src/main/resources/application.properties y añade la configuración de conexión a la base de datos:
# Conexión a MySQL
spring.datasource.url=jdbc:mysql://localhost:3306/tienda_db?createDatabaseIfNotExist=true&useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# JPA e Hibernate
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
# Nombre de la aplicación
spring.application.name=tienda-api
# Puerto del servidor (8080 por defecto)
server.port=8080El parámetro más importante es spring.jpa.hibernate.ddl-auto. Sus valores posibles son:
create: elimina y crea las tablas cada vez que arranca la aplicación. Solo para desarrollo inicial.update: crea las tablas si no existen y aplica los cambios sin borrar datos. Útil en desarrollo.validate: verifica que las tablas coinciden con las entidades pero no las modifica. Para producción.none: no hace nada. Para producción cuando usas migraciones con Flyway o Liquibase.
El parámetro createDatabaseIfNotExist=true en la URL crea automáticamente la base de datos tienda_db si no existe, así no tienes que crearla manualmente.
Para entornos reales, nunca pongas credenciales directamente en el archivo de propiedades. Usa variables de entorno:
spring.datasource.username=${DB_USERNAME:root}
spring.datasource.password=${DB_PASSWORD:root}Paso 3: Crear la entidad Producto
En Spring Data JPA, una entidad es una clase Java que se mapea a una tabla de la base de datos. Cada atributo de la clase corresponde a una columna de la tabla.
Crea el paquete model y dentro el archivo Producto.java:
// src/main/java/com/ejemplo/tiendaapi/model/Producto.java
package com.ejemplo.tiendaapi.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Entity
@Table(name = "productos")
public class Producto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "El nombre es obligatorio")
@Size(min = 2, max = 100, message = "El nombre debe tener entre 2 y 100 caracteres")
@Column(nullable = false, length = 100)
private String nombre;
@Size(max = 500, message = "La descripción no puede superar los 500 caracteres")
@Column(length = 500)
private String descripcion;
@NotNull(message = "El precio es obligatorio")
@DecimalMin(value = "0.0", inclusive = false, message = "El precio debe ser mayor que cero")
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal precio;
@Min(value = 0, message = "El stock no puede ser negativo")
@Column(nullable = false)
private Integer stock = 0;
@Column(nullable = false)
private Boolean disponible = true;
@Column(name = "creado_en", updatable = false)
private LocalDateTime creadoEn;
@Column(name = "actualizado_en")
private LocalDateTime actualizadoEn;
@PrePersist
protected void onCreate() {
creadoEn = LocalDateTime.now();
actualizadoEn = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
actualizadoEn = LocalDateTime.now();
}
// Constructores
public Producto() {}
public Producto(String nombre, String descripcion, BigDecimal precio, Integer stock) {
this.nombre = nombre;
this.descripcion = descripcion;
this.precio = precio;
this.stock = stock;
}
// Getters y Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getNombre() { return nombre; }
public void setNombre(String nombre) { this.nombre = nombre; }
public String getDescripcion() { return descripcion; }
public void setDescripcion(String d) { this.descripcion = d; }
public BigDecimal getPrecio() { return precio; }
public void setPrecio(BigDecimal precio) { this.precio = precio; }
public Integer getStock() { return stock; }
public void setStock(Integer stock) { this.stock = stock; }
public Boolean getDisponible() { return disponible; }
public void setDisponible(Boolean d) { this.disponible = d; }
public LocalDateTime getCreadoEn() { return creadoEn; }
public LocalDateTime getActualizadoEn() { return actualizadoEn; }
}Las anotaciones más importantes:
@Entity: marca la clase como una entidad JPA que se mapea a una tabla.@Table(name = "productos"): especifica el nombre de la tabla en la base de datos.@Id+@GeneratedValue: define la clave primaria con autoincremento gestionado por la base de datos.@Column: personaliza cómo se mapea el atributo a la columna (nombre, longitud, nulabilidad).@PrePersisty@PreUpdate: callbacks que se ejecutan automáticamente antes de guardar o actualizar.- Las anotaciones de validación (
@NotBlank,@NotNull,@DecimalMin) se aplican cuando el controlador recibe datos del cliente.
Paso 4: Crear el repositorio
En Spring Data JPA, un repositorio es una interfaz que proporciona automáticamente los métodos de acceso a la base de datos. Solo tienes que extender JpaRepository y Spring genera toda la implementación en tiempo de ejecución.
Crea el paquete repository y dentro ProductoRepository.java:
// src/main/java/com/ejemplo/tiendaapi/repository/ProductoRepository.java
package com.ejemplo.tiendaapi.repository;
import com.ejemplo.tiendaapi.model.Producto;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.util.List;
@Repository
public interface ProductoRepository extends JpaRepository<Producto, Long> {
// Spring genera el SQL automáticamente a partir del nombre del método
List<Producto> findByDisponibleTrue();
List<Producto> findByNombreContainingIgnoreCase(String nombre);
List<Producto> findByPrecioBetween(BigDecimal min, BigDecimal max);
List<Producto> findByStockGreaterThan(int cantidad);
// JPQL: query orientada a objetos (usa nombres de clases y atributos, no tablas)
@Query("SELECT p FROM Producto p WHERE p.precio < :precio AND p.disponible = true")
List<Producto> findDisponiblesConPrecioMenorA(@Param("precio") BigDecimal precio);
// SQL nativo cuando necesitas algo que JPQL no puede expresar fácilmente
@Query(value = "SELECT * FROM productos ORDER BY precio ASC LIMIT :limite", nativeQuery = true)
List<Producto> findLosMasBaratos(@Param("limite") int limite);
boolean existsByNombre(String nombre);
}Al extender JpaRepository<Producto, Long> obtienes gratis métodos como save(), findById(), findAll(), deleteById(), count() y muchos más. Spring interpreta el nombre de los métodos que declares (como findByNombreContainingIgnoreCase) y genera la consulta SQL correspondiente automáticamente.
Paso 5: Crear los DTOs
Un DTO (Data Transfer Object) es un objeto que define exactamente qué datos entran y salen de la API. Separa la representación de la API de la entidad interna, lo que te da control total sobre qué expones y qué validas.
Crea el paquete dto con dos clases:
// src/main/java/com/ejemplo/tiendaapi/dto/ProductoRequest.java
// Lo que el cliente envía al crear o actualizar un producto
package com.ejemplo.tiendaapi.dto;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
public class ProductoRequest {
@NotBlank(message = "El nombre es obligatorio")
@Size(min = 2, max = 100)
private String nombre;
@Size(max = 500)
private String descripcion;
@NotNull(message = "El precio es obligatorio")
@DecimalMin(value = "0.0", inclusive = false, message = "El precio debe ser mayor que cero")
private BigDecimal precio;
@Min(value = 0, message = "El stock no puede ser negativo")
private Integer stock = 0;
// Getters y Setters
public String getNombre() { return nombre; }
public void setNombre(String nombre) { this.nombre = nombre; }
public String getDescripcion() { return descripcion; }
public void setDescripcion(String d) { this.descripcion = d; }
public BigDecimal getPrecio() { return precio; }
public void setPrecio(BigDecimal precio) { this.precio = precio; }
public Integer getStock() { return stock; }
public void setStock(Integer stock) { this.stock = stock; }
}// src/main/java/com/ejemplo/tiendaapi/dto/ProductoResponse.java
// Lo que la API devuelve al cliente
package com.ejemplo.tiendaapi.dto;
import com.ejemplo.tiendaapi.model.Producto;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class ProductoResponse {
private Long id;
private String nombre;
private String descripcion;
private BigDecimal precio;
private Integer stock;
private Boolean disponible;
private LocalDateTime creadoEn;
private LocalDateTime actualizadoEn;
// Constructor que convierte una entidad en DTO
public static ProductoResponse from(Producto producto) {
ProductoResponse dto = new ProductoResponse();
dto.id = producto.getId();
dto.nombre = producto.getNombre();
dto.descripcion = producto.getDescripcion();
dto.precio = producto.getPrecio();
dto.stock = producto.getStock();
dto.disponible = producto.getDisponible();
dto.creadoEn = producto.getCreadoEn();
dto.actualizadoEn = producto.getActualizadoEn();
return dto;
}
// Getters (solo lectura, sin setters)
public Long getId() { return id; }
public String getNombre() { return nombre; }
public String getDescripcion() { return descripcion; }
public BigDecimal getPrecio() { return precio; }
public Integer getStock() { return stock; }
public Boolean getDisponible() { return disponible; }
public LocalDateTime getCreadoEn() { return creadoEn; }
public LocalDateTime getActualizadoEn() { return actualizadoEn; }
}Paso 6: Crear la capa de servicio
La capa de servicio contiene la lógica de negocio. El controlador recibe la petición HTTP, la pasa al servicio, el servicio la procesa y llama al repositorio. Esta separación hace el código más fácil de testear y mantener.
Crea el paquete service y dentro ProductoService.java:
// src/main/java/com/ejemplo/tiendaapi/service/ProductoService.java
package com.ejemplo.tiendaapi.service;
import com.ejemplo.tiendaapi.dto.ProductoRequest;
import com.ejemplo.tiendaapi.dto.ProductoResponse;
import com.ejemplo.tiendaapi.exception.ProductoNotFoundException;
import com.ejemplo.tiendaapi.exception.ProductoYaExisteException;
import com.ejemplo.tiendaapi.model.Producto;
import com.ejemplo.tiendaapi.repository.ProductoRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional
public class ProductoService {
private final ProductoRepository repositorio;
// Inyección de dependencias por constructor (recomendada sobre @Autowired)
public ProductoService(ProductoRepository repositorio) {
this.repositorio = repositorio;
}
@Transactional(readOnly = true)
public List<ProductoResponse> obtenerTodos() {
return repositorio.findAll()
.stream()
.map(ProductoResponse::from)
.toList();
}
@Transactional(readOnly = true)
public ProductoResponse obtenerPorId(Long id) {
Producto producto = repositorio.findById(id)
.orElseThrow(() -> new ProductoNotFoundException(id));
return ProductoResponse.from(producto);
}
@Transactional(readOnly = true)
public List<ProductoResponse> buscarPorNombre(String nombre) {
return repositorio.findByNombreContainingIgnoreCase(nombre)
.stream()
.map(ProductoResponse::from)
.toList();
}
@Transactional(readOnly = true)
public List<ProductoResponse> obtenerDisponibles() {
return repositorio.findByDisponibleTrue()
.stream()
.map(ProductoResponse::from)
.toList();
}
public ProductoResponse crear(ProductoRequest request) {
if (repositorio.existsByNombre(request.getNombre())) {
throw new ProductoYaExisteException(request.getNombre());
}
Producto producto = new Producto(
request.getNombre(),
request.getDescripcion(),
request.getPrecio(),
request.getStock()
);
Producto guardado = repositorio.save(producto);
return ProductoResponse.from(guardado);
}
public ProductoResponse actualizar(Long id, ProductoRequest request) {
Producto producto = repositorio.findById(id)
.orElseThrow(() -> new ProductoNotFoundException(id));
producto.setNombre(request.getNombre());
producto.setDescripcion(request.getDescripcion());
producto.setPrecio(request.getPrecio());
producto.setStock(request.getStock());
Producto actualizado = repositorio.save(producto);
return ProductoResponse.from(actualizado);
}
public void eliminar(Long id) {
if (!repositorio.existsById(id)) {
throw new ProductoNotFoundException(id);
}
repositorio.deleteById(id);
}
public ProductoResponse cambiarDisponibilidad(Long id, boolean disponible) {
Producto producto = repositorio.findById(id)
.orElseThrow(() -> new ProductoNotFoundException(id));
producto.setDisponible(disponible);
return ProductoResponse.from(repositorio.save(producto));
}
}Paso 7: Manejo de excepciones personalizadas
En lugar de dejar que Spring devuelva errores genéricos, vamos a definir excepciones propias y un manejador global que las convierta en respuestas JSON con el código HTTP correcto.
Crea el paquete exception:
// ProductoNotFoundException.java
package com.ejemplo.tiendaapi.exception;
public class ProductoNotFoundException extends RuntimeException {
public ProductoNotFoundException(Long id) {
super("Producto no encontrado con id: " + id);
}
}// ProductoYaExisteException.java
package com.ejemplo.tiendaapi.exception;
public class ProductoYaExisteException extends RuntimeException {
public ProductoYaExisteException(String nombre) {
super("Ya existe un producto con el nombre: " + nombre);
}
}// GlobalExceptionHandler.java
package com.ejemplo.tiendaapi.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
// Estructura estándar de respuesta de error
private Map<String, Object> construirError(HttpStatus status, String mensaje) {
Map<String, Object> error = new HashMap<>();
error.put("timestamp", LocalDateTime.now().toString());
error.put("status", status.value());
error.put("error", status.getReasonPhrase());
error.put("mensaje", mensaje);
return error;
}
@ExceptionHandler(ProductoNotFoundException.class)
public ResponseEntity<Map<String, Object>> handleNotFound(ProductoNotFoundException ex) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(construirError(HttpStatus.NOT_FOUND, ex.getMessage()));
}
@ExceptionHandler(ProductoYaExisteException.class)
public ResponseEntity<Map<String, Object>> handleConflict(ProductoYaExisteException ex) {
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(construirError(HttpStatus.CONFLICT, ex.getMessage()));
}
// Errores de validación de @Valid
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
Map<String, String> erroresCampos = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach(error -> {
String campo = ((FieldError) error).getField();
String mensaje = error.getDefaultMessage();
erroresCampos.put(campo, mensaje);
});
Map<String, Object> respuesta = new HashMap<>();
respuesta.put("timestamp", LocalDateTime.now().toString());
respuesta.put("status", 400);
respuesta.put("error", "Validación fallida");
respuesta.put("campos", erroresCampos);
return ResponseEntity.badRequest().body(respuesta);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGeneric(Exception ex) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(construirError(HttpStatus.INTERNAL_SERVER_ERROR,
"Error interno del servidor"));
}
}Paso 8: Crear el controlador REST
El controlador es la capa que expone los endpoints HTTP. Recibe las peticiones, llama al servicio y devuelve las respuestas con el código de estado correcto.
// src/main/java/com/ejemplo/tiendaapi/controller/ProductoController.java
package com.ejemplo.tiendaapi.controller;
import com.ejemplo.tiendaapi.dto.ProductoRequest;
import com.ejemplo.tiendaapi.dto.ProductoResponse;
import com.ejemplo.tiendaapi.service.ProductoService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/productos")
@CrossOrigin(origins = "*") // permite peticiones desde cualquier origen (ajustar en producción)
public class ProductoController {
private final ProductoService servicio;
public ProductoController(ProductoService servicio) {
this.servicio = servicio;
}
// GET /api/v1/productos
@GetMapping
public ResponseEntity<List<ProductoResponse>> obtenerTodos() {
return ResponseEntity.ok(servicio.obtenerTodos());
}
// GET /api/v1/productos/disponibles
@GetMapping("/disponibles")
public ResponseEntity<List<ProductoResponse>> obtenerDisponibles() {
return ResponseEntity.ok(servicio.obtenerDisponibles());
}
// GET /api/v1/productos/buscar?nombre=laptop
@GetMapping("/buscar")
public ResponseEntity<List<ProductoResponse>> buscar(@RequestParam String nombre) {
return ResponseEntity.ok(servicio.buscarPorNombre(nombre));
}
// GET /api/v1/productos/{id}
@GetMapping("/{id}")
public ResponseEntity<ProductoResponse> obtenerPorId(@PathVariable Long id) {
return ResponseEntity.ok(servicio.obtenerPorId(id));
}
// POST /api/v1/productos
@PostMapping
public ResponseEntity<ProductoResponse> crear(@Valid @RequestBody ProductoRequest request) {
ProductoResponse creado = servicio.crear(request);
return ResponseEntity.status(HttpStatus.CREATED).body(creado);
}
// PUT /api/v1/productos/{id}
@PutMapping("/{id}")
public ResponseEntity<ProductoResponse> actualizar(
@PathVariable Long id,
@Valid @RequestBody ProductoRequest request) {
return ResponseEntity.ok(servicio.actualizar(id, request));
}
// PATCH /api/v1/productos/{id}/disponibilidad
@PatchMapping("/{id}/disponibilidad")
public ResponseEntity<ProductoResponse> cambiarDisponibilidad(
@PathVariable Long id,
@RequestBody Map<String, Boolean> body) {
boolean disponible = body.getOrDefault("disponible", true);
return ResponseEntity.ok(servicio.cambiarDisponibilidad(id, disponible));
}
// DELETE /api/v1/productos/{id}
@DeleteMapping("/{id}")
public ResponseEntity<Void> eliminar(@PathVariable Long id) {
servicio.eliminar(id);
return ResponseEntity.noContent().build();
}
}Las anotaciones del controlador:
@RestController: combina@Controllery@ResponseBody. Todos los métodos devuelven datos serializados a JSON automáticamente.@RequestMapping("/api/v1/productos"): prefijo de ruta para todos los endpoints de esta clase.@GetMapping,@PostMapping, etc.: mapean cada método HTTP a un método Java.@PathVariable: extrae un valor de la URL (/productos/42→id = 42).@RequestParam: extrae un parámetro de la query string (?nombre=laptop).@RequestBody: deserializa el cuerpo JSON de la petición al objeto indicado.@Valid: activa la validación de las anotaciones de constraint definidas en el DTO.
Paso 9: Arrancar y probar la API
Arranca la aplicación ejecutando la clase principal o con Maven:
./mvnw spring-boot:runSi todo está correcto, verás en la consola algo como:
Tomcat started on port 8080 (http) with context path ''
Started TiendaApiApplication in 3.2 secondsHibernate habrá creado automáticamente la tabla productos en MySQL. Ahora prueba los endpoints:
Crear un producto (POST)
curl -X POST http://localhost:8080/api/v1/productos \
-H "Content-Type: application/json" \
-d '{
"nombre": "Laptop Pro 15",
"descripcion": "Portátil de alta gama con 32GB RAM",
"precio": 1299.99,
"stock": 10
}'Respuesta esperada (201 Created):
{
"id": 1,
"nombre": "Laptop Pro 15",
"descripcion": "Portátil de alta gama con 32GB RAM",
"precio": 1299.99,
"stock": 10,
"disponible": true,
"creadoEn": "2026-05-15T10:30:00",
"actualizadoEn": "2026-05-15T10:30:00"
}Obtener todos los productos (GET)
curl http://localhost:8080/api/v1/productosObtener un producto por ID (GET)
curl http://localhost:8080/api/v1/productos/1Buscar por nombre (GET)
curl "http://localhost:8080/api/v1/productos/buscar?nombre=laptop"Actualizar un producto (PUT)
curl -X PUT http://localhost:8080/api/v1/productos/1 \
-H "Content-Type: application/json" \
-d '{
"nombre": "Laptop Pro 15 v2",
"descripcion": "Versión actualizada",
"precio": 1199.99,
"stock": 8
}'Cambiar disponibilidad (PATCH)
curl -X PATCH http://localhost:8080/api/v1/productos/1/disponibilidad \
-H "Content-Type: application/json" \
-d '{"disponible": false}'
Eliminar un producto (DELETE)
curl -X DELETE http://localhost:8080/api/v1/productos/1Respuesta esperada: 204 No Content sin cuerpo.
Probar la validación
curl -X POST http://localhost:8080/api/v1/productos \
-H "Content-Type: application/json" \
-d '{"nombre": "", "precio": -5}'
Respuesta esperada (400 Bad Request):
{
"timestamp": "2026-05-15T10:35:00",
"status": 400,
"error": "Validación fallida",
"campos": {
"nombre": "El nombre es obligatorio",
"precio": "El precio debe ser mayor que cero"
}
}Paso 10: Añadir paginación y ordenación
Para APIs con muchos registros, devolver todos los datos de una vez no es práctico. Spring Data JPA tiene soporte nativo para paginación con Pageable.
// En el repositorio: añadir método paginado
Page<Producto> findByDisponibleTrue(Pageable pageable);// En el servicio
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
public Page<ProductoResponse> obtenerDisponiblesPaginados(Pageable pageable) {
return repositorio.findByDisponibleTrue(pageable)
.map(ProductoResponse::from);
}// En el controlador
@GetMapping("/disponibles")
public ResponseEntity<Page<ProductoResponse>> obtenerDisponibles(
@RequestParam(defaultValue = "0") int pagina,
@RequestParam(defaultValue = "10") int tamano,
@RequestParam(defaultValue = "id") String ordenPor) {
Pageable pageable = PageRequest.of(pagina, tamano, Sort.by(ordenPor));
return ResponseEntity.ok(servicio.obtenerDisponiblesPaginados(pageable));
}Ahora puedes paginar y ordenar con parámetros en la URL:
# Página 0, 5 elementos por página, ordenados por precio
curl "http://localhost:8080/api/v1/productos/disponibles?pagina=0&tamano=5&ordenPor=precio"La respuesta incluye metadatos de paginación automáticamente: número de páginas, total de elementos, página actual y si hay página siguiente o anterior.
Paso 11: Configurar perfiles por entorno
En proyectos reales necesitas configuraciones distintas para desarrollo y producción. Spring Boot soporta perfiles con archivos de propiedades separados.
# application.properties (configuración común)
spring.application.name=tienda-api
server.port=8080
# application-dev.properties (desarrollo)
spring.datasource.url=jdbc:mysql://localhost:3306/tienda_db?createDatabaseIfNotExist=true
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
# application-prod.properties (producción)
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false# Activar un perfil al arrancar
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
# O con la variable de entorno
export SPRING_PROFILES_ACTIVE=prod
java -jar tienda-api.jarEl pom.xml completo del proyecto
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<relativePath/>
</parent>
<groupId>com.ejemplo</groupId>
<artifactId>tienda-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>tienda-api</name>
<description>API REST con Spring Boot y MySQL</description>
<packaging>jar</packaging>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Resumen
Has construido una API REST completa con Spring Boot y MySQL siguiendo la arquitectura en capas estándar del mundo Java:
- Entidad (
@Entity): mapea la clase Java a la tabla de la base de datos con JPA e Hibernate. - Repositorio (
JpaRepository): proporciona automáticamente todas las operaciones CRUD y permite declarar consultas adicionales por nombre de método o con@Query. - DTO: separa la representación de la API de la entidad interna, controlando qué datos entran y salen.
- Servicio (
@Service): contiene la lógica de negocio, gestiona transacciones y orquesta las llamadas al repositorio. - Controlador (
@RestController): expone los endpoints HTTP, extrae los parámetros de la petición y delega en el servicio. - Manejador de excepciones (
@RestControllerAdvice): centraliza el tratamiento de errores y garantiza que la API siempre devuelve respuestas JSON consistentes.
Desde aquí, los siguientes pasos naturales son añadir autenticación con Spring Security y JWT, documentación automática con Springdoc OpenAPI (Swagger UI), tests de integración con @SpringBootTest y migraciones de base de datos con Flyway.
No hay comentarios todavía. Sé el primero en compartir tu opinión.