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

  1. Qué es un recurso y qué es una URI
  2. Sustantivos y no verbos
  3. Colecciones y elementos
  4. Reglas de nombrado
  5. Jerarquía y anidamiento
  6. Recursos singleton
  7. Path parameters frente a query parameters
  8. Diseño de identificadores
  9. Acciones que no son CRUD
  10. Mapa de URIs de Tienda Aroma

  1. 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/factura en 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_001 deja de funcionar porque se ha refactorizado la base de datos, hemos roto el contrato.

  1. 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 /obtenerCafes repite el verbo y, peor, permite la incoherencia POST /obtenerCafes.
  • Se pierde la semántica de HTTP. GET /borrarResena es 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.

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

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

✅ /cafes            /cafes/caf_001
❌ /cafes            /cafe/caf_001
❌ /listaCafes       /cafes/caf_001

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)

✅ /notas-cata          /metodos-pago        /pedidos/ped_5001/nota-regalo
❌ /notasCata           /notas_cata          /NotasCata

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

✅ /cafes/caf_001                  con cabecera Accept: application/json
❌ /cafes/caf_001.json
❌ /cafes/caf_001.xml

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

❌ /api/v1/cafesController/getAll
❌ /v1/tbl_cafes
❌ /v1/cafesDTO

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.

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

✅ /pedidos/ped_5001/lineas
❌ /clientes/cli_842/pedidos/ped_5001/lineas/lin_3/cafe/caf_001

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_moderacion

Reglas 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 self de 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/resenas no necesita repetir cafeId en 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)"]

  1. 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/json
HTTP/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.

  1. 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/etiopia

Dos matices que conviene conocer:

  • Un filtro que devuelve un único elemento sigue siendo una colección. GET /cafes?nombre=Etiopía Yirgacheffe devuelve {"datos": [...], "total": 1}, no el objeto suelto, y devuelve 200 con lista vacía si no hay coincidencias (no 404). 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).

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

  1. Autodescriptivos. Al leer un log o un ticket, ped_5001 se entiende sin contexto. Con 5001 a secas, no sabes de qué es.
  2. Imposible cruzar tipos. Si alguien envía POST /v1/pedidos con {"clienteId": "caf_001"}, el servidor detecta el prefijo equivocado y responde 400 con datos_invalidos, en lugar de crear un pedido incoherente.
  3. 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.

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

POST /v1/resenas/res_101/aprobar
POST /v1/pedidos/ped_5001/pagar
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: aprobacion es un sustantivo y el verbo lo pone POST.
  • 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/pago devuelve el pago realizado, con su fecha y su referencia.
  • Se autoriza por separado: el permiso para crear /aprobacion es distinto del permiso para editar la reseña (04-03).
  • Deja sitio a la idempotencia: al ser un POST bien delimitado, se le puede exigir Idempotency-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.

  1. 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/origenes se 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_5001 obliga 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_001 rompe la caché por URL, complica los enlaces self y no permite subrecursos.
  • Poner .json al final. Mezcla identidad y formato; para eso está Accept.
  • Pluralizar mal. /cafes y /cafe/caf_001 conviviendo 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/anulacion con POST se 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é:

  1. El panel interno quiere ver todas las líneas vendidas del café caf_001 en el último mes.
  2. Aroma Móvil quiere las reseñas escritas por el cliente cli_842.
  3. RápidoEnvíos quiere actualizar el estado del envío del pedido ped_5001.
  4. 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

  1. 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}/lineas no 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 llamado lineas de un segmento fijo. (Alternativa razonable: un recurso de informes /v1/ventas?cafeId=..., si el panel necesita agregados en vez de líneas sueltas.)
  2. GET /v1/resenas?clienteId=cli_842 — sin anidar bajo el café, porque el criterio es el autor. Anidarlo como /clientes/cli_842/resenas tambié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.
  3. PUT /v1/pedidos/ped_5001/envio — anidado y singleton: un envío no existe fuera de su pedido y solo hay uno. PUT porque RápidoEnvíos envía el estado completo del envío en cada actualización (02-03).
  4. 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 el kebab-case en 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

Módulo 2: Diseño de APIs RESTful

Módulo 3: Desarrollo de APIs RESTful

Módulo 4: Buenas Prácticas y Seguridad

Módulo 5: Herramientas y Frameworks

Módulo 6: Casos de Estudio y Proyectos

© Copyright 2026. Todos los derechos reservados