50% OFF Plus
--:--:--
¡Obtener!

Métodos PUT y PATCH en controladores REST

Avanzado
SpringBoot
SpringBoot
Actualizado: 13/06/2025

¡Desbloquea el curso de SpringBoot completo!

IA
Ejercicios
Certificado
Entrar

Mira la lección en vídeo

Accede al vídeo completo de esta lección y a más contenido exclusivo con el Plan Plus.

Desbloquear Plan Plus

Edición con PutMapping

La anotación @PutMapping está diseñada específicamente para manejar operaciones de actualización completa de recursos en APIs REST. A diferencia de otros métodos HTTP, PUT requiere que el cliente envíe una representación completa del recurso, reemplazando todos los datos existentes.

Características del método PUT

El protocolo HTTP define PUT como un método idempotente, lo que significa que realizar la misma operación múltiples veces produce el mismo resultado. Esta característica es fundamental para garantizar la consistencia en aplicaciones distribuidas donde las peticiones pueden duplicarse por problemas de red.

@RestController
@RequestMapping("/api/usuarios")
public class UsuarioController {
    
    @Autowired
    private UsuarioService usuarioService;
    
    @PutMapping("/{id}")
    public ResponseEntity<Usuario> actualizarUsuario(
            @PathVariable Long id, 
            @RequestBody Usuario usuario) {
        
        // Verificar que el usuario existe
        if (!usuarioService.existeUsuario(id)) {
            return ResponseEntity.notFound().build();
        }
        
        // Establecer el ID del usuario a actualizar
        usuario.setId(id);
        
        // Realizar la actualización completa
        Usuario usuarioActualizado = usuarioService.actualizarCompleto(usuario);
        
        return ResponseEntity.ok(usuarioActualizado);
    }
}

Validación de datos en PUT

Cuando trabajamos con actualizaciones completas, es esencial validar que todos los campos requeridos estén presentes. Spring Boot proporciona herramientas integradas para esta validación:

@Entity
public class Usuario {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "El nombre es obligatorio")
    @Size(min = 2, max = 50, message = "El nombre debe tener entre 2 y 50 caracteres")
    private String nombre;
    
    @Email(message = "El email debe tener un formato válido")
    @NotBlank(message = "El email es obligatorio")
    private String email;
    
    @NotNull(message = "La fecha de nacimiento es obligatoria")
    private LocalDate fechaNacimiento;
    
    // Constructores, getters y setters
}

El controlador debe incluir la anotación @Valid para activar la validación automática:

@PutMapping("/{id}")
public ResponseEntity<?> actualizarUsuario(
        @PathVariable Long id, 
        @Valid @RequestBody Usuario usuario,
        BindingResult result) {
    
    // Verificar errores de validación
    if (result.hasErrors()) {
        Map<String, String> errores = new HashMap<>();
        result.getFieldErrors().forEach(error -> 
            errores.put(error.getField(), error.getDefaultMessage())
        );
        return ResponseEntity.badRequest().body(errores);
    }
    
    if (!usuarioService.existeUsuario(id)) {
        return ResponseEntity.notFound().build();
    }
    
    usuario.setId(id);
    Usuario usuarioActualizado = usuarioService.actualizarCompleto(usuario);
    
    return ResponseEntity.ok(usuarioActualizado);
}

Implementación del servicio para PUT

El servicio debe manejar la lógica de actualización completa, asegurándose de que todos los campos se actualicen correctamente:

@Service
public class UsuarioService {
    
    @Autowired
    private UsuarioRepository usuarioRepository;
    
    public Usuario actualizarCompleto(Usuario usuarioNuevo) {
        // Buscar el usuario existente
        Usuario usuarioExistente = usuarioRepository.findById(usuarioNuevo.getId())
            .orElseThrow(() -> new EntityNotFoundException("Usuario no encontrado"));
        
        // Actualizar todos los campos
        usuarioExistente.setNombre(usuarioNuevo.getNombre());
        usuarioExistente.setEmail(usuarioNuevo.getEmail());
        usuarioExistente.setFechaNacimiento(usuarioNuevo.getFechaNacimiento());
        
        // Guardar y retornar
        return usuarioRepository.save(usuarioExistente);
    }
    
    public boolean existeUsuario(Long id) {
        return usuarioRepository.existsById(id);
    }
}

Manejo de respuestas HTTP apropiadas

Un endpoint PUT bien diseñado debe retornar códigos de estado HTTP apropiados según el resultado de la operación:

@PutMapping("/{id}")
public ResponseEntity<Usuario> actualizarUsuario(
        @PathVariable Long id, 
        @Valid @RequestBody Usuario usuario) {
    
    try {
        // Verificar existencia del recurso
        if (!usuarioService.existeUsuario(id)) {
            return ResponseEntity.notFound().build(); // 404
        }
        
        usuario.setId(id);
        Usuario usuarioActualizado = usuarioService.actualizarCompleto(usuario);
        
        return ResponseEntity.ok(usuarioActualizado); // 200
        
    } catch (DataIntegrityViolationException e) {
        // Error de integridad (email duplicado, etc.)
        return ResponseEntity.status(HttpStatus.CONFLICT).build(); // 409
        
    } catch (Exception e) {
        // Error interno del servidor
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); // 500
    }
}

Ejemplo práctico con entidad Producto

Veamos un ejemplo más completo con una entidad Producto que incluye múltiples campos:

@RestController
@RequestMapping("/api/productos")
public class ProductoController {
    
    @Autowired
    private ProductoService productoService;
    
    @PutMapping("/{id}")
    public ResponseEntity<Producto> actualizarProducto(
            @PathVariable Long id,
            @Valid @RequestBody Producto producto) {
        
        if (!productoService.existeProducto(id)) {
            return ResponseEntity.notFound().build();
        }
        
        producto.setId(id);
        
        try {
            Producto productoActualizado = productoService.actualizarCompleto(producto);
            return ResponseEntity.ok(productoActualizado);
            
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().build();
        }
    }
}

La entidad Producto con sus validaciones correspondientes:

@Entity
public class Producto {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NotBlank(message = "El nombre del producto es obligatorio")
    private String nombre;
    
    @NotBlank(message = "La descripción es obligatoria")
    private String descripcion;
    
    @DecimalMin(value = "0.0", inclusive = false, message = "El precio debe ser mayor que 0")
    @NotNull(message = "El precio es obligatorio")
    private BigDecimal precio;
    
    @Min(value = 0, message = "El stock no puede ser negativo")
    @NotNull(message = "El stock es obligatorio")
    private Integer stock;
    
    @NotBlank(message = "La categoría es obligatoria")
    private String categoria;
    
    // Constructores, getters y setters
}

Esta implementación garantiza que las actualizaciones completas se realicen de manera consistente, validando todos los datos y proporcionando respuestas HTTP apropiadas para cada escenario posible.

Edición parcial con PatchMapping

Guarda tu progreso

Inicia sesión para no perder tu progreso y accede a miles de tutoriales, ejercicios prácticos y nuestro asistente de IA.

Progreso guardado
Asistente IA
Ejercicios
Iniciar sesión gratis

Más de 25.000 desarrolladores ya confían en CertiDevs

La anotación @PatchMapping está específicamente diseñada para realizar actualizaciones parciales de recursos, permitiendo modificar únicamente los campos que el cliente desea cambiar sin afectar el resto de la información almacenada.

A diferencia de PUT que requiere enviar la representación completa del recurso, PATCH permite enviar solo los campos que necesitan modificación. Esta característica resulta especialmente útil en aplicaciones móviles donde el ancho de banda es limitado o cuando trabajamos con entidades que contienen muchos campos.

Implementación básica de PATCH

El enfoque más directo para implementar actualizaciones parciales consiste en verificar qué campos están presentes en la petición y actualizar únicamente esos valores:

@RestController
@RequestMapping("/api/usuarios")
public class UsuarioController {
    
    @Autowired
    private UsuarioService usuarioService;
    
    @PatchMapping("/{id}")
    public ResponseEntity<Usuario> actualizarParcial(
            @PathVariable Long id,
            @RequestBody Map<String, Object> campos) {
        
        if (!usuarioService.existeUsuario(id)) {
            return ResponseEntity.notFound().build();
        }
        
        try {
            Usuario usuarioActualizado = usuarioService.actualizarParcial(id, campos);
            return ResponseEntity.ok(usuarioActualizado);
            
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().build();
        }
    }
}

Servicio para actualizaciones parciales

El servicio debe implementar la lógica para aplicar solo los cambios especificados, manteniendo intactos los demás campos:

@Service
public class UsuarioService {
    
    @Autowired
    private UsuarioRepository usuarioRepository;
    
    public Usuario actualizarParcial(Long id, Map<String, Object> campos) {
        Usuario usuario = usuarioRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("Usuario no encontrado"));
        
        // Actualizar solo los campos presentes en el mapa
        campos.forEach((campo, valor) -> {
            switch (campo) {
                case "nombre":
                    if (valor != null && !valor.toString().trim().isEmpty()) {
                        usuario.setNombre(valor.toString());
                    }
                    break;
                    
                case "email":
                    if (valor != null && !valor.toString().trim().isEmpty()) {
                        usuario.setEmail(valor.toString());
                    }
                    break;
                    
                case "fechaNacimiento":
                    if (valor != null) {
                        usuario.setFechaNacimiento(LocalDate.parse(valor.toString()));
                    }
                    break;
                    
                default:
                    throw new IllegalArgumentException("Campo no válido: " + campo);
            }
        });
        
        return usuarioRepository.save(usuario);
    }
}

Validación en actualizaciones parciales

Las validaciones en PATCH requieren un enfoque diferente, ya que no todos los campos estarán presentes. Necesitamos validar solo los campos que se están actualizando:

@Service
public class UsuarioService {
    
    public Usuario actualizarParcial(Long id, Map<String, Object> campos) {
        Usuario usuario = usuarioRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("Usuario no encontrado"));
        
        // Validar cada campo antes de aplicar el cambio
        for (Map.Entry<String, Object> entrada : campos.entrySet()) {
            String campo = entrada.getKey();
            Object valor = entrada.getValue();
            
            switch (campo) {
                case "nombre":
                    validarNombre(valor);
                    usuario.setNombre(valor.toString());
                    break;
                    
                case "email":
                    validarEmail(valor);
                    usuario.setEmail(valor.toString());
                    break;
                    
                case "fechaNacimiento":
                    validarFechaNacimiento(valor);
                    usuario.setFechaNacimiento(LocalDate.parse(valor.toString()));
                    break;
                    
                default:
                    throw new IllegalArgumentException("Campo no válido: " + campo);
            }
        }
        
        return usuarioRepository.save(usuario);
    }
    
    private void validarNombre(Object valor) {
        if (valor == null || valor.toString().trim().isEmpty()) {
            throw new IllegalArgumentException("El nombre no puede estar vacío");
        }
        if (valor.toString().length() < 2 || valor.toString().length() > 50) {
            throw new IllegalArgumentException("El nombre debe tener entre 2 y 50 caracteres");
        }
    }
    
    private void validarEmail(Object valor) {
        if (valor == null || valor.toString().trim().isEmpty()) {
            throw new IllegalArgumentException("El email no puede estar vacío");
        }
        String emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$";
        if (!valor.toString().matches(emailRegex)) {
            throw new IllegalArgumentException("El formato del email no es válido");
        }
    }
    
    private void validarFechaNacimiento(Object valor) {
        if (valor == null) {
            throw new IllegalArgumentException("La fecha de nacimiento no puede ser nula");
        }
        try {
            LocalDate.parse(valor.toString());
        } catch (DateTimeParseException e) {
            throw new IllegalArgumentException("Formato de fecha inválido");
        }
    }
}

Uso de DTOs para mayor control

Para tener un mayor control sobre las actualizaciones parciales, podemos crear un DTO específico que represente los campos opcionales:

public class UsuarioPatchDTO {
    private String nombre;
    private String email;
    private LocalDate fechaNacimiento;
    
    // Getters y setters
    public String getNombre() { return nombre; }
    public void setNombre(String nombre) { this.nombre = nombre; }
    
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    public LocalDate getFechaNacimiento() { return fechaNacimiento; }
    public void setFechaNacimiento(LocalDate fechaNacimiento) { 
        this.fechaNacimiento = fechaNacimiento; 
    }
}

El controlador utilizando el DTO se simplifica considerablemente:

@PatchMapping("/{id}")
public ResponseEntity<Usuario> actualizarParcial(
        @PathVariable Long id,
        @RequestBody UsuarioPatchDTO patchDTO) {
    
    if (!usuarioService.existeUsuario(id)) {
        return ResponseEntity.notFound().build();
    }
    
    try {
        Usuario usuarioActualizado = usuarioService.actualizarConDTO(id, patchDTO);
        return ResponseEntity.ok(usuarioActualizado);
        
    } catch (IllegalArgumentException e) {
        return ResponseEntity.badRequest().build();
    }
}

Implementación del servicio con DTO

El servicio con DTO proporciona mayor claridad y facilita el mantenimiento del código:

public Usuario actualizarConDTO(Long id, UsuarioPatchDTO patchDTO) {
    Usuario usuario = usuarioRepository.findById(id)
        .orElseThrow(() -> new EntityNotFoundException("Usuario no encontrado"));
    
    // Actualizar solo los campos no nulos del DTO
    if (patchDTO.getNombre() != null) {
        validarNombre(patchDTO.getNombre());
        usuario.setNombre(patchDTO.getNombre());
    }
    
    if (patchDTO.getEmail() != null) {
        validarEmail(patchDTO.getEmail());
        usuario.setEmail(patchDTO.getEmail());
    }
    
    if (patchDTO.getFechaNacimiento() != null) {
        usuario.setFechaNacimiento(patchDTO.getFechaNacimiento());
    }
    
    return usuarioRepository.save(usuario);
}

Ejemplo práctico con entidad Producto

Veamos un ejemplo más complejo con una entidad Producto que incluye diferentes tipos de datos:

@PatchMapping("/{id}")
public ResponseEntity<Producto> actualizarProductoParcial(
        @PathVariable Long id,
        @RequestBody ProductoPatchDTO patchDTO) {
    
    if (!productoService.existeProducto(id)) {
        return ResponseEntity.notFound().build();
    }
    
    try {
        Producto productoActualizado = productoService.actualizarParcial(id, patchDTO);
        return ResponseEntity.ok(productoActualizado);
        
    } catch (IllegalArgumentException e) {
        Map<String, String> error = Map.of("error", e.getMessage());
        return ResponseEntity.badRequest().body(null);
    }
}

El DTO para actualizaciones parciales de productos:

public class ProductoPatchDTO {
    private String nombre;
    private String descripcion;
    private BigDecimal precio;
    private Integer stock;
    private String categoria;
    
    // Getters y setters omitidos por brevedad
}

La implementación del servicio para productos demuestra cómo manejar diferentes tipos de datos en actualizaciones parciales:

public Producto actualizarParcial(Long id, ProductoPatchDTO patchDTO) {
    Producto producto = productoRepository.findById(id)
        .orElseThrow(() -> new EntityNotFoundException("Producto no encontrado"));
    
    if (patchDTO.getNombre() != null) {
        if (patchDTO.getNombre().trim().isEmpty()) {
            throw new IllegalArgumentException("El nombre no puede estar vacío");
        }
        producto.setNombre(patchDTO.getNombre());
    }
    
    if (patchDTO.getPrecio() != null) {
        if (patchDTO.getPrecio().compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("El precio debe ser mayor que 0");
        }
        producto.setPrecio(patchDTO.getPrecio());
    }
    
    if (patchDTO.getStock() != null) {
        if (patchDTO.getStock() < 0) {
            throw new IllegalArgumentException("El stock no puede ser negativo");
        }
        producto.setStock(patchDTO.getStock());
    }
    
    if (patchDTO.getDescripcion() != null) {
        producto.setDescripcion(patchDTO.getDescripcion());
    }
    
    if (patchDTO.getCategoria() != null) {
        producto.setCategoria(patchDTO.getCategoria());
    }
    
    return productoRepository.save(producto);
}

Esta implementación de PATCH permite a los clientes realizar actualizaciones eficientes enviando únicamente los datos que necesitan modificar, optimizando el uso de recursos y mejorando la experiencia del usuario en aplicaciones donde las actualizaciones parciales son frecuentes.

Aprendizajes de esta lección de SpringBoot

  • Comprender la diferencia entre los métodos PUT y PATCH en APIs REST.
  • Implementar actualizaciones completas de recursos usando @PutMapping con validación de datos.
  • Desarrollar actualizaciones parciales de recursos mediante @PatchMapping y manejo selectivo de campos.
  • Aplicar validaciones adecuadas para cada tipo de actualización y gestionar errores.
  • Manejar respuestas HTTP apropiadas para reflejar el resultado de las operaciones de actualización.

Completa este curso de SpringBoot y certifícate

Únete a nuestra plataforma de cursos de programación y accede a miles de tutoriales, ejercicios prácticos, proyectos reales y nuestro asistente de IA personalizado para acelerar tu aprendizaje.

Asistente IA

Resuelve dudas al instante

Ejercicios

Practica con proyectos reales

Certificados

Valida tus conocimientos

Más de 25.000 desarrolladores ya se han certificado con CertiDevs

⭐⭐⭐⭐⭐
4.9/5 valoración