Saltar a contenido

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 exitosa
  • 201 Created - Recurso creado
  • 204 No Content - Eliminación exitosa
  • 400 Bad Request - Error de validación
  • 404 Not Found - Recurso no encontrado
  • 500 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 @Valid para 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