← De Java a la Web: HTTP y Spring Boot

Sesión 2 · Semana 1

Rutas y primeras consultas del proyecto

Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado del repositorio vacío a una url pública. En Servidor continúas la implementación del mismo producto.

Se explica

25 minutos · explicación y demostración

Ya tienes rutas que devuelven un texto fijo. Hoy el cliente enviará datos en la URL y un mismo método responderá según esos datos. Un parámetro es un valor de entrada del método; Spring lo obtiene de la ruta o de la consulta. Todavía devolveremos texto, sin buscar registros reales.

Recibir datos distintos en una misma ruta

El endpoint de ayer siempre responde lo mismo:

@GetMapping("/hola")
public String hola() {
    return "Hola, mundo. Te responde mi servidor.";
}

Si mantuviéramos una respuesta fija, no podríamos elegir a quién saludar ni qué incidencia consultar. Para reutilizar el método, el cliente enviará un dato dentro de la petición y el método lo utilizará al construir su respuesta.

Hoy vemos las dos formas de hacerlo con un GET.

Los dos sitios donde caben datos en una URL
  1. En la ruta: /usuarios/3 — el dato forma parte del camino
  2. En la query string: /usuarios?rol=admin — el dato va detrás de la interrogación

Se parecen, pero no significan lo mismo, y elegir mal es el origen de la mitad de las APIs incómodas de usar. Al final de la sesión tendrás una regla para decidir.

La query string, por dentro

Es lo que va después del ?. Son pares clave=valor separados por &:

/incidencias?estado=abierta&prioridad=alta&pagina=2
?              empieza la query string
estado=abierta primer par
&              separador
prioridad=alta segundo par
&              separador
pagina=2       tercer par

Tres cosas que conviene saber desde hoy:

  • El orden no importa: ?a=1&b=2 y ?b=2&a=1 son la misma petición.
  • Todo llega como texto. pagina=2 no constituye un número, sino la cadena "2". Que acabe siendo un int en tu método es trabajo de Spring, no del navegador.
  • Los caracteres raros se codifican. Un espacio viaja como %20 o como +, y una ñ como %C3%B1. Lo verás en el panel de red y no debe alarmarte.

La regla para decidir dónde va cada dato

Ruta o query string

En la ruta va lo que identifica al recurso. Sin ese dato, la petición no tiene sentido: /usuarios/3 pregunta por un usuario concreto, y /usuarios/ a secas ya es otra cosa distinta.

En la query string va lo que modifica una consulta. Filtros, orden, paginación, búsqueda. Si lo quitas, la petición sigue teniendo sentido: solo devuelve más resultados o en otro orden.

La prueba rápida: ¿puedo borrar este dato de la URL y que siga significando algo? Si sí, es query string. Si no, es ruta.

URL Correcto Por qué
/usuarios/3 El 3 identifica al usuario
/usuarios?id=3 Mejorable Un identificador no es un filtro
/incidencias?estado=abierta Es un filtro sobre una lista
/incidencias/abierta No Parece una incidencia llamada «abierta»
/proyectos/7/incidencias?prioridad=alta Identifica el proyecto y filtra sus incidencias

Esa última fila combina las dos ideas, y es la forma que tendrá casi toda tu API a partir de la UD3.

Se trabaja

140 minutos · implementación guiada sobre el proyecto propio

Paso 1 · Retomar el proyecto y preparar la comprobación

  1. Abre HolaController.java y arranca como en la sesión 1. Comprueba /hola desde el navegador; aún no hay colección HTTP que ejecutar.
  2. Localiza src/main/java/com/ejemplo/gestor/controller. Las nuevas clases irán en ese paquete; si ya tienes una clase con el nombre del ejemplo, modifica esa clase en vez de duplicarla.
  3. Anota un identificador numérico, como 7, y un filtro de tu tema, como estado=activo. Hoy sirven para observar qué valores llegan al método.

Paso 2 · @RequestParam · leer la query string

  1. Crea SaludoController.java en el paquete controller, junto a HolaController. El primer bloque es la clase completa; los siguientes son versiones alternativas de su mismo método saludo.
  2. Ejecuta primero la versión con parámetro obligatorio. Después sustituye ese método por el que incluye defaultValue, conservando package, imports y la clase. No dejes dos métodos que atiendan GET /saludo.
  3. Reinicia después de cada cambio. Comprueba primero la URL completa del ejemplo y después cambia solo un dato: el nombre, su ausencia o su valor vacío. Así sabrás qué modificación explica cada respuesta.

Crea un SaludoController en com.ejemplo.gestor.controller:

package com.ejemplo.gestor.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SaludoController {

    @GetMapping("/saludo")
    public String saludo(@RequestParam(name = "nombre") String nombre) {
        return "Hola, " + nombre + ".";
    }
}

Reinicia y prueba:

http://localhost:8080/saludo?nombre=Marc

Responde Hola, Marc. Cambia el valor de la URL y responde otra cosa. Un método, infinitas respuestas.

Lo que ha ocurrido por dentro es esto:

De la URL al parámetro Java
  1. Llega GET /saludo?nombre=Marc
  2. Spring busca el método de /saludo
  3. Ve @RequestParam(name = "nombre")
  4. Busca nombre en la query string
  5. Pasa "Marc" al método

Escribe siempre el nombre

Verás mucho código con @RequestParam String nombre, sin el name. Funciona porque el proyecto se compila conservando los nombres de los parámetros, pero eso depende de cómo se compile: si alguien cambia la configuración, o si el compilador ofusca los nombres, deja de funcionar sin ningún aviso. Escribe el nombre explícitamente. Cuesta ocho caracteres y no vuelve a fallar nunca.

Ahora pide la ruta sin el parámetro:

http://localhost:8080/saludo

Observa a continuación:

{
  "timestamp": "2026-09-02T09:22:14.831+00:00",
  "status": 400,
  "error": "Bad Request",
  "path": "/saludo"
}

400 Bad Request indica que la petición no se puede aceptar tal como está construida. En este caso falta un parámetro declarado como obligatorio. El método no se ha ejecutado: Spring rechaza la petición al intentar preparar sus argumentos.

Mira además la consola: hay un aviso que dice, más o menos, Required request parameter 'nombre' is not present. El mensaje bueno está siempre ahí.

Esto es importante porque enseña algo que se repetirá todo el curso: Spring valida antes de ejecutar. Cuando tu método arranca, ya se ha comprobado que la petición encaja con lo que has declarado.

Casi nunca queremos un 400 por un parámetro que podría tener un valor razonable:

@GetMapping("/saludo")
public String saludo(
        @RequestParam(name = "nombre", defaultValue = "mundo") String nombre) {
    return "Hola, " + nombre + ".";
}
Petición Respuesta
/saludo Hola, mundo.
/saludo?nombre=Marc Hola, Marc.
/saludo?nombre= Hola, mundo. — el valor por defecto también se aplica al valor vacío

defaultValue se aplica tanto cuando el parámetro no aparece como cuando llega vacío, según la documentación de RequestParam. Repite ambas peticiones para observarlo. Un texto formado por espacios requiere tratar su contenido: no equivale a enviar un valor vacío.

Existe también required = false, que hace opcional el parámetro sin darle valor por defecto. En ese caso, si no llega, el parámetro vale null, y comprobarlo es cosa tuya.

@RequestParam(name = "nombre", required = false) String nombre
@GetMapping("/incidencias")
public String buscar(
        @RequestParam(name = "estado", defaultValue = "todas") String estado,
        @RequestParam(name = "pagina", defaultValue = "1") int pagina) {

    return "Buscando incidencias con estado " + estado
            + ", página " + pagina;
}

Pruébalo con /incidencias?estado=abierta&pagina=3, y también sin ningún parámetro.

Fíjate en int pagina. Por la URL llegó el texto "3" y en tu método hay un entero: Spring ha convertido el tipo por ti. Lo hace con int, long, boolean, LocalDate y muchos más.

Rómpelo de nuevo:

http://localhost:8080/incidencias?pagina=abc

Otro 400. Es exactamente el mismo mecanismo: no se puede construir un int con "abc", así que la petición no encaja con lo declarado y se rechaza antes de ejecutar nada. Declarar el tipo ya es validar. En la UD3 aprenderemos a devolver un mensaje de error mucho mejor que este, pero el comportamiento de base ya te protege.

Paso 3 · @PathVariable · leer un trozo de la ruta

Crea UsuarioController.java con el bloque completo. Las llaves de {id} pertenecen a la anotación Java: en el navegador escribe un valor concreto, como /usuarios/3. El método de rutas anidadas se añade dentro de esa clase, antes de su última llave; conserva los métodos existentes mientras las combinaciones de método y ruta sean distintas. Prueba un número y luego texto para observar la conversión a int.

package com.ejemplo.gestor.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UsuarioController {

    @GetMapping("/usuarios/{id}")
    public String usuario(@PathVariable(name = "id") int id) {
        return "Ficha del usuario " + id;
    }
}

Prueba /usuarios/3, /usuarios/41, /usuarios/999. Un solo método admite esos tres números y devuelve un texto con cada uno. Todavía no consulta una colección, por lo que no comprueba si existe ese usuario.

Las llaves marcan un hueco: {id} no es texto literal, es una variable. Lo que aparezca ahí se captura y se entrega al parámetro que lleva ese mismo nombre.

El nombre tiene que coincidir

El texto entre llaves de la ruta y el name de la anotación deben ser idénticos. Si escribes /usuarios/{id} y anotas @PathVariable(name = "identificador"), Spring no podrá resolver ese argumento al atender la petición. Corrige la coincidencia entre ambos nombres; no lo confundas con un error de conversión del valor.

Puede haber varias, y se anidan con toda naturalidad:

@GetMapping("/proyectos/{proyectoId}/incidencias/{incidenciaId}")
public String incidenciaDeProyecto(
        @PathVariable(name = "proyectoId") int proyectoId,
        @PathVariable(name = "incidenciaId") int incidenciaId) {

    return "Incidencia " + incidenciaId + " del proyecto " + proyectoId;
}

/proyectos/7/incidencias/41 se lee de un vistazo: la incidencia 41, que pertenece al proyecto 7. Esa legibilidad no es casualidad, y es justo lo que vamos a convertir en regla ahora.

Paso 4 · Agrupar rutas con @RequestMapping

Sustituye la versión anterior de UsuarioController por la clase que aparece aquí. Hemos movido el prefijo común a @RequestMapping: comprueba que los métodos ya no repiten /usuarios, o la dirección resultante sería /usuarios/usuarios/.... Después de reiniciar, vuelve a probar listado y detalle para comprobar que la agrupación no ha cambiado sus direcciones.

package com.ejemplo.gestor.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/usuarios")
public class UsuarioController {

    @GetMapping
    public String lista(
            @RequestParam(name = "rol", defaultValue = "todos") String rol) {
        return "Lista de usuarios con rol " + rol;
    }

    @GetMapping("/{id}")
    public String detalle(@PathVariable(name = "id") int id) {
        return "Ficha del usuario " + id;
    }
}

La ruta final es la del @RequestMapping de la clase más la del método:

Método Ruta que atiende
lista /usuarios
detalle /usuarios/3

Un @GetMapping sin argumento designa la ruta declarada en la clase, sin segmento adicional.

Curiosidad · qué pasa si dos rutas encajan a la vez

Imagina que añades /usuarios/nuevo y ya tienes /usuarios/{id}. Una petición a /usuarios/nuevo encaja con las dos.

Spring no elige al azar ni por orden de escritura: prefiere siempre la ruta más específica, y un texto literal es más específico que una variable. Gana /usuarios/nuevo.

Aun así, mezclar identificadores y palabras en el mismo nivel envejece mal. Cuando lleguemos al diseño REST de la UD3 veremos por qué se evita.

Paso 5 · Las rutas del gestor

Construye el controlador de tu entidad principal siguiendo este orden: crea la clase en controller, anótala con @RestController y @RequestMapping, añade el listado y compruébalo; después añade el detalle y compruébalo; por último incorpora los parámetros opcionales y las rutas anidadas. El controlador ProyectoController de la tabla es el patrón: en reservas sería ReservaController y /reservas. Mantén tus nombres en todos los pasos posteriores. Hoy cada método devuelve una frase con los parámetros, no una lista de registros reales.

Escribe un ProyectoController que atienda estas cuatro:

Ruta Qué devuelve
GET /proyectos Lista de proyectos
GET /proyectos?estado=activo Lista de proyectos con estado activo
GET /proyectos/{id} Ficha del proyecto 7
GET /proyectos/{id}/incidencias Incidencias del proyecto 7

Condiciones:

  1. Usa @RequestMapping a nivel de clase. No repitas /proyectos en cada método.
  2. El parámetro estado es opcional, con todos como valor por defecto.
  3. El id debe ser un int, no un String. Después comprueba qué pasa con /proyectos/abc y anótalo.

Amplía tu controlador con dos rutas más, decidiendo tú dónde va cada dato:

  • Una para consultar una incidencia concreta dentro de un proyecto concreto.
  • Una para buscar incidencias filtrando por prioridad y por página.

Para cada una, escribe en un comentario del código la respuesta a esto: qué datos has puesto en la ruta, cuáles en la query string, y qué prueba de la regla has aplicado para decidirlo.

Paso 6 · Comprobar y registrar el resultado del proyecto

  1. Prueba el saludo con el parámetro ausente, vacío y con un nombre. Con defaultValue="mundo", los dos primeros deben devolver Hola, mundo..
  2. Consulta el detalle con 7 y con abc: el primero devuelve el texto que has programado y el segundo produce 400 al no poder convertirse a entero. Un número como 999 todavía no permite saber si existe un registro: esa búsqueda aún no está implementada.

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 · Cuatro peticiones sin ejecutar nada

Con este controlador delante:

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

    @GetMapping("/{id}")
    public String detalle(
            @PathVariable(name = "id") int id,
            @RequestParam(name = "formato", defaultValue = "corto") String formato) {

        return "Tarea " + id + " en formato " + formato;
    }
}

Predice, antes de probarlo, qué devuelve cada petición: el código de estado y el cuerpo si lo hay. Escribe también por qué.

  1. GET /tareas/5
  2. GET /tareas/5?formato=largo
  3. GET /tareas/cinco
  4. GET /tareas
  5. POST /tareas/5

Después copia el controlador en tu proyecto y compruébalas una a una. De las cinco, dos suelen fallarse. Cuando una predicción resulte equivocada, no basta con corregirla: escribe qué regla habías aplicado mal.

Objetivo mínimoLas rutas /saludo, /saludo?nombre=Marc y /usuarios/3 funcionando, y sabes provocar el 400.
Si lo tienesEl ProyectoController completo con sus cuatro rutas y las dos que has diseñado tú, justificadas.
RetoLas cinco predicciones escritas antes de ejecutar, comprobadas y con los fallos explicados.
Ver respuestas

1 · defaultValue pone un valor cuando el parámetro no llega, así que el parámetro nunca es nulo. required = false lo deja llegar como null y te obliga a comprobarlo.

2 · Un 400 Bad Request. Spring intenta convertir el texto al tipo declarado antes de invocar el método; como la conversión falla, la petición se rechaza y el método no llega a ejecutarse.

3 · El identificador en la ruta, el año en la query string. La prueba: si quito el año, /facturas sigue significando algo; si quito el identificador, /facturas/ ya no pregunta por esa factura.

4 · Exactamente /usuarios: la ruta de la clase, sin añadir nada.

Cierre

15 minutos · resultado comprobable y explicación individual

Al terminar la sesión:

Las rutas de listado y detalle devuelven textos que incorporan los parámetros recibidos. Debe ser posible localizar el método que atendió cada petición; la consulta de objetos reales comienza en la sesión 3.

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