Semana 11 - Controladores Spring Boot API REST
Introducción a los Controladores REST
Los controladores REST en Spring Boot son componentes fundamentales que manejan las peticiones HTTP y definen los endpoints de nuestra API. Utilizan anotaciones específicas para mapear URLs a métodos Java y gestionar las operaciones CRUD.
Conceptos Fundamentales
¿Qué es un Controlador REST?
Un controlador REST es una clase Java que: - Maneja peticiones HTTP (GET, POST, PUT, DELETE) - Procesa datos de entrada y salida - Coordina la lógica de negocio - Retorna respuestas en formato JSON/XML
Anotaciones Principales
@RestController
Combina @Controller y @ResponseBody, indicando que la clase maneja peticiones REST y retorna datos directamente (no vistas).
@RestController
@RequestMapping("/api/v1")
public class ProductoController {
// métodos del controlador
}
@RequestMapping
Define la ruta base para todos los endpoints del controlador.
Anotaciones de Métodos HTTP
@GetMapping- Para operaciones de lectura@PostMapping- Para crear recursos@PutMapping- Para actualizar recursos completos@PatchMapping- Para actualizaciones parciales@DeleteMapping- Para eliminar recursos
Ejemplo Práctico: API de Productos
1. Modelo de Datos
@Entity
@Table(name = "productos")
public class Producto {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String nombre;
@Column(nullable = false)
private String descripcion;
@Column(nullable = false)
private BigDecimal precio;
@Column(nullable = false)
private Integer stock;
@CreationTimestamp
private LocalDateTime fechaCreacion;
// Constructores, getters y setters
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...
}
2. DTO (Data Transfer Object)
public class ProductoDTO {
private Long id;
@NotBlank(message = "El nombre es obligatorio")
@Size(min = 2, max = 100, message = "El nombre debe tener entre 2 y 100 caracteres")
private String nombre;
@NotBlank(message = "La descripción es obligatoria")
@Size(max = 500, message = "La descripción no puede exceder 500 caracteres")
private String descripcion;
@NotNull(message = "El precio es obligatorio")
@DecimalMin(value = "0.0", inclusive = false, message = "El precio debe ser mayor a 0")
private BigDecimal precio;
@NotNull(message = "El stock es obligatorio")
@Min(value = 0, message = "El stock no puede ser negativo")
private Integer stock;
private LocalDateTime fechaCreacion;
// Constructores, getters y setters...
}
3. Servicio
@Service
@Transactional
public class ProductoService {
@Autowired
private ProductoRepository productoRepository;
@Autowired
private ModelMapper modelMapper;
public List<ProductoDTO> obtenerTodos() {
List<Producto> productos = productoRepository.findAll();
return productos.stream()
.map(producto -> modelMapper.map(producto, ProductoDTO.class))
.collect(Collectors.toList());
}
public ProductoDTO obtenerPorId(Long id) {
Producto producto = productoRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Producto no encontrado con ID: " + id));
return modelMapper.map(producto, ProductoDTO.class);
}
public ProductoDTO crear(ProductoDTO productoDTO) {
Producto producto = modelMapper.map(productoDTO, Producto.class);
Producto productoGuardado = productoRepository.save(producto);
return modelMapper.map(productoGuardado, ProductoDTO.class);
}
public ProductoDTO actualizar(Long id, ProductoDTO productoDTO) {
Producto producto = productoRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Producto no encontrado con ID: " + id));
producto.setNombre(productoDTO.getNombre());
producto.setDescripcion(productoDTO.getDescripcion());
producto.setPrecio(productoDTO.getPrecio());
producto.setStock(productoDTO.getStock());
Producto productoActualizado = productoRepository.save(producto);
return modelMapper.map(productoActualizado, ProductoDTO.class);
}
public void eliminar(Long id) {
if (!productoRepository.existsById(id)) {
throw new ResourceNotFoundException("Producto no encontrado con ID: " + id);
}
productoRepository.deleteById(id);
}
}
4. Controlador REST Completo
@RestController
@RequestMapping("/api/v1/productos")
@Validated
@CrossOrigin(origins = "*")
public class ProductoController {
@Autowired
private ProductoService productoService;
/**
* Obtener todos los productos
* GET /api/v1/productos
*/
@GetMapping
public ResponseEntity<List<ProductoDTO>> obtenerTodos() {
List<ProductoDTO> productos = productoService.obtenerTodos();
return ResponseEntity.ok(productos);
}
/**
* Obtener producto por ID
* GET /api/v1/productos/{id}
*/
@GetMapping("/{id}")
public ResponseEntity<ProductoDTO> obtenerPorId(@PathVariable Long id) {
ProductoDTO producto = productoService.obtenerPorId(id);
return ResponseEntity.ok(producto);
}
/**
* Crear nuevo producto
* POST /api/v1/productos
*/
@PostMapping
public ResponseEntity<ProductoDTO> crear(@Valid @RequestBody ProductoDTO productoDTO) {
ProductoDTO nuevoProducto = productoService.crear(productoDTO);
return ResponseEntity.status(HttpStatus.CREATED).body(nuevoProducto);
}
/**
* Actualizar producto existente
* PUT /api/v1/productos/{id}
*/
@PutMapping("/{id}")
public ResponseEntity<ProductoDTO> actualizar(
@PathVariable Long id,
@Valid @RequestBody ProductoDTO productoDTO) {
ProductoDTO productoActualizado = productoService.actualizar(id, productoDTO);
return ResponseEntity.ok(productoActualizado);
}
/**
* Eliminar producto
* DELETE /api/v1/productos/{id}
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> eliminar(@PathVariable Long id) {
productoService.eliminar(id);
return ResponseEntity.noContent().build();
}
/**
* Buscar productos por nombre
* GET /api/v1/productos/buscar?nombre=valor
*/
@GetMapping("/buscar")
public ResponseEntity<List<ProductoDTO>> buscarPorNombre(
@RequestParam String nombre) {
List<ProductoDTO> productos = productoService.buscarPorNombre(nombre);
return ResponseEntity.ok(productos);
}
/**
* Obtener productos con paginación
* GET /api/v1/productos/paginado?page=0&size=10&sort=nombre,asc
*/
@GetMapping("/paginado")
public ResponseEntity<Page<ProductoDTO>> obtenerPaginado(
@PageableDefault(size = 10, sort = "nombre") Pageable pageable) {
Page<ProductoDTO> productos = productoService.obtenerPaginado(pageable);
return ResponseEntity.ok(productos);
}
}
Manejo de Errores y Validaciones
1. Excepciones Personalizadas
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
}
2. Manejador Global de Excepciones
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"Errores de validación",
LocalDateTime.now(),
errors
);
return ResponseEntity.badRequest().body(errorResponse);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"Error interno del servidor",
LocalDateTime.now()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
3. Clase de Respuesta de Error
public class ErrorResponse {
private int status;
private String message;
private LocalDateTime timestamp;
private Map<String, String> errors;
// Constructores
public ErrorResponse(int status, String message, LocalDateTime timestamp) {
this.status = status;
this.message = message;
this.timestamp = timestamp;
}
public ErrorResponse(int status, String message, LocalDateTime timestamp, Map<String, String> errors) {
this.status = status;
this.message = message;
this.timestamp = timestamp;
this.errors = errors;
}
// Getters y Setters...
}
Mejores Prácticas
1. Versionado de API
@RequestMapping("/api/v1/productos")
2. Códigos de Estado HTTP Apropiados
200 OK- Operación exitosa201 Created- Recurso creado204 No Content- Eliminación exitosa400 Bad Request- Error de validación404 Not Found- Recurso no encontrado500 Internal Server Error- Error del servidor
3. Documentación con OpenAPI/Swagger
@RestController
@RequestMapping("/api/v1/productos")
@Tag(name = "Productos", description = "API para gestión de productos")
public class ProductoController {
@Operation(summary = "Obtener todos los productos")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Lista de productos obtenida exitosamente"),
@ApiResponse(responseCode = "500", description = "Error interno del servidor")
})
@GetMapping
public ResponseEntity<List<ProductoDTO>> obtenerTodos() {
// implementación
}
}
4. Validación de Entrada
- Usar
@Validpara validar DTOs - Implementar validaciones personalizadas cuando sea necesario
- Manejar errores de validación apropiadamente
5. Seguridad
@PreAuthorize("hasRole('ADMIN')")
@PostMapping
public ResponseEntity<ProductoDTO> crear(@Valid @RequestBody ProductoDTO productoDTO) {
// implementación
}
Pruebas con Postman/cURL
Crear Producto
curl -X POST http://localhost:8080/api/v1/productos \
-H "Content-Type: application/json" \
-d '{
"nombre": "Laptop Gaming",
"descripcion": "Laptop para gaming de alta gama",
"precio": 1500.00,
"stock": 10
}'
Obtener Todos los Productos
curl -X GET http://localhost:8080/api/v1/productos
Actualizar Producto
curl -X PUT http://localhost:8080/api/v1/productos/1 \
-H "Content-Type: application/json" \
-d '{
"nombre": "Laptop Gaming Pro",
"descripcion": "Laptop para gaming profesional",
"precio": 1800.00,
"stock": 5
}'
Eliminar Producto
curl -X DELETE http://localhost:8080/api/v1/productos/1