← De Java a la Web: HTTP y Spring Boot

Sesión 4 · Semana 2

Primera versión CRUD en memoria

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

  1. 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.
  2. 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.
  3. 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.

  1. POST /tareas con "id": 1 y el título que quieras.
  2. POST /tareas otra vez, también con "id": 1 y otro título.
  3. 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é set y no modificar campo a campo
Porque PUT significa «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 es PATCH, 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 un 200 con el cuerpo vacío. Es el mismo defecto que ya anotaste en GET /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:

  1. El identificador lo asigna el servidor y nunca se repite, ni siquiera después de borrar una tarea.
  2. El filtro completada es opcional. Sin él, salen todas.
  3. DELETE no devuelve cuerpo. Declara el método como void y comprueba en Postman qué código de estado sale.
  4. Todas las rutas cuelgan de un único @RequestMapping a nivel de clase.
  5. 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:

  1. El proyecto completo, arrancable con mvnw spring-boot:run.
  2. Un archivo la tabla de comprobaciones con la tabla de las diez peticiones y el resultado real de cada una.
  3. 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.

Objetivo mínimoLos cinco métodos funcionando y la secuencia de diez peticiones ejecutada entera.
Si lo tienesEl filtro opcional resuelto y el paso 10 correcto, con el contador independiente del tamaño de la lista.
RetoLa misma API completa sobre 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

  1. 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.
  2. 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.