Métodos en controladores REST

Avanzado
SpringBoot
SpringBoot
Actualizado: 02/09/2025

GET

Los métodos GET representan el tipo de operación más fundamental en cualquier API REST, diseñados específicamente para recuperar información del servidor sin modificar el estado de los recursos. En Spring Boot, estos métodos se implementan utilizando la anotación @GetMapping, que simplifica significativamente la creación de endpoints de consulta.

Fundamentos de @GetMapping

La anotación @GetMapping actúa como un atajo especializado para @RequestMapping(method = RequestMethod.GET), proporcionando una sintaxis más limpia y expresiva. Esta anotación se coloca directamente sobre los métodos del controlador que deben responder a peticiones HTTP GET.

@RestController
@RequestMapping("/api/usuarios")
public class UsuarioController {

    @GetMapping
    public List<Usuario> obtenerTodosLosUsuarios() {
        // Lógica para recuperar todos los usuarios
        return usuarioService.obtenerTodos();
    }
}

Este ejemplo básico demuestra cómo un endpoint simple responde a peticiones GET en la ruta /api/usuarios, devolviendo una lista completa de usuarios. La ausencia de parámetros en @GetMapping hace que el método responda a la ruta base definida en el controlador.

Trabajando con variables de ruta

Las variables de ruta permiten capturar segmentos dinámicos de la URL y utilizarlos como parámetros en los métodos del controlador. La anotación @PathVariable extrae estos valores automáticamente.

@GetMapping("/{id}")
public ResponseEntity<Usuario> obtenerUsuarioPorId(@PathVariable Long id) {
    Usuario usuario = usuarioService.buscarPorId(id);
    
    if (usuario != null) {
        return ResponseEntity.ok(usuario);
    } else {
        return ResponseEntity.notFound().build();
    }
}

En este caso, una petición a /api/usuarios/123 extraería el valor 123 como el parámetro id. El uso de ResponseEntity permite un control granular sobre la respuesta HTTP, incluyendo códigos de estado específicos.

Manejo de múltiples variables de ruta

Los endpoints pueden capturar múltiples segmentos de la URL simultáneamente, lo que resulta útil para recursos anidados o jerarquías complejas.

@GetMapping("/{usuarioId}/pedidos/{pedidoId}")
public ResponseEntity<Pedido> obtenerPedidoDelUsuario(
        @PathVariable Long usuarioId,
        @PathVariable Long pedidoId) {
    
    Pedido pedido = pedidoService.buscarPedidoPorUsuario(usuarioId, pedidoId);
    return pedido != null ? ResponseEntity.ok(pedido) : ResponseEntity.notFound().build();
}

Parámetros de consulta

Los parámetros de consulta proporcionan una forma flexible de filtrar, paginar o personalizar las respuestas sin modificar la estructura de la URL. La anotación @RequestParam maneja estos parámetros automáticamente.

@GetMapping("/buscar")
public List<Usuario> buscarUsuarios(
        @RequestParam String nombre,
        @RequestParam(required = false) String email,
        @RequestParam(defaultValue = "0") int pagina,
        @RequestParam(defaultValue = "10") int tamaño) {
    
    return usuarioService.buscarConFiltros(nombre, email, pagina, tamaño);
}

Este método responde a URLs como /api/usuarios/buscar?nombre=Juan&email=juan@ejemplo.com&pagina=0&tamaño=5, donde algunos parámetros son opcionales y otros tienen valores por defecto.

Validación de parámetros

Spring Boot integra validación automática para los parámetros de entrada, utilizando anotaciones de Jakarta Bean Validation.

@GetMapping("/{id}")
public ResponseEntity<Usuario> obtenerUsuario(
        @PathVariable @Min(1) @Max(999999) Long id) {
    
    Usuario usuario = usuarioService.buscarPorId(id);
    return ResponseEntity.ok(usuario);
}

@GetMapping("/por-edad")
public List<Usuario> obtenerUsuariosPorEdad(
        @RequestParam @Min(18) @Max(100) Integer edad) {
    
    return usuarioService.buscarPorEdad(edad);
}

Respuestas estructuradas con ResponseEntity

ResponseEntity ofrece control completo sobre la respuesta HTTP, permitiendo personalizar headers, códigos de estado y el cuerpo de la respuesta.

@GetMapping("/{id}/perfil")
public ResponseEntity<Map<String, Object>> obtenerPerfilCompleto(@PathVariable Long id) {
    try {
        Usuario usuario = usuarioService.buscarPorId(id);
        
        if (usuario == null) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                    .body(Map.of("error", "Usuario no encontrado"));
        }
        
        Map<String, Object> perfil = usuarioService.construirPerfilCompleto(usuario);
        
        return ResponseEntity.ok()
                .header("Cache-Control", "max-age=300")
                .body(perfil);
                
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Map.of("error", "Error interno del servidor"));
    }
}

Manejo de tipos de contenido

Los métodos GET pueden especificar los tipos de contenido que producen, ayudando a los clientes a entender qué formato de datos esperar.

@GetMapping(value = "/reporte", produces = "application/json")
public ResponseEntity<ReporteDTO> obtenerReporte() {
    ReporteDTO reporte = reporteService.generarReporte();
    return ResponseEntity.ok(reporte);
}

@GetMapping(value = "/exportar", produces = "text/csv")
public ResponseEntity<String> exportarUsuarios() {
    String csvData = usuarioService.exportarACsv();
    
    return ResponseEntity.ok()
            .header("Content-Disposition", "attachment; filename=usuarios.csv")
            .body(csvData);
}

POST

Los métodos POST constituyen el mecanismo estándar para crear nuevos recursos en las APIs REST. En Spring Boot, la anotación @PostMapping facilita la implementación de estos endpoints, permitiendo recibir datos del cliente y procesarlos para generar nuevos recursos en el sistema.

Fundamentos de @PostMapping

La anotación @PostMapping funciona como un alias especializado para @RequestMapping(method = RequestMethod.POST), ofreciendo una sintaxis más expresiva y concisa. A diferencia de los métodos GET, los endpoints POST están diseñados para recibir y procesar datos enviados en el cuerpo de la petición HTTP.

@RestController
@RequestMapping("/api/productos")
public class ProductoController {

    @PostMapping
    public ResponseEntity<Producto> crearProducto(@RequestBody Producto producto) {
        Producto nuevoProducto = productoService.crear(producto);
        return ResponseEntity.status(HttpStatus.CREATED).body(nuevoProducto);
    }
}

Este ejemplo demuestra la estructura básica de un endpoint POST, donde @RequestBody indica que Spring debe deserializar automáticamente el JSON recibido en un objeto Java, y ResponseEntity permite retornar el código de estado HTTP 201 (Created) junto con el recurso creado.

Trabajando con @RequestBody

La anotación @RequestBody es fundamental en los métodos POST, ya que instruye a Spring para que tome el contenido del cuerpo de la petición HTTP y lo convierta automáticamente en un objeto Java mediante el proceso de deserialización.

@PostMapping("/crear")
public ResponseEntity<Usuario> registrarUsuario(@RequestBody Usuario usuario) {
    // Validaciones de negocio
    if (usuarioService.existeEmail(usuario.getEmail())) {
        return ResponseEntity.badRequest().build();
    }
    
    // Asignación de valores por defecto
    usuario.setFechaRegistro(LocalDateTime.now());
    usuario.setActivo(true);
    
    Usuario usuarioGuardado = usuarioService.guardar(usuario);
    return ResponseEntity.status(HttpStatus.CREATED).body(usuarioGuardado);
}

Validación de datos con @Valid

La validación automática de los datos recibidos es crucial para mantener la integridad de la aplicación. Spring Boot integra Bean Validation mediante la anotación @Valid, que activa las validaciones definidas en el modelo de datos.

Definición del modelo con validaciones:

public class Usuario {
    @NotBlank(message = "El nombre es obligatorio")
    @Size(min = 2, max = 50, message = "El nombre debe tener entre 2 y 50 caracteres")
    private String nombre;
    
    @NotNull(message = "El email es obligatorio")
    @Email(message = "El formato del email no es válido")
    private String email;
    
    @Min(value = 18, message = "La edad mínima es 18 años")
    @Max(value = 120, message = "La edad máxima es 120 años")
    private Integer edad;
    
    // Getters y setters
}

Implementación del endpoint con validación:

@PostMapping("/registrar")
public ResponseEntity<?> crearUsuario(@Valid @RequestBody Usuario usuario) {
    Usuario nuevoUsuario = usuarioService.crear(usuario);
    
    return ResponseEntity.status(HttpStatus.CREATED)
            .location(URI.create("/api/usuarios/" + nuevoUsuario.getId()))
            .body(nuevoUsuario);
}

Manejo de errores de validación

Cuando las validaciones fallan, Spring lanza una excepción MethodArgumentNotValidException. Es recomendable implementar un manejador global de excepciones para proporcionar respuestas consistentes.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> manejarErroresValidacion(
            MethodArgumentNotValidException ex) {
        
        Map<String, String> errores = new HashMap<>();
        
        ex.getBindingResult().getFieldErrors().forEach(error -> 
            errores.put(error.getField(), error.getDefaultMessage())
        );
        
        return ResponseEntity.badRequest().body(errores);
    }
}

Especificación de tipos de contenido

Los métodos POST pueden especificar explícitamente qué tipos de contenido aceptan y producen mediante los atributos consumes y produces.

@PostMapping(value = "/subir-archivo", 
            consumes = "multipart/form-data",
            produces = "application/json")
public ResponseEntity<Map<String, String>> subirArchivo(
        @RequestParam("archivo") MultipartFile archivo,
        @RequestParam("descripcion") String descripcion) {
    
    if (archivo.isEmpty()) {
        return ResponseEntity.badRequest()
                .body(Map.of("error", "El archivo no puede estar vacío"));
    }
    
    String nombreArchivo = archivoService.guardar(archivo, descripcion);
    
    return ResponseEntity.status(HttpStatus.CREATED)
            .body(Map.of("archivo", nombreArchivo, "estado", "guardado"));
}

Combinando @RequestBody con @PathVariable

Los endpoints POST pueden combinar datos del cuerpo de la petición con variables de ruta para operaciones más específicas.

@PostMapping("/{categoriaId}/productos")
public ResponseEntity<Producto> agregarProductoACategoria(
        @PathVariable Long categoriaId,
        @Valid @RequestBody Producto producto) {
    
    // Verificar que la categoría existe
    if (!categoriaService.existe(categoriaId)) {
        return ResponseEntity.notFound().build();
    }
    
    // Asignar la categoría al producto
    producto.setCategoriaId(categoriaId);
    
    Producto productoGuardado = productoService.crear(producto);
    return ResponseEntity.status(HttpStatus.CREATED).body(productoGuardado);
}

Respuestas enriquecidas con headers

Los endpoints POST pueden incluir headers adicionales en las respuestas para proporcionar información contextual sobre el recurso creado.

@PostMapping("/documentos")
public ResponseEntity<Documento> subirDocumento(@Valid @RequestBody DocumentoRequest request) {
    Documento documento = documentoService.crear(request);
    
    return ResponseEntity.status(HttpStatus.CREATED)
            .header("Location", "/api/documentos/" + documento.getId())
            .header("X-Resource-Version", documento.getVersion().toString())
            .header("X-Created-At", documento.getFechaCreacion().toString())
            .body(documento);
}

PUT y PATCH

Los métodos PUT y PATCH están diseñados para actualizar recursos existentes, pero con enfoques conceptualmente diferentes que determinan cuándo usar cada uno. Spring Boot proporciona las anotaciones @PutMapping y @PatchMapping para implementar estos endpoints, cada una con sus propias características y casos de uso específicos.

Diferencias conceptuales fundamentales

PUT representa una actualización completa del recurso, donde el cliente debe enviar la representación completa del objeto que reemplazará al existente. Este método es idempotente, lo que significa que realizar la misma operación múltiples veces produce el mismo resultado.

PATCH, por otro lado, permite actualizaciones parciales donde solo se envían los campos que necesitan modificarse. Aunque puede ser idempotente, no está obligado a serlo según la especificación HTTP.

@RestController
@RequestMapping("/api/usuarios")
public class UsuarioController {

    // PUT - Actualización completa
    @PutMapping("/{id}")
    public ResponseEntity<Usuario> actualizarUsuarioCompleto(
            @PathVariable Long id,
            @Valid @RequestBody Usuario usuario) {
        
        Usuario usuarioExistente = usuarioService.buscarPorId(id);
        
        if (usuarioExistente == null) {
            return ResponseEntity.notFound().build();
        }
        
        // PUT reemplaza completamente el recurso
        usuario.setId(id);
        Usuario usuarioActualizado = usuarioService.actualizar(usuario);
        
        return ResponseEntity.ok(usuarioActualizado);
    }
}

Implementación de @PutMapping

Los métodos @PutMapping requieren que el cliente envíe todos los campos del recurso, incluso aquellos que no van a cambiar. Los campos omitidos pueden ser establecidos como null o valores por defecto.

@PutMapping("/{id}")
public ResponseEntity<Producto> reemplazarProducto(
        @PathVariable Long id,
        @Valid @RequestBody Producto producto) {
    
    if (!productoService.existe(id)) {
        return ResponseEntity.notFound().build();
    }
    
    // Validaciones de negocio específicas para PUT
    if (producto.getPrecio() != null && producto.getPrecio().compareTo(BigDecimal.ZERO) <= 0) {
        return ResponseEntity.badRequest().build();
    }
    
    // Preservar campos que no deben cambiar
    producto.setId(id);
    producto.setFechaCreacion(productoService.obtenerFechaCreacion(id));
    
    Producto productoActualizado = productoService.reemplazar(producto);
    
    return ResponseEntity.ok()
            .header("Last-Modified", productoActualizado.getFechaModificacion().toString())
            .body(productoActualizado);
}

Implementación de @PatchMapping

@PatchMapping ofrece mayor flexibilidad al permitir actualizaciones selectivas. Spring Boot puede manejar automáticamente los campos null como valores a ignorar en lugar de establecer.

@PatchMapping("/{id}")
public ResponseEntity<Usuario> actualizarUsuarioParcial(
        @PathVariable Long id,
        @RequestBody Map<String, Object> camposActualizar) {
    
    Usuario usuarioExistente = usuarioService.buscarPorId(id);
    
    if (usuarioExistente == null) {
        return ResponseEntity.notFound().build();
    }
    
    // Procesar solo los campos enviados
    camposActualizar.forEach((campo, valor) -> {
        switch (campo) {
            case "nombre":
                usuarioExistente.setNombre((String) valor);
                break;
            case "email":
                usuarioExistente.setEmail((String) valor);
                break;
            case "activo":
                usuarioExistente.setActivo((Boolean) valor);
                break;
            // Ignorar campos no permitidos o no reconocidos
        }
    });
    
    Usuario usuarioActualizado = usuarioService.guardar(usuarioExistente);
    return ResponseEntity.ok(usuarioActualizado);
}

Manejo avanzado con DTOs específicos

Para actualizaciones parciales más estructuradas, es recomendable crear DTOs específicos que representen claramente qué campos pueden actualizarse.

public class ActualizarUsuarioDTO {
    private String nombre;
    private String email;
    private Boolean activo;
    
    // Getters y setters
}

@PatchMapping("/{id}")
public ResponseEntity<Usuario> actualizarUsuario(
        @PathVariable Long id,
        @Valid @RequestBody ActualizarUsuarioDTO datosActualizacion) {
    
    Usuario usuario = usuarioService.buscarPorId(id);
    
    if (usuario == null) {
        return ResponseEntity.notFound().build();
    }
    
    // Aplicar solo los campos no nulos
    if (datosActualizacion.getNombre() != null) {
        usuario.setNombre(datosActualizacion.getNombre());
    }
    
    if (datosActualizacion.getEmail() != null) {
        // Validar que el email no esté en uso
        if (usuarioService.existeOtroUsuarioConEmail(id, datosActualizacion.getEmail())) {
            return ResponseEntity.badRequest().build();
        }
        usuario.setEmail(datosActualizacion.getEmail());
    }
    
    if (datosActualizacion.getActivo() != null) {
        usuario.setActivo(datosActualizacion.getActivo());
    }
    
    Usuario usuarioGuardado = usuarioService.guardar(usuario);
    return ResponseEntity.ok(usuarioGuardado);
}

Validación condicional en actualizaciones

Las validaciones en PUT y PATCH pueden diferir según el contexto de la actualización. PUT debe validar el objeto completo, mientras que PATCH solo valida los campos proporcionados.

@PutMapping("/{id}")
public ResponseEntity<?> actualizarPerfilCompleto(
        @PathVariable Long id,
        @Valid @RequestBody PerfilUsuario perfil) {
    
    // Validación completa del perfil
    if (perfil.getFechaNacimiento().isAfter(LocalDate.now().minusYears(13))) {
        return ResponseEntity.badRequest()
                .body(Map.of("error", "El usuario debe ser mayor de 13 años"));
    }
    
    return ResponseEntity.ok(perfilService.reemplazar(id, perfil));
}

@PatchMapping("/{id}/preferencias")
public ResponseEntity<Usuario> actualizarPreferencias(
        @PathVariable Long id,
        @RequestBody PreferenciasDTO preferencias) {
    
    Usuario usuario = usuarioService.buscarPorId(id);
    
    if (usuario == null) {
        return ResponseEntity.notFound().build();
    }
    
    // Validaciones específicas para cada preferencia
    if (preferencias.getIdioma() != null) {
        if (!Arrays.asList("es", "en", "fr").contains(preferencias.getIdioma())) {
            return ResponseEntity.badRequest()
                    .body(null);
        }
        usuario.setIdioma(preferencias.getIdioma());
    }
    
    if (preferencias.getNotificacionesEmail() != null) {
        usuario.setNotificacionesEmail(preferencias.getNotificacionesEmail());
    }
    
    return ResponseEntity.ok(usuarioService.guardar(usuario));
}

Optimización con verificación de cambios

Para mejorar el rendimiento, especialmente en PATCH, es útil verificar si realmente hay cambios antes de persistir.

@PatchMapping("/{id}")
public ResponseEntity<Producto> actualizarProducto(
        @PathVariable Long id,
        @RequestBody ProductoActualizacionDTO actualizacion) {
    
    Producto producto = productoService.buscarPorId(id);
    
    if (producto == null) {
        return ResponseEntity.notFound().build();
    }
    
    boolean huboCambios = false;
    
    if (actualizacion.getNombre() != null && 
        !actualizacion.getNombre().equals(producto.getNombre())) {
        producto.setNombre(actualizacion.getNombre());
        huboCambios = true;
    }
    
    if (actualizacion.getPrecio() != null && 
        !actualizacion.getPrecio().equals(producto.getPrecio())) {
        producto.setPrecio(actualizacion.getPrecio());
        huboCambios = true;
    }
    
    if (!huboCambios) {
        // Retornar 304 Not Modified si no hay cambios
        return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
    }
    
    producto.setFechaModificacion(LocalDateTime.now());
    Producto productoActualizado = productoService.guardar(producto);
    
    return ResponseEntity.ok(productoActualizado);
}

Manejo de relaciones y actualizaciones complejas

Cuando los recursos tienen relaciones complejas, PUT y PATCH manejan estas asociaciones de manera diferente.

@PutMapping("/{id}/configuracion")
public ResponseEntity<ConfiguracionUsuario> reemplazarConfiguracion(
        @PathVariable Long id,
        @Valid @RequestBody ConfiguracionUsuario nuevaConfiguracion) {
    
    if (!usuarioService.existe(id)) {
        return ResponseEntity.notFound().build();
    }
    
    // PUT reemplaza completamente la configuración
    configuracionService.eliminarConfiguracionExistente(id);
    nuevaConfiguracion.setUsuarioId(id);
    
    ConfiguracionUsuario configuracionGuardada = 
            configuracionService.crear(nuevaConfiguracion);
    
    return ResponseEntity.ok(configuracionGuardada);
}

@PatchMapping("/{id}/configuracion/notificaciones")
public ResponseEntity<ConfiguracionUsuario> actualizarNotificaciones(
        @PathVariable Long id,
        @RequestBody Map<String, Boolean> notificaciones) {
    
    ConfiguracionUsuario config = configuracionService.buscarPorUsuario(id);
    
    if (config == null) {
        return ResponseEntity.notFound().build();
    }
    
    // PATCH actualiza selectivamente las notificaciones
    notificaciones.forEach((tipo, habilitada) -> {
        switch (tipo) {
            case "email":
                config.setNotificacionEmail(habilitada);
                break;
            case "push":
                config.setNotificacionPush(habilitada);
                break;
            case "sms":
                config.setNotificacionSms(habilitada);
                break;
        }
    });
    
    return ResponseEntity.ok(configuracionService.guardar(config));
}

DELETE

Los métodos DELETE están diseñados para eliminar recursos existentes del sistema, representando una de las operaciones más críticas en las APIs REST debido a su naturaleza irreversible en muchos casos. En Spring Boot, la anotación @DeleteMapping facilita la implementación de estos endpoints, proporcionando control granular sobre cómo y cuándo se eliminan los recursos.

Fundamentos de @DeleteMapping

La anotación @DeleteMapping actúa como un alias especializado para @RequestMapping(method = RequestMethod.DELETE), ofreciendo una sintaxis clara y expresiva para endpoints de eliminación. A diferencia de otros métodos HTTP, DELETE generalmente no requiere un cuerpo en la petición, basándose en variables de ruta o parámetros de consulta para identificar el recurso a eliminar.

@RestController
@RequestMapping("/api/productos")
public class ProductoController {

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> eliminarProducto(@PathVariable Long id) {
        if (!productoService.existe(id)) {
            return ResponseEntity.notFound().build();
        }
        
        productoService.eliminar(id);
        return ResponseEntity.noContent().build();
    }
}

Este ejemplo demuestra la estructura básica de un endpoint DELETE, donde se verifica la existencia del recurso antes de proceder con la eliminación y se retorna el código de estado HTTP 204 (No Content) para indicar una eliminación exitosa.

Códigos de respuesta apropiados

La selección del código de estado HTTP correcto es fundamental en los métodos DELETE para comunicar efectivamente el resultado de la operación al cliente.

@DeleteMapping("/{id}")
public ResponseEntity<Map<String, String>> eliminarUsuario(@PathVariable Long id) {
    try {
        Usuario usuario = usuarioService.buscarPorId(id);
        
        if (usuario == null) {
            return ResponseEntity.notFound().build(); // 404 Not Found
        }
        
        // Verificar si el usuario puede ser eliminado
        if (usuarioService.tieneRecursosAsociados(id)) {
            return ResponseEntity.status(HttpStatus.CONFLICT)
                    .body(Map.of("error", "El usuario tiene recursos asociados y no puede eliminarse"));
        }
        
        usuarioService.eliminar(id);
        return ResponseEntity.noContent().build(); // 204 No Content
        
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Map.of("error", "Error interno al eliminar el usuario"));
    }
}

Eliminación suave vs eliminación física

Las aplicaciones empresariales frecuentemente requieren dos tipos de eliminación: física (hard delete) y suave (soft delete). La eliminación suave mantiene los datos marcándolos como inactivos, mientras que la eliminación física los remueve permanentemente de la base de datos.

Implementación de eliminación suave:

@DeleteMapping("/{id}")
public ResponseEntity<Map<String, Object>> desactivarDocumento(@PathVariable Long id) {
    Documento documento = documentoService.buscarPorId(id);
    
    if (documento == null) {
        return ResponseEntity.notFound().build();
    }
    
    if (documento.isEliminado()) {
        return ResponseEntity.status(HttpStatus.GONE)
                .body(Map.of("mensaje", "El documento ya fue eliminado previamente"));
    }
    
    // Eliminación suave - marcar como eliminado
    documento.setEliminado(true);
    documento.setFechaEliminacion(LocalDateTime.now());
    documento.setEliminadoPor(usuarioService.obtenerUsuarioActual().getId());
    
    documentoService.guardar(documento);
    
    return ResponseEntity.ok()
            .body(Map.of(
                "mensaje", "Documento eliminado correctamente",
                "recuperable", true,
                "fechaEliminacion", documento.getFechaEliminacion()
            ));
}

Endpoint para eliminación física definitiva:

@DeleteMapping("/{id}/definitivo")
public ResponseEntity<Void> eliminarDefinitivamente(@PathVariable Long id) {
    Documento documento = documentoService.buscarPorIdIncuyendoEliminados(id);
    
    if (documento == null) {
        return ResponseEntity.notFound().build();
    }
    
    // Validar permisos para eliminación definitiva
    if (!usuarioService.tienePermisoEliminacionDefinitiva()) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
    }
    
    // Eliminar archivos asociados del sistema de almacenamiento
    if (documento.getTieneArchivos()) {
        archivoService.eliminarArchivosFisicos(documento.getId());
    }
    
    // Eliminación física de la base de datos
    documentoService.eliminarFisicamente(id);
    
    return ResponseEntity.noContent().build();
}

Validaciones antes de la eliminación

Las validaciones previas son cruciales para mantener la integridad referencial y evitar eliminaciones que puedan causar inconsistencias en el sistema.

@DeleteMapping("/categorias/{id}")
public ResponseEntity<?> eliminarCategoria(@PathVariable Long id) {
    Categoria categoria = categoriaService.buscarPorId(id);
    
    if (categoria == null) {
        return ResponseEntity.notFound().build();
    }
    
    // Validar que no tenga productos asociados
    if (categoriaService.tieneProductosAsociados(id)) {
        return ResponseEntity.status(HttpStatus.CONFLICT)
                .body(Map.of(
                    "error", "No se puede eliminar la categoría",
                    "razon", "Tiene productos asociados",
                    "productosAsociados", categoriaService.contarProductos(id)
                ));
    }
    
    // Validar que no sea una categoría del sistema
    if (categoria.isSistema()) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN)
                .body(Map.of("error", "Las categorías del sistema no pueden eliminarse"));
    }
    
    // Verificar dependencias circulares en categorías padre/hijo
    if (categoriaService.tieneSubcategorias(id)) {
        return ResponseEntity.status(HttpStatus.CONFLICT)
                .body(Map.of(
                    "error", "Elimine primero las subcategorías",
                    "subcategorias", categoriaService.obtenerSubcategorias(id)
                ));
    }
    
    categoriaService.eliminar(id);
    return ResponseEntity.noContent().build();
}

Eliminación en lotes

Para operaciones que requieren eliminar múltiples recursos simultáneamente, es útil implementar endpoints que manejen eliminaciones en lotes de forma eficiente.

@DeleteMapping("/lote")
public ResponseEntity<Map<String, Object>> eliminarProductosEnLote(
        @RequestBody List<Long> idsProductos) {
    
    if (idsProductos == null || idsProductos.isEmpty()) {
        return ResponseEntity.badRequest()
                .body(Map.of("error", "La lista de IDs no puede estar vacía"));
    }
    
    if (idsProductos.size() > 100) {
        return ResponseEntity.badRequest()
                .body(Map.of("error", "No se pueden eliminar más de 100 productos por lote"));
    }
    
    List<Long> eliminados = new ArrayList<>();
    List<Map<String, Object>> errores = new ArrayList<>();
    
    for (Long id : idsProductos) {
        try {
            if (!productoService.existe(id)) {
                errores.add(Map.of("id", id, "error", "Producto no encontrado"));
                continue;
            }
            
            if (productoService.tienePedidosAsociados(id)) {
                errores.add(Map.of("id", id, "error", "Tiene pedidos asociados"));
                continue;
            }
            
            productoService.eliminar(id);
            eliminados.add(id);
            
        } catch (Exception e) {
            errores.add(Map.of("id", id, "error", e.getMessage()));
        }
    }
    
    return ResponseEntity.ok()
            .body(Map.of(
                "eliminados", eliminados,
                "errores", errores,
                "total", idsProductos.size(),
                "exitosos", eliminados.size(),
                "fallidos", errores.size()
            ));
}

Eliminación con confirmación

Para recursos críticos, es recomendable implementar un mecanismo de confirmación que requiera una acción explícita del usuario antes de proceder con la eliminación.

@DeleteMapping("/{id}/confirmar")
public ResponseEntity<?> eliminarConConfirmacion(
        @PathVariable Long id,
        @RequestParam String token) {
    
    // Validar el token de confirmación
    if (!tokenService.esValido(token, "eliminar_cuenta", id)) {
        return ResponseEntity.badRequest()
                .body(Map.of("error", "Token de confirmación inválido o expirado"));
    }
    
    Cuenta cuenta = cuentaService.buscarPorId(id);
    
    if (cuenta == null) {
        return ResponseEntity.notFound().build();
    }
    
    // Realizar operaciones de limpieza antes de eliminar
    cuentaService.cancelarSuscripciones(id);
    cuentaService.transferirDatosAArchivo(id);
    cuentaService.notificarEliminacionAServicios(id);
    
    // Eliminar la cuenta
    cuentaService.eliminarDefinitivamente(id);
    
    // Invalidar el token usado
    tokenService.invalidar(token);
    
    return ResponseEntity.noContent()
            .header("X-Account-Deleted", "true")
            .header("X-Deletion-Time", LocalDateTime.now().toString())
            .build();
}

Fuentes y referencias

Documentación oficial y recursos externos para profundizar en SpringBoot

Documentación oficial de SpringBoot
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, SpringBoot es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de SpringBoot

Explora más contenido relacionado con SpringBoot y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

  • Comprender el uso de las anotaciones @GetMapping, @PostMapping, @PutMapping, @PatchMapping y @DeleteMapping en Spring Boot.
  • Aprender a manejar parámetros de ruta, parámetros de consulta y cuerpos de petición en métodos REST.
  • Conocer las diferencias conceptuales y prácticas entre PUT y PATCH para actualizaciones de recursos.
  • Implementar validaciones automáticas y manejo de errores en peticiones REST.
  • Aplicar estrategias para respuestas HTTP adecuadas y manejo avanzado de eliminación de recursos.