Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado el primer workflow. En Servidor continúas la implementación del mismo producto.
Se explica
25 minutos · explicación y demostración
Ya sabes cómo llegan los datos al controlador. Hoy precisarás qué significa repetir una escritura. Una operación idempotente deja el mismo estado final al repetirse; no exige que todas las respuestas sean idénticas. Usarás esa distinción para comparar PUT, PATCH y DELETE.
Lo que ya haces y todavía no sabes justificar
En la UD1 escribiste POST, PUT y DELETE, y funcionan. Ante la pregunta de por qué modificar corresponde a PUT y no a POST, sin embargo, la respuesta honesta hoy sería «porque lo indican los apuntes».
Esta sesión pasa de la mecánica al criterio, que se apoya en dos propiedades no opinables: están definidas en la especificación de HTTP y el resto de Internet cuenta con ellas.
Seguro e idempotente
Seguro
Una operación es segura cuando no modifica nada. Solo consulta. GET es seguro.
Idempotente
Una operación es idempotente cuando hacerla una vez y hacerla diez veces dejan el servidor igual. Ojo: no se trata de que devuelva lo mismo, sino de que el efecto acumulado sea el mismo.
| Método | ¿Seguro? | ¿Idempotente? | Efecto de repetirlo cinco veces |
|---|---|---|---|
GET |
Sí | Sí | Nada, cinco veces |
POST |
No | No | Cinco recursos nuevos |
PUT |
No | Sí | El recurso queda igual que tras la primera |
PATCH |
No | Depende | Normalmente igual, pero no está garantizado |
DELETE |
No | Sí | Se borra una vez; las otras cuatro no hay nada que borrar |
Por qué esto no es teoría
Imagina esta situación, que ocurre todos los días:
- El cliente envía la petición
- El servidor la recibe y la procesa correctamente
- La respuesta se pierde por el camino: se corta la red, expira el tiempo de espera
- El cliente no ha recibido nada. No sabe si se hizo o no
- El cliente reintenta
Ahí está todo el asunto. Si la operación era un PUT, reintentar es seguro: el recurso acaba en el mismo estado. Si era un POST, reintentar crea un segundo recurso, y acabas de duplicar una incidencia, un pedido o un cobro.
Por eso las reglas no son un capricho
Los navegadores, los proxies, las pasarelas y las librerías cliente reintentan automáticamente las operaciones idempotentes cuando fallan, y no reintentan las que no lo son. Cuentan con que respetes la semántica.
Si escribes un GET que crea cosas, o un POST donde debía ir un PUT, no estás rompiendo una convención de estilo: estás rompiendo suposiciones que otros programas ya están haciendo sobre tu API.
Se trabaja
140 minutos · implementación guiada sobre el proyecto propio
Paso 1 · Retomar el proyecto y preparar la comprobación
- Abre los métodos de escritura del controlador y sus peticiones en Postman o Bruno. Crea un registro con todos sus campos informados.
- Localiza el PUT que sustituye el objeto. Antes de editar, guarda un GET de detalle que permita comprobar el contenido después de cada escritura.
- Anota qué campos debe conservar un PATCH si no los envías y qué política tendrá tu API al borrar un id inexistente.
Paso 2 · Comprobar que PUT sustituye la representación completa
Aquí está el error conceptual más extendido del tema. PUT no significa «actualiza esto». Significa:
Toma esta representación completa y deja el recurso exactamente así.
Lo que no envías, no se conserva: se pierde, porque estás diciendo cómo debe quedar el recurso entero.
Con una tarea ya creada que tenga título, prioridad y estado:
PUT /tareas/1
{
"titulo": "Revisar el login otra vez"
}
Mira la tarea después con un GET /tareas/1. La prioridad ha desaparecido, y lo ha hecho correctamente: has dicho que la tarea, entera, es solo eso.
Casi nadie quiere eso. Lo que casi todo el mundo quiere es cambiar un campo y dejar el resto en paz. Para eso existe el otro método.
Paso 3 · PATCH · cambiar solo lo que envías
Añade el método PATCH al controlador y el import org.springframework.web.bind.annotation.PatchMapping. El bloque conserva una búsqueda por id y cambia solo los campos cuyo valor recibido no sea nulo. Compruébalo con una tarea que tenga título y prioridad: envía solo el título y vuelve a consultar para verificar que conserva la prioridad. No utilices aún este procedimiento para distinguir false de un booleano omitido: se resuelve con un DTO específico en la sesión 11.
PUT · sustituye
«El recurso, completo, es esto.» Lo que falta en el cuerpo se pierde. Idempotente.
PATCH · modifica
«De este recurso, cambia estas cosas.» Lo que falta en el cuerpo se queda como estaba.
La implementación es el ejemplo resuelto de hoy:
@PatchMapping("/{id}")
public Tarea modificar(
@PathVariable(name = "id") int id,
@RequestBody Tarea cambios) {
for (Tarea tarea : tareas) {
if (tarea.getId() == id) {
if (cambios.getTitulo() != null) {
tarea.setTitulo(cambios.getTitulo());
}
if (cambios.getPrioridad() != null) {
tarea.setPrioridad(cambios.getPrioridad());
}
return tarea;
}
}
return null;
}
- Por qué cada campo lleva su
if - Porque de el trabajo anterior sabes que lo que no viene en el JSON llega como
null. Esenulles la señal de «no me han mandado esto», y eliflo traduce a «no lo toques». - Por qué no se toca el id
- Igual que en
PUT: el recurso que se modifica lo dice la ruta. Si el cuerpo trae un id, se ignora. - Por qué esto se vuelve pesado enseguida
- Con cuatro campos son cuatro
if. Con quince son quince, y hay que acordarse de añadir uno cada vez que crece el modelo. Es un olor a que falta una herramienta, y la herramienta llega en la UD3.
Dos limitaciones de esta versión · anótalas
Uno. Con este código no se puede vaciar un campo a propósito. Si envías {"prioridad": null} pidiendo borrar la prioridad, tu if lo interpreta como «no me lo han mandado» y no hace nada. No hay forma de distinguir «ausente» de «enviado como nulo», y es la misma distinción que ya apareció con defaultValue en la UD1.
Dos. El campo completada es un boolean primitivo, que no puede valer null: siempre llega como false, y no hay manera de saber si te lo han enviado. Por eso no aparece en el código de arriba.
Las dos se resuelven con una clase distinta para la entrada, con tipos que admitan nulo. Eso es un DTO, y es la UD3.
Paso 4 · Repetir DELETE y distinguir estado de los datos y respuesta
@DeleteMapping("/{id}")
public void eliminar(@PathVariable(name = "id") int id) {
tareas.removeIf(tarea -> tarea.getId() == id);
}
Ejecútalo dos veces seguidas sobre la misma tarea. La primera borra; la segunda no encuentra nada y no hace nada. Y eso está bien.
Es la idempotencia en acción: el resultado que pedías —que esa tarea no exista— se cumple igual la primera vez que la quinta. Un DELETE repetido no debe explotar ni quejarse; el estado final es el que el cliente pidió.
Entonces, ¿la segunda vez debería devolver 404?
Es una discusión clásica y no hay una respuesta única. Los dos criterios son defendibles:
204 siempre: el cliente pidió que no existiera, y no existe. Objetivo cumplido, no hay nada que reportar.
404 la segunda vez: es información honesta, «eso que me pides borrar ya no está».
Lo importante es que sea una decisión escrita y no un accidente. La tomarás en el trabajo siguiente, cuando aprendas a fijar el código de estado.
Paso 5 · Registrar el estado final después de repetir cada escritura
Esta es la comprobación que demuestra que has entendido la sesión. Sobre tu API, ejecuta cada operación dos veces seguidas y anota el estado del servidor después de cada una.
| Operación | Estado tras la 1.ª | Estado tras la 2.ª | ¿Idempotente? |
|---|---|---|---|
POST /tareas con la misma tarea |
|||
PUT /tareas/1 con el mismo cuerpo |
|||
PATCH /tareas/1 con {"prioridad":"baja"} |
|||
DELETE /tareas/1 |
|||
GET /tareas/1 |
Para «estado del servidor», usa un GET /tareas después de cada paso y anota cuántas tareas hay y cómo están.
Una de las filas debería incomodarte: la del POST. Después de dos ejecuciones tienes dos tareas idénticas con ids distintos, y tu API no tiene forma de saber que la segunda era un reintento. No lo arreglamos hoy, pero que conste que lo has visto.
Paso 6 · Las escrituras de proyectos
En el controlador de tu segunda entidad, implementa primero PUT copiando el recorrido buscar → comprobar existencia → asignar todos los campos editables. Después crea PATCH con asignaciones condicionadas para los campos opcionales y, por último, DELETE sobre el id. Prueba cada operación antes de añadir la siguiente. En las tablas registra también el GET posterior: repetir una respuesta correcta no demuestra por sí solo que se hayan guardado los datos esperados.
- Implementa
PUT /proyectos/{id}con semántica de sustitución completa. - Implementa
PATCH /proyectos/{id}que solo cambie los campos enviados. - Implementa
DELETE /proyectos/{id}. - Demuestra con dos peticiones consecutivas que
PUTdeja el recurso igual y quePOSTno. - Escribe un comentario en el código explicando qué campo de tu modelo no puedes modificar con
PATCHy por qué.
Paso 7 · Comprobar y registrar el resultado del proyecto
- Envía dos veces el mismo PUT y consulta el recurso después de cada envío. El estado final debe ser el mismo.
- Envía un PATCH que cambie un único campo y verifica que conserva los demás. Repite un DELETE y explica por qué el recurso sigue ausente aunque pueda cambiar el código de respuesta.
Ampliación si has completado el trabajo
Primero termina y verifica los pasos anteriores. Estos retos profundizan en el mismo contenido; no sustituyen la entrega ni obligan a iniciar otro proyecto.
Reto · Elige el método y defiéndelo
Para cada situación, di qué método HTTP usarías, qué ruta, y qué pasaría si el cliente la repitiera por un reintento. No hay una única respuesta correcta en todas; hay respuestas defendibles y respuestas que no lo son.
- Marcar una incidencia como resuelta.
- Añadir un comentario a una incidencia.
- Cambiar el correo de un usuario.
- Archivar todas las incidencias cerradas de un proyecto.
- Asignar una incidencia a una persona.
- Sustituir por completo la lista de etiquetas de una incidencia.
Para las seis, escribe la respuesta en este formato:
| Situación | Método | Ruta | Si se repite… | ¿Es correcto que se repita? |
|---|
La número 4 es la difícil, y merece un párrafo aparte explicando tu decisión: no encaja limpiamente en ninguna de las categorías de hoy, y saber decir por qué vale más que acertar.
Ver respuestas
1 · Que repetirla deja el servidor en el mismo estado que hacerla una sola vez. Le importa porque, cuando una respuesta se pierde, el cliente no sabe si la petición llegó, y solo puede reintentar sin riesgo si la operación es idempotente.
2 · Se pierde. PUT declara cómo queda el recurso completo, así que lo que no aparece en el cuerpo deja de estar.
3 · Porque el POST crea un recurso nuevo cada vez y acabas con duplicados, mientras que el PUT deja el mismo recurso en el mismo estado, se ejecute una vez o diez.
4 · Porque un campo ausente y un campo enviado como null llegan los dos como null, y el código no puede distinguirlos. Hace falta una clase de entrada con tipos que sepan expresar esa diferencia.
Cierre
15 minutos · resultado comprobable y explicación individual
Al terminar la sesión:
El cliente distingue una creación, una modificación, un borrado y un recurso inexistente por la respuesta recibida.
Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.