Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado enlaces rotos y formato. En Servidor continúas la implementación del mismo producto.
Se explica
25 minutos · explicación y demostración
Tus operaciones ya tienen un comportamiento definido. Ahora controlarás también su respuesta HTTP y las convertirás en una comprobación repetible. ResponseEntity permite elegir estado, cabeceras y cuerpo; una colección agrupa peticiones y un entorno guarda valores como la dirección del servidor.
Llevas tres unidades mintiendo
Es fuerte dicho así, pero es literalmente lo que hace tu API ahora mismo:
| Lo que ocurre de verdad | Lo que tu API responde | Lo que le está diciendo al cliente |
|---|---|---|
| La tarea 999 no existe | 200 con cuerpo vacío |
«Aquí tienes lo que pediste» |
| Se ha creado una tarea nueva | 200 |
«Todo bien», sin decir que hay algo nuevo |
| Se ha borrado una tarea | 200 con cuerpo vacío |
«Aquí tienes lo que pediste» |
| Han pedido modificar algo inexistente | 200 con cuerpo vacío |
«Hecho» |
Ninguna es un fallo técnico: la aplicación no se rompe. Son fallos de comunicación, y son peores, porque el cliente construye su lógica encima. Una aplicación que recibe 200 da por hecho que la operación salió bien y sigue adelante.
El código de estado no es decoración. Es la parte de la respuesta que se lee primero y sobre la que se decide.
El problema que ya tienes y todavía no te ha estallado
Llevas tres semanas escribiendo peticiones a mano. Cada sesión reescribes las mismas URLs, vuelves a pegar los mismos cuerpos JSON y vuelves a mirar los mismos códigos.
Eso tiene tres consecuencias, y ninguna es cómoda:
- Repites trabajo cada día, y con las prisas escribes mal el cuerpo y depuras un fallo que no existe
- Cuando cambias algo, compruebas solo lo que has tocado
- Lo que se rompe es lo que no has tocado, y nadie lo mira
Regresión
Algo que funcionaba y ha dejado de funcionar por culpa de un cambio en otro sitio. Es el tipo de fallo más caro que existe, porque nadie lo está buscando: se descubre tarde, y normalmente lo descubre otra persona.
Hoy le ponemos remedio con la herramienta que ya tienes instalada.
Tres conceptos y ya podemos empezar
Colección
Un conjunto de peticiones guardadas, con nombre, organizadas en carpetas y en un orden. No constituye un repositorio desordenado, sino un guion ejecutable de principio a fin.
Variable
Un valor con nombre que se escribe una vez y se usa en muchas peticiones. Se escribe entre llaves dobles: {{baseUrl}}.
Entorno
Un juego de valores para esas variables. El entorno «local» dice que baseUrl es http://localhost:8080; mañana, el entorno «producción» dirá otra cosa. La misma colección, ejecutada contra sitios distintos, sin tocar ni una petición.
Se trabaja
140 minutos · implementación guiada sobre el proyecto propio
Paso 1 · Retomar el proyecto y preparar la comprobación
- Abre los controladores y las peticiones que has utilizado hasta ahora. Ejecuta una secuencia de alta, consulta y borrado para conocer la respuesta actual.
- Anota por operación el estado que quieres devolver. Todavía no crees los scripts de la colección: primero ajustarás esas respuestas en los controladores.
- Localiza la carpeta donde guardarás la colección exportada o los archivos de Bruno. Debe estar dentro del repositorio para que otra persona pueda repetirla.
Paso 2 · ResponseEntity · la respuesta entera, en tus manos
Importa org.springframework.http.ResponseEntity y sustituye el GET de detalle anterior. Cambian tanto el tipo de retorno del método como todas sus ramas: el caso encontrado devuelve ok(...) y el ausente notFound().build(). No dejes un return null de la versión anterior. Comprueba ambos casos antes de aplicar el mismo patrón a PUT y PATCH.
Cuando devuelves un ResponseEntity, decides tú las tres cosas.
@GetMapping("/{id}")
public ResponseEntity<Tarea> detalle(@PathVariable(name = "id") int id) {
for (Tarea tarea : tareas) {
if (tarea.getId() == id) {
return ResponseEntity.ok(tarea);
}
}
return ResponseEntity.notFound().build();
}
- El tipo devuelto
ResponseEntity<Tarea>significa «una respuesta HTTP completa cuyo cuerpo, si lo hay, es una tarea». El objeto deja de ser la respuesta para pasar a ser una parte de ella.ResponseEntity.ok(tarea)- Código 200 y la tarea como cuerpo. Es exactamente lo que hacía Spring solo, escrito a mano.
ResponseEntity.notFound().build()- Código 404 y sin cuerpo.
build()cierra la construcción cuando no hay nada que poner dentro. - Por qué
build()y nobody(null) - Porque expresa la intención: no es que el cuerpo esté vacío por accidente, es que esta respuesta no lleva cuerpo.
Los constructores que vas a usar
| Escribes | Responde |
|---|---|
ResponseEntity.ok(objeto) |
200 con cuerpo |
ResponseEntity.status(HttpStatus.CREATED).body(objeto) |
201 con cuerpo |
ResponseEntity.created(uri).body(objeto) |
201 con cuerpo y cabecera Location |
ResponseEntity.noContent().build() |
204 sin cuerpo |
ResponseEntity.notFound().build() |
404 sin cuerpo |
ResponseEntity.badRequest().body(algo) |
400 con cuerpo |
Todos siguen el mismo patrón: primero el estado, después las cabeceras si hacen falta, y al final body(...) o build().
Paso 3 · Un recurso que no existe
Ya está hecha: es el ejemplo de arriba. Aplícala también a PUT y a PATCH, que tienen el mismo problema.
@PutMapping("/{id}")
public ResponseEntity<Tarea> reemplazar(
@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 ResponseEntity.ok(datos);
}
}
return ResponseEntity.notFound().build();
}
Comprueba en Postman que PUT /tareas/999 ahora responde 404 y no 200.
Paso 4 · Crear devuelve 201, y dice dónde
Una creación correcta responde 201 Created. Existe una segunda parte que suele omitirse:
Cabecera Location
En una respuesta 201, dice en qué URL vive el recurso que se acaba de crear. Sin ella, el cliente tiene el objeto pero no sabe a dónde volver para consultarlo o modificarlo.
@PostMapping
public ResponseEntity<Tarea> crear(@RequestBody Tarea tarea) {
tarea.setId(siguienteId);
siguienteId = siguienteId + 1;
tareas.add(tarea);
URI ubicacion = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(tarea.getId())
.toUri();
return ResponseEntity.created(ubicacion).body(tarea);
}
Necesitarás dos importaciones: java.net.URI y org.springframework.web.servlet.support.ServletUriComponentsBuilder.
Ese constructor toma la URL de la petición actual —http://localhost:8080/tareas— y le añade el id, quedando http://localhost:8080/tareas/4. Se construye así, y no concatenando texto a mano, porque el servidor no siempre está en localhost:8080: en producción tendrá otro dominio, y esto se adapta solo.
Crea una tarea y mira la pestaña Headers de la respuesta. Ahí está Location. Copia esa URL, pégala en una petición nueva con GET y envíala: te devuelve la tarea que acabas de crear.
Eso es una API que se explica sola. El cliente no ha tenido que construir ninguna URL: se la has dado tú.
Paso 5 · Borrar devuelve 204
@DeleteMapping("/{id}")
public ResponseEntity<Void> eliminar(@PathVariable(name = "id") int id) {
tareas.removeIf(tarea -> tarea.getId() == id);
return ResponseEntity.noContent().build();
}
204 No Content
«Ha ido bien y no tengo nada que devolverte.» No constituye un error ni una respuesta vacía por omisión, sino la forma correcta de responder a una operación que no produce contenido.
Fíjate en ResponseEntity<Void>: el tipo declara que esta respuesta nunca lleva cuerpo. Es documentación que además comprueba el compilador.
Y la decisión que dejamos pendiente ayer
En la sesión 6 quedó abierto si un DELETE sobre algo que ya no existe debe dar 204 o 404. Ahora ya puedes implementar las dos.
removeIf devuelve un boolean: true si borró algo. Con eso puedes elegir. Elige una, impleméntala y escribe en un comentario por qué. Lo que no vale es que salga una u otra sin haberlo decidido.
Paso 6 · La alternativa ligera · @ResponseStatus
Este bloque muestra una alternativa de anotación, no otra forma de crear registros que deba quedar publicada. Léelo comparándolo con el POST que ya funciona. Si lo ejecutas para observar @ResponseStatus, conserva la asignación de ids y elimina después /rapida: el contrato del proyecto debe mantener un único procedimiento de alta comprobado.
@PostMapping("/rapida")
@ResponseStatus(HttpStatus.CREATED)
public Tarea crearRapida(@RequestBody Tarea tarea) {
tareas.add(tarea);
return tarea;
}
@ResponseStatus
Un solo código posible, siempre el mismo. Más corto y más legible. No permite cabeceras ni respuestas alternativas.
ResponseEntity
El método puede responder cosas distintas según lo que ocurra, y puede añadir cabeceras. Es lo que necesitas en cuanto hay un caso de «no encontrado».
Regla práctica: si el método puede terminar de más de una manera, ResponseEntity. Si no, @ResponseStatus y menos ruido.
Paso 7 · La tabla del contrato
Esta tabla es el objetivo de la sesión. Tu API debe cumplirla entera:
| Operación | Caso | Código | ¿Cuerpo? |
|---|---|---|---|
GET /tareas |
Siempre | 200 |
El array, aunque esté vacío |
GET /tareas/{id} |
Existe | 200 |
La tarea |
GET /tareas/{id} |
No existe | 404 |
No |
POST /tareas |
Correcto | 201 |
La tarea creada, con Location |
PUT /tareas/{id} |
Existe | 200 |
La tarea sustituida |
PUT /tareas/{id} |
No existe | 404 |
No |
PATCH /tareas/{id} |
Existe | 200 |
La tarea modificada |
PATCH /tareas/{id} |
No existe | 404 |
No |
DELETE /tareas/{id} |
Existe | 204 |
No |
DELETE /tareas/{id} |
No existía | 204 o 404, tu decisión |
No |
Fíjate en la primera fila: una colección vacía no es un 404. La ruta /tareas existe y la respuesta correcta es un array vacío con 200. El 404 es para un recurso concreto que no está, no para una búsqueda sin resultados.
Paso 8 · El contrato de proyectos
- Aplica la tabla completa al
ProyectoController. - Añade la cabecera
Locationa su creación. - Comprueba las diez filas en Postman y anota el código real de cada una.
- Provoca un caso que no esté en la tabla —por ejemplo, un
PUTcon un id en el cuerpo distinto al de la ruta— y decide qué debería responder. Impleméntalo y justifícalo en un comentario.
Colecciones, variables y entornos
Paso 9 · Crea la colección
En Postman, New → Collection. Llámala Gestor de incidencias.
Dentro, crea dos carpetas: Tareas y Proyectos.
Ahora ve guardando en ellas las peticiones que ya usas. Cada vez que tengas una petición que funciona, Save y elige la carpeta.
Los nombres importan más de lo que parece
No llames a una petición «POST 1». Llámala «Crear tarea válida», «Crear tarea sin título», «Obtener tarea inexistente».
El nombre tiene que decir qué caso comprueba, porque dentro de un mes la colección la va a ejecutar alguien —tú incluido— que no recuerde qué hacía la número 7. Una colección bien nombrada es la primera documentación de tu API.
Paso 10 · Saca la dirección a una variable
Ahora mismo todas tus peticiones empiezan por http://localhost:8080. Si mañana cambias el puerto, las reescribes todas.
Environments → Create Environment, llámalo Local.- Añade una variable: nombre
baseUrl, valorhttp://localhost:8080. - Guarda y selecciónalo en el desplegable de arriba a la derecha. Es el paso que se olvida: un entorno creado pero no seleccionado no hace nada.
- En cada petición, sustituye el principio de la URL:
{{baseUrl}}/tareas/1
Pasa el ratón por encima de {{baseUrl}}: Postman te enseña el valor que va a usar. Si aparece en rojo o dice unresolved, es que no has seleccionado el entorno.
Cambia ahora el valor de la variable a http://localhost:8081, cambia el puerto de la aplicación en application.properties, y comprueba que toda la colección sigue funcionando sin haber tocado ni una petición. Después devuélvelo todo al 8080.
Paso 11 · Encadena peticiones
Selecciona la petición POST de creación, abre Scripts → Post-response y coloca allí el código que guarda el id; no va en el cuerpo JSON ni en Pre-request. Envía el POST y comprueba la variable tareaId en las variables de la colección. Después cambia las URLs de detalle, edición y borrado a {{tareaId}}. Ejecuta primero el alta: una consulta no puede utilizar una variable que aún no has creado.
La solución es guardar el id de la respuesta en una variable. En Postman, en la petición de creación, pestaña Scripts (o Tests, según la versión):
pm.collectionVariables.set("tareaId", pm.response.json().id);
Una línea. Se lee así: «del JSON de la respuesta, coge el campo id y guárdalo en la variable tareaId».
A partir de ahí, en las peticiones siguientes:
{{baseUrl}}/tareas/{{tareaId}}
Por qué esto lo cambia todo
Con las peticiones encadenadas, tu colección deja de ser una lista de cosas sueltas y pasa a ser un escenario completo: crear, consultar lo creado, modificarlo, borrarlo y comprobar que ya no está.
Ese escenario se ejecuta completo con una sola acción y sin intervención humana. Eso es exactamente lo que hace un test automático, que es a donde vamos en la UD4.
Paso 12 · Comprobaciones automáticas
Añade las aserciones de creación a la misma sección Post-response del POST, después de guardar el id. En las demás peticiones ajusta la aserción al resultado de esa petición: el GET espera 200, el DELETE 204 y el GET posterior al borrado 404. No copies la comprobación de JSON al DELETE, porque su cuerpo está vacío. Guarda cada petición antes de ejecutar la colección.
pm.test("Responde 201", function () {
pm.response.to.have.status(201);
});
pm.test("Devuelve un id asignado", function () {
pm.expect(pm.response.json().id).to.be.above(0);
});
No hace falta que sepas JavaScript: el patrón es siempre el mismo, un nombre y una comprobación. Copia, cambia el número y cambia el campo.
Al enviar la petición, abajo aparece la pestaña Test Results con una línea verde por cada comprobación superada y roja por cada una fallida.
Qué comprobar y qué no
Comprueba el contrato: el código de estado, que exista un campo, que un valor sea el que enviaste. Eso es lo que has prometido y no debería cambiar.
No compruebes cosas que van a cambiar solas: que el id sea exactamente 3, que la lista tenga exactamente cinco elementos, la fecha de creación. Una prueba que falla sin que nadie haya roto nada acaba ignorándose, y una prueba ignorada es peor que no tenerla.
Paso 13 · Ejecuta la colección entera
Botón derecho sobre la colección, Run collection. Se abre el ejecutor: elige el orden, pulsa Run y en unos segundos tienes el informe completo.
Verde entero significa que todo el contrato de tu API sigue en pie. En diez segundos, y sin haber escrito una URL a mano.
Si usas Bruno en lugar de Postman
Todo lo de hoy existe igual, con dos ventajas: no pide cuenta y guarda cada petición como un archivo .bru de texto dentro de tu propio proyecto, así que va al repositorio con el resto del código.
El encadenado se escribe así:
vars:post-response { tareaId: res.body.id }
Y las comprobaciones así:
assert { res.status: eq 201 }
Paso 14 · Guárdala en el repositorio
Una colección que solo existe en tu portátil no es evidencia de nada.
En Postman: botón derecho sobre la colección, Export, y guarda el .json en una carpeta pruebas/ dentro del proyecto. En Bruno ya está dentro.
A partir de ahora, la colección se entrega con el código. Forma parte del trabajo igual que el pom.xml.
Paso 15 · El escenario completo
Monta en la colección este escenario, en este orden, cada petición con sus comprobaciones:
| # | Petición | Comprueba |
|---|---|---|
| 1 | Listar tareas | 200 y que la respuesta sea un array |
| 2 | Crear tarea válida | 201, que haya Location, y guarda el id en {{tareaId}} |
| 3 | Obtener la tarea creada | 200 y que el título sea el que enviaste |
| 4 | Modificar con PATCH |
200 y que el campo cambiado sea el nuevo |
| 5 | Obtener tarea inexistente | 404 |
| 6 | Crear con cuerpo inválido | 400 |
| 7 | Crear con Content-Type incorrecto |
415 |
| 8 | Borrar la tarea creada | 204 |
| 9 | Obtener la tarea borrada | 404 |
Ejecútalo entero. Tiene que salir verde de arriba abajo.
Fíjate en que los pasos 3, 4, 8 y 9 usan {{tareaId}}: ninguno tiene un número escrito a mano. Por eso el escenario se puede ejecutar mil veces seguidas.
Paso 16 · Rompe el código a propósito
Esta es la comprobación de que la colección sirve para algo.
- Con la colección en verde, ve al código y rompe una cosa: cambia el
201de la creación por un200. - No toques la colección. Reinicia y ejecútala.
- Anota qué peticiones fallan y qué dice el informe.
- Repara el código y vuelve a ejecutarla.
Repítelo con otras dos averías a tu elección: por ejemplo, que el borrado no borre nada, o que la consulta por id devuelva siempre la primera tarea.
Escribe después una frase por avería: ¿cuánto habrías tardado en darte cuenta sin la colección?
Paso 17 · Comprobar y registrar el resultado del proyecto
- Ejecuta la colección completa desde un estado conocido. Debe crear su propio registro, recoger su id y usarlo en las peticiones siguientes, sin ids copiados manualmente.
- Comprueba 201 y Location al crear, 404 al consultar un recurso ausente y 204 sin cuerpo al borrar. Cambia únicamente la variable de dirección para repetir el recorrido en otro entorno disponible.
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 · Ocho situaciones y su código
Para cada una, escribe el código de estado que devolverías y una frase justificándolo. Algunas admiten más de una respuesta defendible; lo que se evalúa es el argumento.
- Se piden todas las tareas y no hay ninguna.
- Se piden las tareas de un proyecto que no existe.
- Se crea una tarea correctamente.
- Se crea una tarea sin título, y tu API todavía no valida.
- Se pide
/tareas/abc, con un id que no es un número. - Se borra una tarea que ya se había borrado hace un minuto.
- Se hace un
PUTsobre/tareas/5con{"id": 9, "titulo": "Algo"}. - Salta una excepción inesperada dentro de tu método.
Las dos difíciles son la 2 y la 7. En la 2, piensa qué es lo que no existe. En la 7, piensa quién manda, si la ruta o el cuerpo, y qué es peor: adivinar o rechazar.
Ver respuestas
1 · Porque la ruta de la colección existe y la consulta se ha resuelto correctamente: el resultado es que no hay elementos. El 404 dice que no existe el recurso que se pedía, no que una búsqueda no haya encontrado nada.
2 · La URL donde vive el recurso recién creado. Viaja en la respuesta 201 de una creación, y evita que el cliente tenga que construir esa URL por su cuenta.
3 · El 204 declara que no hay contenido y que eso es lo correcto. Un 200 con el cuerpo vacío dice «aquí tienes lo que pediste» y no entrega nada, que es justo lo que confunde a quien llama.
4 · Cuando el método solo puede terminar de una manera y siempre devuelve el mismo código. En cuanto haya un caso alternativo, como «no encontrado», hace falta ResponseEntity.
Reto · La colección de proyectos, sin guion
Construye tú solo el escenario equivalente para proyectos, con estas condiciones añadidas:
- Al menos doce peticiones, cubriendo los casos correctos y los de error.
- Ninguna URL con un identificador escrito a mano.
- Todas con al menos una comprobación automática.
- Ninguna comprobación frágil, de las que fallan sola sin que nadie rompa nada.
- El escenario termina dejando el servidor como estaba al empezar: lo que creas, lo borras.
La condición 5 es la difícil y es la más importante: una colección que ensucia los datos solo se puede ejecutar una vez. Explica en un comentario cómo la has resuelto.
baseUrl y las peticiones de tareas guardadas y nombradas.{{tareaId}}, en verde, y las tres averías detectadas.Ver respuestas
1 · Algo que funcionaba y ha dejado de funcionar por un cambio hecho en otro sitio. Es caro porque nadie lo está buscando: se descubre tarde y suele descubrirlo otra persona.
2 · La variable es el hueco con nombre que dejas en las peticiones; el entorno es el juego de valores que rellena esos huecos. Cambiando de entorno, la misma colección apunta a otro servidor.
3 · Porque ese valor cambia solo según lo que se haya creado antes, y la prueba fallaría sin que nadie hubiera roto nada. Una prueba que da falsas alarmas se acaba ignorando.
4 · Para poder ejecutarlo tantas veces como haga falta con el mismo resultado. Si deja datos, la segunda ejecución parte de una situación distinta y sus comprobaciones dejan de ser fiables.
Cierre
15 minutos · resultado comprobable y explicación individual
Al terminar la sesión:
Una ejecución limpia crea, consulta, modifica y borra sus propios datos sin depender de pruebas anteriores.
Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.