En la lección anterior fijamos el método de trabajo y decidimos, con criterio, qué sustantivos del dominio de Tienda Aroma merecen ser recursos. Ahora toca lo siguiente: darles dirección. La URI es la parte más visible y más duradera de una API —los clientes la escriben en su código, la guardan en marcadores, la copian en tickets— y por eso es también la más cara de cambiar. Esta lección establece las reglas de nombrado, resuelve cuándo anidar y cuándo no, elige el tipo de identificador y afronta el problema que ningún CRUD resuelve por sí solo: cómo se modelan acciones como pagar un pedido o moderar una reseña. Terminaremos con el mapa completo de URIs de Tienda Aroma, que el resto del módulo dará por bueno.
Contenido
- Qué es un recurso y qué es una URI
- Sustantivos y no verbos
- Colecciones y elementos
- Reglas de nombrado
- Jerarquía y anidamiento
- Recursos singleton
- Path parameters frente a query parameters
- Diseño de identificadores
- Acciones que no son CRUD
- Mapa de URIs de Tienda Aroma
- Qué es un recurso y qué es una URI
Recordemos la definición de 01-04: un recurso es cualquier cosa con identidad sobre la que tenga sentido operar; la URI es su identificador estable; la representación es una de sus posibles formas concretas (el JSON que viaja, el PDF, el HTML).
Tres consecuencias prácticas que gobiernan toda esta lección:
- Un recurso puede tener varias representaciones (
/pedidos/ped_5001/facturaen JSON o en PDF), pero mantiene una sola URI. La representación se elige por negociación de contenido (02-05), no cambiando la URL con.pdf. - Una URI identifica, no describe la operación. Lo que se hace con el recurso lo dice el método (02-03).
- Las URIs deberían sobrevivir a los cambios internos. Si
/cafes/caf_001deja de funcionar porque se ha refactorizado la base de datos, hemos roto el contrato.
- Sustantivos y no verbos
La regla más citada del diseño REST, y la que más se incumple:
| ❌ Verbo en la ruta | ✅ Sustantivo + método |
|---|---|
GET /obtenerCafes |
GET /cafes |
POST /crearPedido |
POST /pedidos |
POST /actualizarStock?id=caf_001 |
PATCH /cafes/caf_001 |
GET /borrarResena?id=res_101 |
DELETE /resenas/res_101 |
POST /listarPedidosDeCliente |
GET /clientes/cli_842/pedidos |
Por qué importa, más allá de la estética:
- El método ya es el verbo.
GET /obtenerCafesrepite el verbo y, peor, permite la incoherenciaPOST /obtenerCafes. - Se pierde la semántica de HTTP.
GET /borrarResenaes una barbaridad funcional: un buscador o un prefetch del navegador podría borrar reseñas al recorrer enlaces, porque GET es seguro por definición (02-03). - Se pierde la previsibilidad. Con verbos, cada endpoint hay que memorizarlo:
obtenerCafes,getPedidos,listarResenas,consultarCliente.
Ojo con un matiz importante: los identificadores del código sí van en español y con verbo (obtenerCafes(), crearPedido()), como fija la guía de estilo. Lo que no lleva verbo es la URI.
- Colecciones y elementos
Toda API orientada a recursos se estructura sobre dos figuras:
- Colección: un conjunto de recursos del mismo tipo.
/cafes. - Elemento (o recurso individual): un miembro concreto.
/cafes/caf_001.
El patrón se lee de izquierda a derecha como una ruta de navegación:
/v1/clientes/cli_842/pedidos/ped_5001 │ │ │ │ └── elemento: un pedido concreto │ │ │ └────────── colección: los pedidos de ese cliente │ │ └────────────────── elemento: un cliente concreto │ └─────────────────────────── colección: todos los clientes └─────────────────────────────── versión de la API
Cada nivel alterna colección → elemento → colección → elemento. Si tu URI rompe esa alternancia (/clientes/pedidos/cli_842), casi siempre hay un error de diseño.
Y cada figura admite operaciones distintas, lo que anticipa 02-03:
Colección /cafes |
Elemento /cafes/caf_001 |
|
|---|---|---|
GET |
Lista, filtrada y paginada | Devuelve ese café |
POST |
Crea uno nuevo | No se usa (salvo subrecurso de acción) |
PUT |
No se usa (reemplazar toda la colección es peligroso) | Reemplaza ese café |
PATCH |
No se usa | Modifica campos de ese café |
DELETE |
No se usa (borrar todo el catálogo por accidente) | Borra ese café |
- Reglas de nombrado
Estas son las reglas que Tienda Aroma incorpora a su guía de estilo.
4.1. Plural consistente
/cafes, /clientes, /pedidos, /resenas, /carritos. Siempre plural, incluso cuando suene raro, porque la alternativa —singular para el elemento y plural para la colección— obliga a recordar dos formas por recurso.
La única excepción son los singleton (sección 6), que por definición no son colección.
4.2. Minúsculas siempre
El componente de ruta de una URL distingue mayúsculas de minúsculas (a diferencia del nombre de host). /Cafes y /cafes son dos recursos distintos para el estándar, y aceptar los dos duplica la superficie del contrato y estropea la caché.
4.3. Guiones para palabras compuestas (kebab-case)
Motivos: es la convención dominante en la web, es más legible y los buscadores tratan el guion como separador de palabras (relevante si parte de la API se indexa o si compartes enlaces de documentación). Fíjate en la asimetría deliberada: kebab-case en la URI, camelCase en el JSON. Es una inconsistencia aparente, pero son dos mundos con convenciones propias y consolidadas; lo importante es que cada mundo sea coherente consigo mismo.
4.4. Sin extensiones de fichero
El formato es una cuestión de representación, y se negocia con cabeceras (02-05). Poner .json mezcla identidad y formato: si mañana añades XML o PDF, cada recurso tiene tres URIs para la misma cosa.
4.5. Sin barra final
/cafes y /cafes/ son técnicamente rutas distintas. Elige una —Tienda Aroma usa sin barra final— y redirige la otra con 301 (02-04) en lugar de servir las dos.
4.6. Sin sufijos técnicos ni jerga interna
El nombre de la clase, de la tabla o del patrón de implementación no es asunto del consumidor. Ojo también con /api: si el host ya es api.tiendaaroma.example, repetirlo en la ruta es redundante.
4.7. Sin caracteres problemáticos
Nada de acentos, ñ, espacios ni mayúsculas en los segmentos de ruta que tú controlas. Por eso la colección es /resenas y no /reseñas: aunque los navegadores modernos codifican los caracteres no ASCII, en la práctica acaban apareciendo como /rese%C3%B1as en logs, ejemplos de curl y clientes antiguos.
- Jerarquía y anidamiento
Anidar expresa pertenencia: /cafes/caf_001/resenas son las reseñas de ese café.
5.1. La regla práctica
Anida un subrecurso solo si no tiene sentido fuera de su padre, o si la relación de pertenencia es la forma natural de acceder a él.
Ejemplos en Tienda Aroma:
| URI | ¿Anidar? | Razón |
|---|---|---|
/cafes/caf_001/resenas |
Sí | Las reseñas de un café son un caso de uso central (la ficha de producto) |
/carritos/car_77/lineas/caf_002 |
Sí | Una línea de carrito no existe sin su carrito |
/pedidos/ped_5001/pago |
Sí | El pago pertenece a un pedido concreto |
/clientes/cli_842/pedidos |
Sí | "Mis pedidos" es un caso de uso real de la SPA y de Aroma Móvil |
/clientes/cli_842/pedidos/ped_5001/lineas/1/cafe |
No | Cuatro niveles: ilegible y frágil. Enlaza al café, no lo anides |
/origenes/etiopia/cafes |
No | El origen es un atributo: se resuelve con un filtro ?origen=Etiopía |
5.2. Máximo dos niveles
Una regla que ahorra disgustos: no pases de /coleccion/{id}/subcoleccion/{id}. A partir de ahí la URI se vuelve ilegible, acopla el cliente a una jerarquía que puede cambiar y obliga a validar cadenas de pertenencia largas.
Cuando necesites bajar más, corta la jerarquía y usa una colección de primer nivel con filtro:
# En lugar de anidar tres niveles
curl "https://api.tiendaaroma.example/v1/resenas?cafeId=caf_001&estado=pendiente_moderacion"5.3. Doble acceso: anidado y plano
Un mismo recurso puede ser alcanzable por dos rutas si cada una sirve a un caso de uso distinto. En Tienda Aroma:
# Ficha de producto: las reseñas de un café (SPA y Aroma Móvil)
GET /v1/cafes/caf_001/resenas
# Panel interno: todas las reseñas pendientes de moderar, de cualquier café
GET /v1/resenas?estado=pendiente_moderacionReglas para que esto no se convierta en un problema:
- El elemento vive en una sola URI canónica:
/resenas/res_101. La forma anidada es solo para listar y crear. - El enlace
selfde la representación apunta siempre a la canónica, para que dos rutas no generen dos identidades. - La creación anidada es más cómoda:
POST /cafes/caf_001/resenasno necesita repetircafeIden el cuerpo, porque ya está en la URI.
graph TD
C["GET /v1/cafes/caf_001/resenas<br/><i>vista anidada</i>"] --> R["res_101<br/>res_102"]
P["GET /v1/resenas?estado=pendiente_moderacion<br/><i>vista plana con filtro</i>"] --> R
R --> CAN["URI canónica del elemento:<br/><b>/v1/resenas/res_101</b><br/>(es la que va en _links.self)"]
- Recursos singleton
Un singleton es un recurso del que solo existe una instancia en su contexto. No tiene colección ni identificador propio, y por eso va en singular.
/v1/clientes/cli_842/preferencias preferencias del cliente (idioma, moneda, boletín) /v1/pedidos/ped_5001/pago el pago de ese pedido /v1/pedidos/ped_5001/envio el envío gestionado por RápidoEnvíos /v1/pedidos/ped_5001/factura la factura de ese pedido /v1/cafes/caf_001/imagen la imagen principal del café
Un singleton típicamente admite GET, PUT y a veces DELETE, pero no POST (no hay colección a la que añadir), con la excepción de los singleton que modelan una acción, que sí usan POST (sección 9).
GET /v1/clientes/cli_842/preferencias HTTP/1.1
Host: api.tiendaaroma.example
Accept: application/jsonHTTP/1.1 200 OK
Content-Type: application/json
{
"idioma": "es",
"moneda": "EUR",
"boletin": true,
"tuestePreferido": "medio",
"_links": {
"self": { "href": "/v1/clientes/cli_842/preferencias" },
"cliente": { "href": "/v1/clientes/cli_842" }
}
}Cuidado con el singleton mal usado: /clientes/cli_842/direccion está bien si el cliente solo puede tener una; en cuanto pueda tener varias, se convierte en /clientes/cli_842/direcciones y eso es un cambio rompedor (02-07). Si tienes dudas, empieza por colección.
- Path parameters frente a query parameters
La confusión más habitual del diseño de URIs. La regla es sencilla y casi nunca falla:
La ruta identifica el recurso. La query string modifica cómo se devuelve la colección.
| Va en la ruta | Va en la query string |
|---|---|
Identificadores de recurso: /cafes/caf_001 |
Filtros: ?origen=Colombia&tueste=medio |
Jerarquía de pertenencia: /cafes/caf_001/resenas |
Ordenación: ?ordenar=-precioEuros |
Subrecursos singleton: /pedidos/ped_5001/pago |
Paginación: ?limite=20&desplazamiento=40 |
Acciones modeladas como subrecurso: /resenas/res_101/aprobacion |
Búsqueda: ?q=yirgacheffe |
Selección de campos: ?campos=id,nombre,precioEuros |
|
Expansión: ?expandir=lineas.cafe |
Ejemplo comparado:
# ✅ Correcto: el id identifica, va en la ruta
curl https://api.tiendaaroma.example/v1/cafes/caf_001
# ❌ Incorrecto: el id no es un filtro
curl "https://api.tiendaaroma.example/v1/cafes?id=caf_001"
# ✅ Correcto: origen es un criterio de selección sobre la colección
curl "https://api.tiendaaroma.example/v1/cafes?origen=Etiop%C3%ADa"
# ❌ Incorrecto: convierte un valor de atributo en jerarquía
curl https://api.tiendaaroma.example/v1/cafes/origen/etiopiaDos matices que conviene conocer:
- Un filtro que devuelve un único elemento sigue siendo una colección.
GET /cafes?nombre=Etiopía Yirgacheffedevuelve{"datos": [...], "total": 1}, no el objeto suelto, y devuelve200con lista vacía si no hay coincidencias (no404). La razón: el recurso "colección filtrada" existe aunque esté vacío. - Los parámetros de query afectan a la caché. Cada combinación distinta es una URL distinta y, por tanto, una entrada de caché distinta. Es un argumento más para no multiplicar parámetros sin necesidad (04-06).
- Diseño de identificadores
El identificador que pongas en la URI es contrato para siempre. Las opciones:
| Tipo | Ejemplo | Ventajas | Inconvenientes |
|---|---|---|---|
| Entero autoincremental | /cafes/1 |
Corto, legible, índice barato | Filtra volumen de negocio, enumerable, choca al fusionar bases de datos |
| UUID v4 | /cafes/6f1c... |
No adivinable, generable por el cliente, único entre sistemas | Largo, ilegible, peor localidad en índices |
| UUID v7 / ULID | /cafes/01HQ... |
Ordenable por tiempo, buen comportamiento en índices | Revela el instante de creación |
| Slug | /cafes/etiopia-yirgacheffe |
Legible, bueno para SEO | Cambia si cambia el nombre; hay que gestionar duplicados |
| Id con prefijo | /cafes/caf_001 |
Autodescriptivo, imposible confundir tipos, buscable en logs | Convención propia, no estándar |
La decisión de Tienda Aroma: id con prefijo
caf_001, cli_842, ped_5001, res_101, car_77, fac_88, evt_9f2c. Es el estilo que popularizó Stripe y las razones son muy prácticas:
- Autodescriptivos. Al leer un log o un ticket,
ped_5001se entiende sin contexto. Con5001a secas, no sabes de qué es. - Imposible cruzar tipos. Si alguien envía
POST /v1/pedidoscon{"clienteId": "caf_001"}, el servidor detecta el prefijo equivocado y responde400condatos_invalidos, en lugar de crear un pedido incoherente. - Opacos por contrato. La documentación dice explícitamente: el identificador es una cadena opaca, no lo parsees, no supongas longitud, no supongas que la parte numérica es correlativa. Así podemos migrar mañana a
caf_01HQ8ZK...sin romper a nadie.
En producción, la parte que sigue al prefijo debería ser aleatoria y no correlativa. Los caf_001 y ped_5001 del curso son didácticos; en una tienda real, publicar identificadores correlativos tiene dos problemas:
- Fuga de información de negocio. Un competidor que pide un pedido el lunes y otro el viernes sabe cuántos pedidos has recibido esa semana. Es el clásico German tank problem.
- Enumeración. Con ids correlativos, recorrer
ped_5001,ped_5002,ped_5003… es trivial. Que un tercero pueda leer pedidos ajenos es un fallo de autorización, no de ids —se llama IDOR y se trata en 04-02—, pero los ids adivinables convierten un fallo puntual en una fuga masiva. La regla es: autoriza siempre, y además no lo pongas fácil.
Sobre los slugs: son excelentes para la web pública (tiendaaroma.example/cafes/etiopia-yirgacheffe) y malos como identidad de API, porque cambian. Si los quieres, el patrón habitual es que el slug sea un campo más de la representación y un filtro (?slug=etiopia-yirgacheffe), mientras la URI canónica sigue siendo el id opaco.
- Acciones que no son CRUD
Aquí está el problema de diseño más interesante de la lección. Muchas operaciones del negocio no son "crear, leer, actualizar, borrar":
- Pagar un pedido.
- Anular un pedido.
- Aprobar o rechazar una reseña.
- Vaciar un carrito.
- Reenviar el correo de confirmación.
Hay tres estrategias, y conviene entender las tres antes de elegir.
Estrategia A: cambiar el estado con PATCH
PATCH /v1/resenas/res_101 HTTP/1.1
Content-Type: application/merge-patch+json
{ "estado": "publicada" }| A favor | En contra |
|---|---|
| No inventa recursos nuevos | Los efectos secundarios quedan ocultos: aprobar dispara correos, recalcula la puntuación media del café… |
| CRUD puro, fácil de implementar | No se pueden pasar parámetros propios de la acción (motivo del rechazo) |
| Semántica clara para el cliente | No distingue "cambiar un dato" de "ejecutar una transición" |
| Difícil de autorizar por separado: quien puede editar puede aprobar |
Es aceptable cuando la transición es puramente un cambio de dato, sin lógica ni efectos.
Estrategia B: verbo en la URI (RPC sobre HTTP)
| A favor | En contra |
|---|---|
| Intención explícita e inmediata | Reintroduce verbos en la URI, justo lo que evitamos en la sección 2 |
| Fácil de explicar | Se degrada rápido: aprobarConComentario, aprobarYNotificar |
| Es lo que hacen muchas APIs reales | Baja al nivel 1 de Richardson en esos endpoints |
Estrategia C: la acción se convierte en un subrecurso (la elección de Tienda Aroma)
Se busca el sustantivo que hay detrás del verbo: aprobar → una aprobación; pagar → un pago; anular → una anulación. Ese sustantivo es un recurso que se crea con POST.
POST /v1/resenas/res_101/aprobacion HTTP/1.1
Host: api.tiendaaroma.example
Authorization: Bearer <token del moderador>
Content-Type: application/json
{ "nota": "Reseña verificada, compra confirmada" }HTTP/1.1 201 Created
Content-Type: application/json
Location: /v1/resenas/res_101/aprobacion
{
"estado": "publicada",
"moderadorId": "cli_003",
"fechaAprobacion": "2026-03-14T10:32:00Z",
"_links": {
"self": { "href": "/v1/resenas/res_101/aprobacion" },
"resena": { "href": "/v1/resenas/res_101" }
}
}Ventajas, que son justo las que buscábamos:
- Sin verbos en la ruta:
aprobaciones un sustantivo y el verbo lo ponePOST. - La acción admite cuerpo propio: el motivo del rechazo, la referencia del pago, el importe.
- La acción es un recurso consultable:
GET /v1/pedidos/ped_5001/pagodevuelve el pago realizado, con su fecha y su referencia. - Se autoriza por separado: el permiso para crear
/aprobaciones distinto del permiso para editar la reseña (04-03). - Deja sitio a la idempotencia: al ser un
POSTbien delimitado, se le puede exigirIdempotency-Key(02-03).
Y el mapa de acciones de Tienda Aroma queda así:
| Acción de negocio | URI | Método | Verbo evitado |
|---|---|---|---|
| Pagar un pedido | /pedidos/{id}/pago |
POST | pagar |
| Anular un pedido | /pedidos/{id}/anulacion |
POST | anular |
| Devolver un pedido | /pedidos/{id}/devolucion |
POST | devolver |
| Aprobar una reseña | /resenas/{id}/aprobacion |
POST | aprobar |
| Rechazar una reseña | /resenas/{id}/rechazo |
POST | rechazar |
| Responder a una reseña | /resenas/{id}/respuestas |
POST | responder |
| Vaciar un carrito | /carritos/{id}/lineas |
DELETE | vaciar |
Fíjate en la última fila: antes de inventar un subrecurso, comprueba si un método estándar ya lo expresa. Vaciar el carrito es exactamente "borrar todas sus líneas", así que DELETE /carritos/car_77/lineas es más natural que un POST /carritos/car_77/vaciado. Es la excepción a la regla del apartado 3 sobre no usar DELETE en colecciones, y es legítima porque aquí la colección está acotada a un carrito concreto y su borrado completo es una operación de negocio real.
- Mapa de URIs de Tienda Aroma
Este es el resultado de la lección y el mapa que el resto del módulo dará por bueno. Los métodos aparecen para dar contexto; su semántica exacta es 02-03 y los códigos de respuesta, 02-04.
| URI | Métodos | Descripción |
|---|---|---|
/v1/cafes |
GET, POST, HEAD, OPTIONS | Catálogo de cafés; filtrable, ordenable y paginado |
/v1/cafes/{cafeId} |
GET, PUT, PATCH, DELETE, HEAD | Un café concreto |
/v1/cafes/{cafeId}/imagen |
GET, PUT, DELETE | Imagen principal (singleton, no JSON) |
/v1/cafes/{cafeId}/resenas |
GET, POST | Reseñas de un café (vista anidada) |
/v1/clientes |
GET, POST | Clientes registrados (solo panel interno) |
/v1/clientes/{clienteId} |
GET, PATCH, DELETE | Un cliente concreto |
/v1/clientes/{clienteId}/preferencias |
GET, PUT | Preferencias del cliente (singleton) |
/v1/clientes/{clienteId}/pedidos |
GET | "Mis pedidos" |
/v1/carritos |
POST | Crea un carrito |
/v1/carritos/{carritoId} |
GET, DELETE | Un carrito concreto |
/v1/carritos/{carritoId}/lineas |
GET, POST, DELETE | Líneas del carrito; DELETE lo vacía |
/v1/carritos/{carritoId}/lineas/{cafeId} |
GET, PUT, DELETE | Una línea; PUT fija la cantidad |
/v1/pedidos |
GET, POST | Pedidos; POST confirma un carrito |
/v1/pedidos/{pedidoId} |
GET, PATCH | Un pedido concreto |
/v1/pedidos/{pedidoId}/lineas |
GET | Líneas del pedido (inmutables) |
/v1/pedidos/{pedidoId}/pago |
GET, POST | Pago del pedido (acción + consulta) |
/v1/pedidos/{pedidoId}/anulacion |
POST | Anula el pedido |
/v1/pedidos/{pedidoId}/devolucion |
POST | Solicita devolución |
/v1/pedidos/{pedidoId}/envio |
GET, PUT | Envío; PUT lo actualiza RápidoEnvíos |
/v1/pedidos/{pedidoId}/factura |
GET | Factura (JSON o PDF, según Accept) |
/v1/resenas |
GET | Todas las reseñas (vista plana, filtrable) |
/v1/resenas/{resenaId} |
GET, PATCH, DELETE | Una reseña (URI canónica) |
/v1/resenas/{resenaId}/aprobacion |
POST | Aprueba la reseña |
/v1/resenas/{resenaId}/rechazo |
POST | Rechaza la reseña |
/v1/resenas/{resenaId}/respuestas |
GET, POST | Respuestas de la tienda a la reseña |
Decisiones anotadas para no olvidarlas:
- No hay
POST /v1/resenas: una reseña siempre nace asociada a un café, así que solo se crea en/cafes/{cafeId}/resenas. La vista plana es de solo lectura y sirve al panel interno. - No hay
DELETE /v1/pedidos/{id}: un pedido no se borra, se anula. El histórico contable es sagrado. /v1/origenesse ha considerado y descartado para la v1: el origen es un atributo y se filtra con?origen=. Si algún día tiene datos propios (altitud, cooperativa, foto), será una colección y podrá añadirse sin romper nada (02-07).
Errores Comunes y Consejos
- Meter el verbo en la ruta "solo para esta operación". Empieza con una excepción y acaba con veinte. Si necesitas una acción, busca su sustantivo.
- Anidar por costumbre.
/clientes/cli_842/pedidos/ped_5001obliga al servidor a validar que ese pedido es de ese cliente y al cliente a conocer dos ids para pedir uno. Anida para listar, usa la URI canónica para el elemento. - Usar la query string para identificar.
/cafes?id=caf_001rompe la caché por URL, complica los enlacesselfy no permite subrecursos. - Poner
.jsonal final. Mezcla identidad y formato; para eso estáAccept. - Pluralizar mal.
/cafesy/cafe/caf_001conviviendo es el bug de documentación más frecuente del mundo. - Exponer el id de base de datos por comodidad. Cambiar de motor o fusionar entornos te obligará a romper el contrato. Un id opaco te deja libertad.
- Consejo: escribe las URIs antes que el código y léelas en voz alta. Si al leer
/pedidos/ped_5001/anulacionconPOSTse entiende sin explicar, está bien diseñada. - Consejo: mantén una tabla como la de la sección 10 en el repositorio. Es el índice del contrato y el sitio donde se discute cualquier endpoint nuevo antes de existir.
Ejercicios
Ejercicio 1: corregir un conjunto de URIs
Un equipo ha propuesto estas rutas para Tienda Aroma. Corrígelas y justifica cada cambio.
GET /v1/api/getCafes.json POST /v1/Cafe/crear GET /v1/cafes?id=caf_001 POST /v1/cafes/caf_001/resenas/res_101/aprobar GET /v1/clientes/cli_842/pedidos/ped_5001/lineas/1/cafe/caf_001/resenas DELETE /v1/carritos/car_77/vaciarCarrito GET /v1/cafes/origen/etiopia/tueste/claro
Ejercicio 2: modelar una acción nueva
Tienda Aroma quiere que un cliente pueda regalar un pedido: al confirmarlo indica el correo del destinatario y un mensaje, y el sistema envía un aviso y oculta el precio en el albarán.
Modela esta funcionalidad con las tres estrategias de la sección 9 (PATCH, verbo en la URI, subrecurso), muestra la petición HTTP de cada una y elige la que encaja con la guía de estilo, justificándolo.
Ejercicio 3: decidir anidamientos
Para cada necesidad, decide la URI y di si has anidado o no y por qué:
- El panel interno quiere ver todas las líneas vendidas del café
caf_001en el último mes. - Aroma Móvil quiere las reseñas escritas por el cliente
cli_842. - RápidoEnvíos quiere actualizar el estado del envío del pedido
ped_5001. - La SPA quiere el histórico de cambios de precio de
caf_001.
Soluciones
Solución 1
| Propuesta | Corrección | Motivo |
|---|---|---|
GET /v1/api/getCafes.json |
GET /v1/cafes |
Sobra /api (ya está en el host), sobra el verbo get (lo pone el método) y sobra .json (lo negocia Accept) |
POST /v1/Cafe/crear |
POST /v1/cafes |
Minúsculas, plural y sin verbo: POST sobre la colección ya significa crear |
GET /v1/cafes?id=caf_001 |
GET /v1/cafes/caf_001 |
El identificador va en la ruta; la query filtra colecciones |
POST /v1/cafes/caf_001/resenas/res_101/aprobar |
POST /v1/resenas/res_101/aprobacion |
Verbo → sustantivo, y se recorta a la URI canónica de la reseña: el café es redundante |
GET /v1/clientes/.../cafe/caf_001/resenas |
GET /v1/cafes/caf_001/resenas |
Cinco niveles de anidamiento innecesarios: la reseña depende del café, no del pedido del cliente |
DELETE /v1/carritos/car_77/vaciarCarrito |
DELETE /v1/carritos/car_77/lineas |
El método estándar ya expresa la acción sobre la subcolección |
GET /v1/cafes/origen/etiopia/tueste/claro |
GET /v1/cafes?origen=Etiopía&tueste=claro |
Origen y tueste son atributos, no jerarquía: son filtros |
Solución 2
Estrategia A — PATCH sobre el pedido:
PATCH /v1/pedidos/ped_5001
Content-Type: application/merge-patch+json
{ "regalo": { "destinatario": "[email protected]", "mensaje": "¡Felicidades!" } }Funciona, pero el envío del aviso queda como efecto oculto de un cambio de campo, y no hay dónde consultar después si el aviso se envió.
Estrategia B — verbo en la URI:
POST /v1/pedidos/ped_5001/regalar
Content-Type: application/json
{ "destinatario": "[email protected]", "mensaje": "¡Felicidades!" }Intención clarísima, pero verbo en la ruta: incumple la guía de estilo y abre la puerta a regalarSinAviso.
Estrategia C — subrecurso (elegida):
POST /v1/pedidos/ped_5001/regalo
Content-Type: application/json
{ "destinatario": "[email protected]", "mensaje": "¡Felicidades!", "ocultarPrecio": true }HTTP/1.1 201 Created
Location: /v1/pedidos/ped_5001/regalo
{
"destinatario": "[email protected]",
"mensaje": "¡Felicidades!",
"ocultarPrecio": true,
"avisoEnviado": true,
"fechaAviso": "2026-03-14T10:35:00Z",
"_links": { "self": { "href": "/v1/pedidos/ped_5001/regalo" },
"pedido": { "href": "/v1/pedidos/ped_5001" } }
}Es la elegida: sustantivo (regalo), cuerpo propio para los parámetros de la acción, consultable después con GET, y se puede cancelar con DELETE /v1/pedidos/ped_5001/regalo mientras el pedido no se haya enviado. Encaja exactamente con /pago, /anulacion y /aprobacion.
Solución 3
GET /v1/lineas-pedido?cafeId=caf_001&desde=2026-02-14— sin anidar. Lo que se busca cruza todos los pedidos, así que la jerarquía/pedidos/{id}/lineasno sirve: hace falta una colección plana de primer nivel consultable con filtros. Cuidado con la tentación de escribir/v1/pedidos/lineas: colisionaría con/v1/pedidos/{pedidoId}, porque el enrutador no puede distinguir un id llamadolineasde un segmento fijo. (Alternativa razonable: un recurso de informes/v1/ventas?cafeId=..., si el panel necesita agregados en vez de líneas sueltas.)GET /v1/resenas?clienteId=cli_842— sin anidar bajo el café, porque el criterio es el autor. Anidarlo como/clientes/cli_842/resenastambién sería defendible si "mis reseñas" fuese una pantalla propia de Aroma Móvil; ambas cumplen la regla, y en ese caso convendría elegir una sola para no duplicar contrato.PUT /v1/pedidos/ped_5001/envio— anidado y singleton: un envío no existe fuera de su pedido y solo hay uno.PUTporque RápidoEnvíos envía el estado completo del envío en cada actualización (02-03).GET /v1/cafes/caf_001/historico-precios— anidado: el histórico no tiene sentido fuera de su café y es de solo lectura. Nótese elkebab-caseen la palabra compuesta. Si el histórico se consultase globalmente para todo el catálogo, se convertiría en/v1/historico-precios?cafeId=caf_001.
Conclusión
Las URIs son la cara pública y más duradera de la API, y ahora tienes reglas concretas para diseñarlas: sustantivos en plural y minúsculas, kebab-case para palabras compuestas, sin extensiones ni verbos, con anidamiento solo cuando expresa pertenencia real y sin pasar de dos niveles, singleton en singular para lo que es único, identificación en la ruta y modificación de colecciones en la query string, e identificadores opacos con prefijo que no filtren información de negocio. Y sobre todo tienes resuelto el problema que descoloca a todo el mundo: las acciones que no son CRUD se modelan como subrecursos creados con POST, lo que le da a cada acción cuerpo propio, consulta posterior y autorización independiente. El mapa de URIs de Tienda Aroma está cerrado.
Sabemos ya qué recursos existen y dónde viven. Falta decir con precisión qué se puede hacer con cada uno. En la lección siguiente, 02-03 Métodos HTTP, recorreremos GET, POST, PUT, PATCH, DELETE, HEAD y OPTIONS aplicados a este mapa; entenderemos por qué la seguridad y la idempotencia son mucho más que teoría cuando RápidoEnvíos reintenta una petición o un cliente pulsa dos veces el botón de pagar; compararemos a fondo PUT frente a PATCH con JSON Merge Patch y JSON Patch; y diseñaremos las claves de idempotencia del pago de un pedido.
Curso de REST API: Principios de Diseño y Desarrollo de APIs RESTful
Módulo 1: Introducción a las APIs RESTful
- ¿Qué es una API?
- Historia y evolución de las APIs
- Fundamentos de HTTP para APIs
- Principios básicos de REST
- Modelo de madurez de Richardson y HATEOAS
- REST vs. SOAP
- REST frente a GraphQL, gRPC y webhooks
Módulo 2: Diseño de APIs RESTful
- Principios de diseño de APIs RESTful
- Recursos y URIs
- Métodos HTTP
- Códigos de estado HTTP
- Representaciones, cabeceras y negociación de contenido
- Filtrado, ordenación, paginación y búsqueda
- Versionado de APIs
- Documentación de APIs
Módulo 3: Desarrollo de APIs RESTful
- Configuración del entorno de desarrollo
- Creación de un servidor básico
- Manejo de peticiones y respuestas
- Validación de datos de entrada
- Persistencia y capa de acceso a datos
- Autenticación y autorización
- Manejo de errores
- Pruebas y validación
Módulo 4: Buenas Prácticas y Seguridad
- Buenas prácticas en el diseño de APIs
- Seguridad en APIs RESTful
- OAuth 2.0 y OpenID Connect en la práctica
- Rate limiting y throttling
- CORS y políticas de seguridad
- Caché HTTP y rendimiento
- Observabilidad: logs, métricas y trazas
Módulo 5: Herramientas y Frameworks
- Postman para pruebas de APIs
- Swagger y OpenAPI para documentación
- Frameworks populares para APIs RESTful
- Contratos, mocks y pruebas automatizadas de API
- Integración continua y despliegue
- API gateways y portales de desarrollador
