Construcción de una API REST con Spring Boot
Índice
- 1. Qué es REST y qué no es
- 2. Recursos, URIs y representaciones
- 3. Configuración del proyecto
- 4. Definición del modelo y repositorio
- 5. Implementación del controller REST
- 6. Manejo de errores y códigos de respuesta
- 7. Conclusión
1. Qué es REST y qué no es
REST (REpresentational State Transfer) es un estilo arquitectónico para sistemas hipermedia distribuidos, definido por Roy Fielding en su disertación del año 2000. No es un protocolo ni un estándar: es un conjunto de principios que, cuando se respetan, permiten llamar a una interfaz RESTful.
Fielding no prescribe HTTP en ninguna parte de su definición. Una API puede ser RESTful usando cualquier protocolo que satisfaga las seis restricciones. En la práctica, HTTP es la elección predominante porque sus semánticas (métodos, códigos de estado, cabeceras) encajan de forma natural con esos principios.
Las seis restricciones son:
| Restricción | Descripción |
|---|---|
| Interfaz uniforme | Identificación de recursos mediante URIs; representaciones estándar para manipular el estado |
| Cliente-servidor | Separación de responsabilidades entre UI y almacenamiento de datos |
| Sin estado | Cada request debe contener toda la información necesaria para procesarlo; el servidor no guarda contexto entre llamadas |
| Cacheable | Las respuestas deben indicar si son cacheables para que el cliente pueda reutilizarlas |
| Sistema en capas | Los componentes solo conocen la capa inmediata con la que interactúan |
| Código en demanda (opcional) | El servidor puede enviar código ejecutable al cliente (ej.: scripts) |
La restricción de sin estado tiene consecuencias directas en el diseño: no se usa HttpSession en el servidor para rastrear al cliente. Toda la información de autenticación, paginación u otros contextos viaja en cada request (generalmente en cabeceras o parámetros).
2. Recursos, URIs y representaciones
En REST, la abstracción central es el recurso. Un recurso es cualquier información identificable: un usuario, un pedido, una colección de productos. El estado de un recurso en un momento dado se denomina representación, que incluye los datos, los metadatos que los describen y los hipervínculos a recursos relacionados.
Las URIs identifican recursos, no acciones. El método HTTP expresa la acción sobre ese recurso.
| Incorrecto | Correcto |
|---|---|
POST /crearProducto |
POST /productos |
GET /obtenerProductoPorId?id=5 |
GET /productos/5 |
POST /eliminarProducto/5 |
DELETE /productos/5 |
Los métodos HTTP y su semántica esperada en un contexto RESTful:
| Método | Semántica | Idempotente | Seguro |
|---|---|---|---|
| GET | Obtener representación del recurso | Sí | Sí |
| POST | Crear un nuevo recurso subordinado | No | No |
| PUT | Reemplazar el recurso completo | Sí | No |
| PATCH | Modificación parcial del recurso | No necesariamente | No |
| DELETE | Eliminar el recurso | Sí | No |
3. Configuración del proyecto
Se usa Spring Boot 3.x con Jakarta EE 10. Las dependencias mínimas para un API REST con persistencia son:
<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>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
Configuración mínima en application.properties:
spring.datasource.url=jdbc:mysql://localhost:3306/springwriter_demo
spring.datasource.username=root
spring.datasource.password=secret
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
ddl-auto=validate es apropiado para entornos que no son desarrollo: Hibernate valida el esquema contra las entidades pero no lo modifica. En desarrollo local se puede usar update; en producción se recomienda migración explícita con Flyway o Liquibase.
4. Definición del modelo y repositorio
El recurso de ejemplo es Producto. La entidad usa anotaciones de Jakarta Persistence:
package com.springwriter.demo.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "productos")
public class Producto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 150)
private String nombre;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal precio;
@Column(nullable = false)
private Integer stock;
// constructores, getters y setters
}
El repositorio extiende JpaRepository, que provee operaciones CRUD sin necesidad de implementación manual:
package com.springwriter.demo.repository;
import com.springwriter.demo.model.Producto;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductoRepository extends JpaRepository<Producto, Long> {
}
Para no exponer la entidad directamente en la API - lo que acoplaría el contrato HTTP al modelo de persistencia -, se define un DTO:
package com.springwriter.demo.dto;
import java.math.BigDecimal;
public record ProductoRequest(
String nombre,
BigDecimal precio,
Integer stock
) {}
public record ProductoResponse(
Long id,
String nombre,
BigDecimal precio,
Integer stock
) {}
El uso de record (disponible desde Java 16) elimina boilerplate para clases de datos inmutables.
5. Implementación del controller REST
@RestController combina @Controller y @ResponseBody. Todos los métodos retornan datos serializados directamente en el cuerpo de la respuesta, por defecto en JSON.
package com.springwriter.demo.controller;
import com.springwriter.demo.dto.ProductoRequest;
import com.springwriter.demo.dto.ProductoResponse;
import com.springwriter.demo.model.Producto;
import com.springwriter.demo.repository.ProductoRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import java.net.URI;
import java.util.List;
@RestController
@RequestMapping("/api/v1/productos")
public class ProductoController {
private final ProductoRepository repo;
public ProductoController(ProductoRepository repo) {
this.repo = repo;
}
@GetMapping
public List<ProductoResponse> listar() {
return repo.findAll().stream()
.map(this::toResponse)
.toList();
}
@GetMapping("/{id}")
public ResponseEntity<ProductoResponse> obtener(@PathVariable Long id) {
return repo.findById(id)
.map(p -> ResponseEntity.ok(toResponse(p)))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<ProductoResponse> crear(@RequestBody ProductoRequest req) {
Producto nuevo = new Producto();
nuevo.setNombre(req.nombre());
nuevo.setPrecio(req.precio());
nuevo.setStock(req.stock());
Producto guardado = repo.save(nuevo);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(guardado.getId())
.toUri();
return ResponseEntity.created(location).body(toResponse(guardado));
}
@PutMapping("/{id}")
public ResponseEntity<ProductoResponse> actualizar(
@PathVariable Long id,
@RequestBody ProductoRequest req) {
return repo.findById(id).map(p -> {
p.setNombre(req.nombre());
p.setPrecio(req.precio());
p.setStock(req.stock());
return ResponseEntity.ok(toResponse(repo.save(p)));
}).orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> eliminar(@PathVariable Long id) {
if (!repo.existsById(id)) {
return ResponseEntity.notFound().build();
}
repo.deleteById(id);
return ResponseEntity.noContent().build();
}
private ProductoResponse toResponse(Producto p) {
return new ProductoResponse(p.getId(), p.getNombre(), p.getPrecio(), p.getStock());
}
}
Puntos a destacar:
POST /api/v1/productosretorna 201 Created con la cabeceraLocationapuntando al recurso creado. Esto respeta la semántica HTTP y permite al cliente navegar al nuevo recurso sin información adicional out-of-band.DELETEretorna 204 No Content en éxito, que es el código correcto cuando no hay cuerpo en la respuesta.PUTretorna 404 si el recurso no existe. Una implementación alternativa válida es crear el recurso si no existe (upsert), siempre que el cliente proporcione el ID. En ese caso el comportamiento debe documentarse explícitamente.
6. Manejo de errores y códigos de respuesta
Retornar solo 200 o 500 no es suficiente para una API consumible. Spring MVC permite centralizar el manejo de excepciones con @RestControllerAdvice:
package com.springwriter.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RecursoNoEncontradoException.class)
public ProblemDetail handleNotFound(RecursoNoEncontradoException ex) {
ProblemDetail detail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
detail.setTitle("Recurso no encontrado");
return detail;
}
@ExceptionHandler(IllegalArgumentException.class)
public ProblemDetail handleBadRequest(IllegalArgumentException ex) {
ProblemDetail detail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, ex.getMessage());
detail.setTitle("Solicitud inválida");
return detail;
}
}
ProblemDetail implementa el estándar RFC 9457 (Problem Details for HTTP APIs) y está disponible en Spring Framework 6+. Retorna un JSON estructurado con campos type, title, status, detail e instance.
{
"type": "about:blank",
"title": "Recurso no encontrado",
"status": 404,
"detail": "Producto con id 99 no existe",
"instance": "/api/v1/productos/99"
}
Los códigos de respuesta más relevantes en una API REST sobre HTTP:
| Código | Uso |
|---|---|
| 200 OK | GET, PUT o PATCH exitoso con cuerpo en la respuesta |
| 201 Created | POST que crea un recurso; incluir Location |
| 204 No Content | DELETE exitoso o PUT/PATCH sin cuerpo en respuesta |
| 400 Bad Request | Request mal formado o con datos inválidos |
| 404 Not Found | El recurso identificado por la URI no existe |
| 409 Conflict | Conflicto de estado (ej.: unicidad violada) |
| 422 Unprocessable Entity | El request está bien formado pero falla validación de negocio |
| 500 Internal Server Error | Error no controlado en el servidor |
7. Conclusión
Una API REST construida con Spring Boot no requiere configuración especial más allá de spring-boot-starter-web: el dispatcher servlet, la serialización JSON con Jackson y la negociación de contenido están disponibles por defecto.
Los aspectos más críticos no son técnicos sino de diseño: mantener URIs centradas en recursos, respetar la semántica de los métodos HTTP, retornar los códigos de estado correctos y no filtrar detalles internos del modelo de persistencia a través del contrato HTTP.
El siguiente paso natural es agregar validación de entrada con jakarta.validation y seguridad con Spring Security.
Comentarios (0)
Todavía no hay comentarios.
Dejá un comentario