← 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, listar y consultar objetos por su identificador en memoria. Hoy conservarás esas operaciones, incorporarás la sustitución y el borrado y añadirás un filtro al listado. 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.

En este proyecto, el servidor asignará el identificador. El cliente enviará los datos que desea crear y recibirá el identificador asignado; así evitamos que distintos clientes elijan el mismo 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

Continúa con el flujo del backend. Comprueba que la PR de la sesión 3 está revisada e integrada. Crea una issue para completar el CRUD en memoria, indicando las operaciones que comprobarás. En los comandos se usa el número 13: sustitúyelo por el número real de tu nueva issue.

En la carpeta del backend, con git status sin cambios pendientes:

git switch main
git pull --ff-only
git switch -c 13-crud-en-memoria

Realiza los cambios de hoy en esa rama. No vuelvas a enviar código directamente a main: se integra mediante una PR, como has practicado en Intermodular.

  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

Antes de modificar el POST, reinicia para partir de la lista vacía de la sesión 3. Reproduce primero el fallo de los identificadores elegidos por el cliente con estas peticiones:

  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.

Consulta también GET /tareas: hay dos tareas distintas con el mismo identificador, pero GET /tareas/1 solo encuentra la primera de la lista. La segunda aparece en el listado, aunque no puedes seleccionarla de forma inequívoca por su id.

Quién decide el identificador

En nuestra API, el servidor asigna el identificador. El cliente envía el contenido de la tarea y el servidor lleva un contador para evitar identificadores repetidos en las pruebas secuenciales de esta versión. Esta es una decisión de diseño del proyecto, no una obligación universal de HTTP.

Nuestro POST devuelve la tarea creada con su id. Lee ese valor en la respuesta para construir las siguientes consultas y modificaciones. Más adelante conocerás también la cabecera Location, que permite indicar la dirección del recurso creado.

Ahora corrige el POST. En TareaController, conserva la lista existente, añade a su lado private int siguienteId = 1; y sustituye únicamente el método crear por esta versión:

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;
}

La declaración de tareas aparece para situar el nuevo contador: no dupliques el atributo que ya existe. Cada alta asigna el valor actual, aumenta el contador y añade la tarea a la lista. En esta versión, el contador y la lista se reinician al arrancar la aplicación.

Guarda y reinicia para eliminar los registros duplicados. En Postman prepara POST http://localhost:8080/tareas, selecciona Body → raw → JSON y envía:

{
  "titulo": "Revisar el login",
  "prioridad": "alta",
  "completada": false
}

La respuesta debe contener esos campos y "id": 1. Cambia el título por Actualizar dependencias, la prioridad por baja y envía de nuevo: debe recibir "id": 2. Consulta el listado para ver ambas tareas. Como tercera prueba, añade "id": 999 al cuerpo, separado del resto de campos por una coma: el servidor debe devolver "id": 3. La prueba de borrar y crear llegará después de implementar DELETE.

Paso 3 · PUT · sustituir una tarea entera

Añade el método siguiente dentro de TareaController, antes de su última llave. Conserva los GET y el POST anteriores. Añade import org.springframework.web.bind.annotation.PutMapping; junto a los demás imports; RequestBody y PathVariable ya se utilizan en el controlador.

@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.
Qué diferencia hay entre i e id
i es la posición dentro de la lista y empieza en 0; id identifica la tarea y procede de la URL. Una lista con tareas de ids 2 y 3 tiene posiciones 0 y 1. tareas.get(i) obtiene el objeto de esa posición y getId() permite comprobar si es el que buscamos.
Qué cambia tareas.set(i, datos)
Sustituye el objeto de la posición i por el objeto recibido. No añade otra tarea ni cambia el tamaño de la lista. datos.setId(id), en cambio, modifica un campo de ese objeto antes de guardarlo. Son dos operaciones distintas.
Qué ocurre con un campo omitido
Este PUT sustituye todos los datos editables. En nuestro modelo, una prioridad omitida queda en null y una completada omitida queda en false; no se recuperan los valores anteriores. Aún falta validar los campos obligatorios, algo que se abordará en la UD3. PATCH permite modificaciones parciales y se introducirá en 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.

Primera modificación completa desde Postman

  1. Guarda el controlador, reinicia y espera a que arranque. El reinicio ha vaciado la lista: envía primero el POST sin id del paso 2. Debe crear la tarea 1. Un PUT a una lista vacía no puede modificarla.
  2. Abre otra pestaña HTTP en Postman. Selecciona PUT y escribe http://localhost:8080/tareas/1. Si estás utilizando otro registro, sustituye 1 por el id real de su respuesta POST; no escribas literalmente {id}.
  3. Selecciona Body → raw → JSON y escribe todos los campos editables:
{
  "titulo": "Revisar el login y cerrar la incidencia",
  "prioridad": "baja",
  "completada": true
}
  1. Pulsa Send. Debes recibir 200 OK y este objeto:
{
  "id": 1,
  "titulo": "Revisar el login y cerrar la incidencia",
  "prioridad": "baja",
  "completada": true
}
  1. En la pestaña de consultas, envía GET a http://localhost:8080/tareas/1, con Body → none. Debe devolver los datos modificados. Consulta después http://localhost:8080/tareas: debe seguir habiendo una sola tarea con el mismo id.

Comprueba ahora una omisión deliberada: en el cuerpo del PUT elimina la línea de prioridad, manteniendo una coma entre titulo y completada. Envía el PUT y consulta el detalle: la prioridad debe ser null, porque has sustituido el objeto por otro que no la incluye. Restaura el JSON completo anterior y vuelve a enviarlo antes de continuar. Cambiar el texto en Postman sin pulsar Send no modifica los datos del servidor.

Paso 4 · Completar consulta individual, listado y borrado

Conserva el GET de detalle de la sesión 3 y el POST y PUT que acabas de comprobar. Sustituye el GET de listado por el siguiente e importa RequestParam. El parámetro Boolean puede valer null, lo que permite distinguir «sin filtro» de false:

@GetMapping
public List<Tarea> lista(
        @RequestParam(name = "completada", required = false) Boolean completada) {
    if (completada == null) {
        return tareas;
    }
    List<Tarea> resultado = new ArrayList<>();
    for (Tarea tarea : tareas) {
        if (tarea.isCompletada() == completada) {
            resultado.add(tarea);
        }
    }
    return resultado;
}

La lista local resultado recoge las coincidencias para esta respuesta. Añadir una tarea a resultado no la elimina de tareas: al quitar el filtro volverás a ver todos los registros. Sustituye el método de listado anterior; no dejes dos métodos con @GetMapping sin ruta.

Añade DELETE dentro del mismo controlador e importa org.springframework.web.bind.annotation.DeleteMapping:

@DeleteMapping("/{id}")
public void eliminar(@PathVariable(name = "id") int id) {
    tareas.removeIf(tarea -> tarea.getId() == id);
}

removeIf recorre la lista y elimina los elementos para los que la condición resulta verdadera. La expresión tarea -> tarea.getId() == id es una lambda: a la izquierda de -> se da un nombre al elemento que se está examinando; a la derecha se indica la comprobación. Se lee «para cada tarea, comprueba si su identificador coincide con el de la ruta». No usa la posición de la lista.

Guarda y reinicia una vez. Prepara los datos y comprueba el filtro y el borrado sin editar código entre peticiones:

  1. Envía dos POST sin id: uno con completada: false y otro con completada: true, utilizando títulos distintos. Deben recibir ids 1 y 2.
  2. Consulta GET http://localhost:8080/tareas: deben aparecer ambas. Consulta http://localhost:8080/tareas?completada=true: solo debe aparecer la segunda. Cambia true por false: solo debe aparecer la primera. Quita ?completada=false: deben volver a aparecer ambas.
  3. Abre otra pestaña HTTP, selecciona DELETE, escribe http://localhost:8080/tareas/1 y deja Body → none. El id de la URL basta para seleccionar la tarea; no se necesita JSON. Pulsa Send.
  4. Debes recibir 200 OK con el cuerpo vacío: el método Java devuelve void. En la UD2 ajustarás el estado a 204 No Content. El cuerpo vacío por sí solo no demuestra que se haya borrado el registro.
  5. Envía GET http://localhost:8080/tareas: solo debe quedar la tarea 2. GET http://localhost:8080/tareas/1 devuelve 200 y cuerpo vacío, la limitación conocida de la consulta de un id inexistente. GET http://localhost:8080/tareas/2 debe seguir devolviendo la segunda tarea.
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 no se repite durante esta ejecución, ni después de borrar una tarea. Tras reiniciar, la lista y el contador comienzan de nuevo.
  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

removeIf modifica la lista y devuelve un booleano que indica si ha eliminado algún elemento. En este método no utilizamos ese resultado: void significa que eliminar() no devuelve un objeto para el cuerpo HTTP.

Si no desaparece la tarea, compara el id de la URL con el que figura en el listado. No deduzcas el identificador a partir de su posición: la primera tarea de la lista puede tener id 2 después de borrar la de id 1.

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.

Para este escenario concreto, reinicia una vez y comprueba que el listado está vacío. Ejecuta las diez peticiones sin reiniciar entre ellas. Así los ids 1 y 2 corresponden a las altas indicadas. Anota estado y cuerpo y predice cada resultado antes de enviar. En los dos primeros POST utiliza el JSON completo del paso 2, con los títulos y prioridades de la tabla y completada: false, sin id. En el PUT conserva el título y la prioridad de la tarea 2 y cambia solo el valor de completada a true, enviando también los demás campos.

# 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 Recibe id 3: no reutiliza el 1 borrado ni el 2 que sigue existiendo

Después del paso 10, consulta el listado: debe contener dos registros con ids distintos, 2 y 3. Si calculas el nuevo id con tareas.size() + 1, obtendrás 2 y duplicarás el registro existente. La prueba debe detectar también ese caso, no solo que se reutilice el 1.

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 UD7 serán tests automatizados de Java.

Paso 6 · Completar la segunda entidad del mismo proyecto

Retoma Proyecto.java y ProyectoController.java de la sesión 3. Ya deben permitir crear, listar y consultar proyectos por id en memoria. Aplica ahora las mismas operaciones sobre esa entidad, manteniendo el CRUD de tareas. Si tu dominio utiliza otros nombres, conserva los que ya has elegido.

  1. En ProyectoController, localiza la lista de proyectos y añade a su lado su propio private int siguienteId = 1;. Modifica su POST para asignar el id, incrementar el contador y añadir el proyecto. Cada controlador mantiene su propia lista y contador; una tarea y un proyecto pueden tener ambos id 1 porque sus rutas son diferentes.
  2. Añade un PUT con @PutMapping("/{id}"), un id de ruta y @RequestBody Proyecto datos. Adapta el bucle del paso 3 para recorrer proyectos: busca el id, asigna ese id a datos, sustituye el objeto con proyectos.set(i, datos) y devuélvelo. Si no existe, conserva por ahora el return null.
  3. Sustituye el GET de listado por un filtro opcional adecuado al modelo. Para Proyecto, utiliza @RequestParam(name = "activo", required = false) Boolean activo. Sin filtro devuelve proyectos; con filtro recorre la lista y añade a resultado los proyectos cuyo proyecto.isActivo() == activo. El resultado debe ser una List<Proyecto>. Si conservabas el filtro demostrativo estado de la sesión 2, sustitúyelo por activo, que corresponde a un campo real de este modelo.
  4. Añade DELETE con el id de ruta y proyectos.removeIf(proyecto -> proyecto.getId() == id);. Conserva el GET de detalle que busca el proyecto en la lista. Revisa los imports de las anotaciones que acabas de añadir y los tipos: en este controlador no debe quedar ningún Tarea ni una referencia a la lista tareas.
  5. Guarda y reinicia. En Postman crea un proyecto con POST http://localhost:8080/proyectos y Body → raw → JSON:
{
  "nombre": "Gestor de tareas",
  "descripcion": "Organizar las tareas del equipo",
  "activo": true,
  "numeroDeIncidencias": 0
}

Debe recibir id 1. Crea otro con nombre Archivo de proyectos, descripción Consultar proyectos terminados, activo: false y numeroDeIncidencias: 0; debe recibir id 2. Comprueba, en este orden:

Petición Resultado esperado
GET http://localhost:8080/proyectos Los dos proyectos
GET http://localhost:8080/proyectos/1 Solo el primero
GET http://localhost:8080/proyectos?activo=true Solo el primero
GET http://localhost:8080/proyectos?activo=false Solo el segundo

Envía PUT a http://localhost:8080/proyectos/1, con Body → raw → JSON y este cuerpo completo:

{
  "nombre": "Gestor de tareas revisado",
  "descripcion": "Organizar y revisar las tareas del equipo",
  "activo": false,
  "numeroDeIncidencias": 1
}

Consulta el detalle: debe conservar id 1 y mostrar los cuatro valores nuevos. El listado con activo=true debe devolver []; al quitar el filtro vuelven a aparecer ambos proyectos. Envía DELETE a http://localhost:8080/proyectos/1, con Body → none, y comprueba que el listado solo contiene el segundo. Crea un tercer proyecto: debe recibir id 3. Todos estos envíos responden por ahora con 200; el DELETE tiene el cuerpo vacío.

Por último, envía un POST válido a /tareas y comprueba que sus rutas siguen funcionando. Las dos entidades pertenecen al mismo backend, pero todavía no están relacionadas entre sí: esa relación se incorporará más adelante. Antes de continuar debes poder crear, consultar, sustituir, filtrar y borrar en ambas.

Paso 7 · 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 UD3, 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 8 · Revisar y explicar esta versión

Comprueba que tu repositorio contiene el proyecto completo y que puedes explicar el resultado del trabajo:

  1. El proyecto completo, arrancable con mvnw spring-boot:run.
  2. El resultado real de las diez peticiones y las diferencias que hayas corregido.
  3. Una explicación breve de estas tres cuestiones:
    • 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é.

Conserva la pregunta pendiente para contrastarla con las explicaciones de la siguiente unidad.

Objetivo esencialLas dos entidades tienen su CRUD y filtro opcional comprobados. La secuencia de diez peticiones verifica también que borrar y crear no duplica identificadores.
Comprobación adicionalUn filtro sin coincidencias devuelve un array vacío; quitar el filtro recupera todos los registros existentes.
Trabajo sobre tu dominioComprueba que los campos y filtros de ambas entidades corresponden a tu producto y explica una secuencia real de uso. Ambas se utilizarán en la sesión 6.
Ver respuestas

1 · En este proyecto centralizamos la asignación para evitar que distintos clientes elijan el mismo número. El contador permite comprobarlo con peticiones secuenciales; esta versión en memoria todavía no resuelve el acceso concurrente.

2 · Nuestro PUT sustituye el objeto almacenado por el recibido. Los campos omitidos toman los valores iniciales del modelo, en lugar de conservar los anteriores. PATCH permitirá expresar una modificación parcial.

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 9 · 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.

Referencia · Controlador de tareas al terminar la sesión

Utiliza esta versión para contrastar el resultado de los pasos anteriores si encuentras un error de integración. Es el contenido completo de TareaController.java, no un fragmento para añadir al final del archivo. Mantén el paquete y los nombres de tu proyecto y conserva el modelo Tarea.java de la sesión 3, incluido el constructor vacío con @JsonCreator del paso 9. No sustituye las comprobaciones de Postman ni la adaptación del controlador de la segunda entidad.

package com.ejemplo.gestor.controller;

import com.ejemplo.gestor.model.Tarea;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/tareas")
public class TareaController {

    private final List<Tarea> tareas = new ArrayList<>();
    private int siguienteId = 1;

    @GetMapping
    public List<Tarea> lista(
            @RequestParam(name = "completada", required = false) Boolean completada) {
        if (completada == null) {
            return tareas;
        }
        List<Tarea> resultado = new ArrayList<>();
        for (Tarea tarea : tareas) {
            if (tarea.isCompletada() == completada) {
                resultado.add(tarea);
            }
        }
        return resultado;
    }

    @GetMapping("/{id}")
    public Tarea detalle(@PathVariable(name = "id") int id) {
        for (Tarea tarea : tareas) {
            if (tarea.getId() == id) {
                return tarea;
            }
        }
        return null;
    }

    @PostMapping
    public Tarea crear(@RequestBody Tarea tarea) {
        tarea.setId(siguienteId);
        siguienteId = siguienteId + 1;
        tareas.add(tarea);
        return tarea;
    }

    @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;
    }

    @DeleteMapping("/{id}")
    public void eliminar(@PathVariable(name = "id") int id) {
        tareas.removeIf(tarea -> tarea.getId() == id);
    }
}

Cierre

15 minutos · resultado comprobable y explicación individual

Al terminar la sesión:

El CRUD de ambas entidades funciona mediante las peticiones del cliente HTTP y el README declara que pierde los datos al reiniciar. La colección automatizada se construirá en la sesión 7; hoy deben quedar comprobados los efectos de cada operación.

Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.

Antes de cerrar, publica la rama y abre su PR. Utiliza el nombre de rama con el número real de la issue creada al inicio.

git status
git add .
git diff --cached
git commit -m "Completar CRUD en memoria"
git push -u origin 13-crud-en-memoria

Antes del commit, revisa que solo preparas los archivos del proyecto y que target/ queda excluido. En la PR hacia main, incluye Closes #13 con el número real y los pasos para comprobar crear, listar, consultar, modificar y borrar. Solicita la revisión sobre la rama, atiende las correcciones y fusiona cuando se hayan comprobado los criterios. Comprueba además que el README integrado declara la pérdida de datos al reiniciar. La revisión se realiza sobre el backend local; su puesta en producción llegará más adelante en Intermodular.