Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado issues, tablero y la primera pull request. En Servidor continúas la implementación del mismo producto.
Se explica
25 minutos · explicación y demostración
Ya puedes añadir y listar objetos en memoria. Hoy completas la consulta individual, la sustitución y el borrado. El identificador permitirá distinguir registros y pasará a asignarlo el servidor. PUT sustituye los datos de un recurso; DELETE solicita su eliminación.
Dónde estamos
Esto es lo que responde tu API al terminar la sesión 3:
| Método y ruta | Estado |
|---|---|
GET /tareas |
Funciona |
GET /tareas/{id} |
Funciona, con un defecto conocido |
POST /tareas |
Funciona, con un defecto conocido |
PUT /tareas/{id} |
No existe |
DELETE /tareas/{id} |
No existe |
Hoy se cierra la tabla.
Qué significa que el CRUD esté completo
Las operaciones comparten el mismo estado. Si creas un registro, tiene que aparecer en el listado y poder consultarse por su identificador; si lo modificas, las lecturas siguientes deben reflejar el cambio; si lo borras, tiene que desaparecer tanto del listado como del detalle. Cinco métodos que funcionan aislados no bastan si cada uno utiliza una colección diferente.
El identificador pertenece al servidor. El cliente envía los datos que desea crear y recibe el identificador asignado; no debe poder sustituir accidentalmente otro registro eligiendo su número. En una modificación, el identificador de la ruta selecciona el registro existente. Primero se busca y después se cambia: que no exista es un caso que la aplicación debe tratar.
Hoy la colección vive en el proceso Java. El reinicio elimina sus datos y es una limitación conocida de esta versión. No se corrige guardando el JSON en el navegador: la persistencia del servidor se incorporará con PostgreSQL. La prueba de hoy recorre crear, consultar, modificar, volver a consultar, borrar y comprobar la ausencia, en ese orden, sin editar el código entre peticiones.
Se trabaja
140 minutos · implementación guiada sobre el proyecto propio
Paso 1 · Retomar el proyecto y preparar la comprobación
- Abre el controlador y la clase del modelo utilizados en la sesión 3. Arranca y reproduce el alta y el listado con tu cliente HTTP.
- Localiza la lista que guarda los objetos y el método POST. Ahí revisarás la asignación de identificadores; conserva las rutas que ya funcionan.
- Prepara dos objetos con datos diferentes. Guarda las peticiones de alta para recrearlos después de cada reinicio, ya que todavía no hay base de datos.
Paso 2 · El primer defecto · el id lo pone el cliente
En TareaController.java, localiza el campo que contiene la lista y declara a su lado el contador de ids. Sustituye el método POST por el del bloque siguiente; conserva los GET existentes. Crea dos tareas sin escribir id en el JSON y consulta el listado. Deben recibir ids distintos. Borra una y crea otra: el contador no debe reutilizar el id borrado mientras siga activo ese proceso.
POST /tareascon"id": 1y el título que quieras.POST /tareasotra vez, también con"id": 1y otro título.GET /tareas/1.
Tienes dos tareas distintas con el mismo identificador, y la consulta solo encuentra una: la primera que aparece en la lista. La otra existe y es inalcanzable.
Quién decide el identificador
El identificador de un recurso lo asigna siempre el servidor, nunca quien lo crea. El cliente no puede saber qué ids están libres, no puede coordinarse con los demás clientes, y no tiene ningún motivo para que le importe.
Lo que envía el cliente es el contenido de la tarea. Lo que devuelve el servidor es la tarea ya creada, con su id puesto. Por eso un POST devuelve el objeto: es la única forma que tiene quien llama de enterarse del identificador.
La solución, en dos líneas:
private final List<Tarea> tareas = new ArrayList<>();
private int siguienteId = 1;
@PostMapping
public Tarea crear(@RequestBody Tarea tarea) {
tarea.setId(siguienteId);
siguienteId = siguienteId + 1;
tareas.add(tarea);
return tarea;
}
Ahora el id que llegue en el JSON se descarta: se sobrescribe antes de guardar. Compruébalo enviando "id": 999 y viendo qué te devuelve.
Paso 3 · PUT · sustituir una tarea entera
Añade el PUT dentro del controlador y los imports de PutMapping y RequestBody si faltan. Para probarlo, crea primero una tarea y copia el id devuelto: sustituye por ese número el {id} de la URL. Envía los campos editables completos, ejecuta el PUT y después un GET de detalle. La comprobación consiste en observar el nuevo contenido conservando la identidad del registro.
@PutMapping("/{id}")
public Tarea actualizar(
@PathVariable(name = "id") int id,
@RequestBody Tarea datos) {
for (int i = 0; i < tareas.size(); i++) {
if (tareas.get(i).getId() == id) {
datos.setId(id);
tareas.set(i, datos);
return datos;
}
}
return null;
}
- Por qué el id va en la ruta y no en el cuerpo
- Porque identifica qué tarea se sustituye. Es la regla de la sesión 2: sin ese dato la petición no significa nada. Lo que va en el cuerpo es el contenido nuevo.
- Por qué
datos.setId(id) - Para que mande la ruta. Si el cuerpo trae un id distinto —o ninguno— y no lo forzamos, la tarea se guardaría con un identificador equivocado y desaparecería de las consultas. Cuando dos sitios dicen lo mismo, hay que decidir cuál gana y dejarlo escrito.
- Por qué
sety no modificar campo a campo - Porque
PUTsignifica «sustituye el recurso por este». Si el cuerpo no trae prioridad, la tarea se queda sin prioridad, y eso es correcto. Cambiar solo algunos campos esPATCH, que es otra operación distinta y no la haremos hasta la UD2. - Qué pasa si el id no existe
- Devuelve
null, y por tanto un200con el cuerpo vacío. Es el mismo defecto que ya anotaste enGET /tareas/{id}. Sigue anotado.
Paso 4 · Completar consulta individual, listado y borrado
Los bloques siguientes actualizan métodos del controlador que ya abriste: reemplaza las versiones anteriores de listado y detalle si coinciden sus rutas, y añade DELETE si falta. Conserva el contador y la lista como campos de clase. No pegues un GET nuevo con la misma ruta junto al anterior. Guarda, reinicia y reconstruye los datos con POST antes de ejecutar el recorrido de aceptación.
| Método y ruta | Recibe | Devuelve |
|---|---|---|
GET /tareas |
Nada | El array de todas las tareas |
GET /tareas?completada=true |
Filtro opcional | Solo las que coincidan |
GET /tareas/{id} |
El id en la ruta | Esa tarea como objeto |
POST /tareas |
La tarea en el cuerpo | La tarea creada, con su id asignado |
PUT /tareas/{id} |
El id en la ruta y la tarea en el cuerpo | La tarea sustituida |
DELETE /tareas/{id} |
El id en la ruta | Nada |
Requisitos que se comprueban:
- El identificador lo asigna el servidor y nunca se repite, ni siquiera después de borrar una tarea.
- El filtro
completadaes opcional. Sin él, salen todas. DELETEno devuelve cuerpo. Declara el método comovoidy comprueba en Postman qué código de estado sale.- Todas las rutas cuelgan de un único
@RequestMappinga nivel de clase. - No se escribe ninguna ruta con un verbo dentro, del tipo
/tareas/borrar/3. La acción la expresa el método HTTP.
Estoy atascado · el filtro opcional
Ya lo has hecho en la sesión 2, pero con un String. Aquí el parámetro es un Boolean con required = false: si no llega, vale null, y entonces devuelves la lista entera.
Usa el envoltorio Boolean y no el tipo primitivo boolean. Un boolean no puede valer null, así que no podrías distinguir «no me han filtrado» de «me han pedido las no completadas».
Estoy atascado · el borrado
Sobre una List tienes removeIf, que recibe la condición y devuelve true si ha borrado algo. Una línea.
Si prefieres el bucle, recuerda no borrar de una lista mientras la recorres con un for normal: es la forma clásica de saltarte elementos.
Paso 5 · La prueba de aceptación
Una API no está terminada porque compile. Está terminada cuando una secuencia de peticiones se comporta como se esperaba.
Ejecuta esto en Postman, en orden, y anota el código de estado y el cuerpo de cada paso. Predice cada respuesta antes de pulsar Send.
| # | Petición | Qué debe ocurrir |
|---|---|---|
| 1 | GET /tareas |
[] |
| 2 | POST /tareas · «Revisar el login», alta |
Devuelve la tarea con id 1 |
| 3 | POST /tareas · «Actualizar dependencias», baja |
Devuelve la tarea con id 2 |
| 4 | GET /tareas |
Array con las dos |
| 5 | GET /tareas/2 |
Solo la segunda, como objeto |
| 6 | PUT /tareas/2 · misma tarea con completada: true |
La devuelve modificada |
| 7 | GET /tareas?completada=true |
Solo la tarea 2 |
| 8 | DELETE /tareas/1 |
Sin cuerpo |
| 9 | GET /tareas |
Solo queda la tarea 2 |
| 10 | POST /tareas · una tercera |
Su id no es 1 |
El paso 10 es el que suspende a más gente. Si tu contador vuelve a repartir el 1, es que lo estás calculando a partir del tamaño de la lista en lugar de llevar la cuenta de cuántas has creado.
Guarda estas diez peticiones
No las borres al terminar. En la UD2 aprenderás a agruparlas en una colección, ponerles nombre, sacar la dirección del servidor a una variable y ejecutarlas todas de una vez. Esta lista de diez pasos es el primer borrador de esa colección, y es también la primera versión de lo que en la UD10 serán tests automáticos.
Paso 6 · Lo que tu API todavía hace mal
Este apartado no constituye un ejercicio de autocrítica, sino el índice de las cuatro unidades siguientes. Comprueba tú mismo cada punto y anota qué responde.
| Prueba esto | Lo que pasa | Lo correcto | Dónde se arregla |
|---|---|---|---|
GET /tareas/999 |
200 con cuerpo vacío |
404 Not Found |
UD2 |
POST con {} |
Crea una tarea sin título | 400 explicando qué falta |
UD3 |
POST correcto |
Responde 200 |
201 Created |
UD2 |
POST con "completada": "quizás" |
400 sin explicación útil |
Un error legible | UD3 |
| Reiniciar la aplicación | Se pierde todo | Los datos siguen ahí | UD5 |
| Un campo interno del modelo | Se publica sin querer | Solo se publica lo que decidas | UD2, con DTO |
Que sepas enumerar estos seis defectos vale tanto como haber hecho funcionar la API. Saber qué le falta a lo que has construido es la parte difícil de este oficio.
Paso 7 · Preparar la evidencia de esta versión
Sube a tu repositorio del módulo:
- El proyecto completo, arrancable con
mvnw spring-boot:run. - Un archivo la tabla de comprobaciones con la tabla de las diez peticiones y el resultado real de cada una.
- Al final de ese archivo, tres apartados breves:
- Decisiones. Por qué el id lo pone el servidor y por qué el filtro va en la query string.
- Defectos conocidos. Los seis de la tabla anterior, con tus palabras.
- Una pregunta. Algo que hayas hecho funcionar sin acabar de entender del todo por qué.
Ese tercer apartado no resta nota. Se lee en la primera sesión de la UD2.
Proyecto, escrita sin volver a mirar la de tareas.Ver respuestas
1 · Porque el cliente no sabe qué ids están ocupados ni puede coordinarse con los demás clientes. Dos peticiones simultáneas elegirían el mismo y una de las dos tareas quedaría inalcanzable.
2 · PUT sustituye el recurso entero por lo que envías, así que lo que no mandas se pierde. PATCH modifica solo los campos que envías.
3 · En cuanto borras algo. Si creas dos tareas, borras la primera y creas otra, el tamaño vuelve a ser 1 y repartes un id que ya existe.
4 · Porque utiliza una consulta GET para cambiar datos. Los clientes pueden repetir o precargar consultas suponiendo que no modifican el estado; reserva las escrituras para los métodos HTTP correspondientes.
Paso 8 · Comprobar y registrar el resultado del proyecto
- Ejecuta en orden crear, listar, consultar por id, modificar, consultar de nuevo y borrar. Comprueba los datos después de cada escritura, no solo el estado HTTP.
- Crea dos registros sin elegir sus ids: el servidor debe asignar valores diferentes. Consulta un id ausente y anota la limitación que aún tenga la respuesta; los estados se ajustarán en la UD2.
Cierre
15 minutos · resultado comprobable y explicación individual
Al terminar la sesión:
El CRUD funciona desde la colección HTTP y el README declara que esta primera versión pierde datos al reiniciar.
Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.