Ya tenemos el mapa de recursos y sus URIs. Ahora hay que decir qué se puede hacer con cada uno, y en REST eso lo dice el método HTTP. Esta lección recorre en profundidad GET, POST, PUT, PATCH, DELETE, HEAD y OPTIONS aplicados a Tienda Aroma, con petición y respuesta completas de cada uno. Después entra en los dos conceptos que separan una API que aguanta la realidad de una que se rompe en producción: la seguridad (safety) y la idempotencia. No son sutilezas académicas: son la diferencia entre que RápidoEnvíos reintente una petición sin consecuencias o que un cliente acabe pagando dos veces el mismo pedido. Cerraremos con PUT frente a PATCH a fondo, el borrado lógico, las claves de idempotencia y las operaciones en lote.

Contenido

  1. Los métodos de un vistazo
  2. GET: leer sin efectos
  3. POST: crear y ejecutar acciones
  4. PUT: reemplazo total
  5. PATCH: modificación parcial
  6. PUT frente a PATCH, y qué elige Tienda Aroma
  7. DELETE: borrado físico y lógico
  8. HEAD y OPTIONS
  9. Seguridad e idempotencia
  10. Claves de idempotencia para el pago
  11. Operaciones en lote

  1. Los métodos de un vistazo

Método Qué significa ¿Cuerpo en la petición? ¿Cuerpo en la respuesta? Seguro Idempotente
GET Obtener la representación No
HEAD Como GET, solo cabeceras No No
OPTIONS Qué se puede hacer aquí No Opcional
POST Crear subordinado o ejecutar acción No No
PUT Reemplazar por completo Sí (o 204) No
PATCH Modificar parcialmente Sí (o 204) No Depende
DELETE Eliminar No (normalmente) Opcional (o 204) No

Dos métodos más existen y aquí solo los mencionamos: TRACE (eco de la petición, se deshabilita por seguridad) y CONNECT (túneles de proxy). Ninguna API REST los usa.

Si un cliente usa un método que el recurso no admite, la respuesta correcta es 405 Method Not Allowed con la cabecera Allow; si el servidor no conoce el método en absoluto, es 501 Not Implemented. El detalle de los códigos es la lección siguiente.

  1. GET: leer sin efectos

GET obtiene la representación de un recurso. Es el método más usado de cualquier API y el único que se puede cachear con garantías (04-06).

curl -i "https://api.tiendaaroma.example/v1/cafes/caf_001" \
  -H "Accept: application/json"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Language: es
Cache-Control: public, max-age=300

{
  "id": "caf_001",
  "nombre": "Etiopía Yirgacheffe",
  "origen": "Etiopía",
  "tueste": "claro",
  "precioEuros": 14.50,
  "stock": 120,
  "notasCata": ["cítrico", "floral", "té negro"],
  "fechaCreacion": "2026-01-15T08:30:00Z",
  "_links": {
    "self": { "href": "/v1/cafes/caf_001" },
    "resenas": { "href": "/v1/cafes/caf_001/resenas" }
  }
}

Reglas de diseño para GET en Tienda Aroma:

  • Nunca modifica nada. Ni siquiera un contador "discreto" de visitas: los prefetchers de los navegadores, los rastreadores y los proxys hacen GET por su cuenta. Si necesitas registrar la visita, hazlo fuera de la semántica del recurso o con un POST explícito.
  • No lleva cuerpo. Técnicamente HTTP no lo prohíbe, pero muchos intermediarios lo descartan y algunos clientes ni lo envían. Consultas complejas por cuerpo → POST (02-06).
  • Un GET sobre una colección vacía es 200 con {"datos": [], "total": 0}, no 404. La colección existe aunque no tenga elementos.
  • Un GET sobre un elemento inexistente es 404 con el error de negocio correspondiente:
HTTP/1.1 404 Not Found
Content-Type: application/json

{
  "error": {
    "codigo": "cafe_no_encontrado",
    "mensaje": "No existe ningún café con el identificador 'caf_999'.",
    "detalles": []
  }
}

  1. POST: crear y ejecutar acciones

POST es el método de propósito general: crea un recurso subordinado a la colección o ejecuta la acción que representa un subrecurso (02-02).

3.1. Crear un elemento en una colección

curl -i -X POST "https://api.tiendaaroma.example/v1/cafes" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "nombre": "Colombia Huila",
    "origen": "Colombia",
    "tueste": "medio",
    "precioEuros": 12.90,
    "stock": 80,
    "notasCata": ["chocolate", "caramelo", "naranja"]
  }'
HTTP/1.1 201 Created
Content-Type: application/json
Location: https://api.tiendaaroma.example/v1/cafes/caf_002

{
  "id": "caf_002",
  "nombre": "Colombia Huila",
  "origen": "Colombia",
  "tueste": "medio",
  "precioEuros": 12.90,
  "stock": 80,
  "notasCata": ["chocolate", "caramelo", "naranja"],
  "fechaCreacion": "2026-03-14T09:12:00Z",
  "_links": { "self": { "href": "/v1/cafes/caf_002" } }
}

Tres puntos del contrato:

  1. El identificador lo asigna el servidor. El cliente no envía id; si lo envía, se rechaza con 400.
  2. Location es obligatoria en toda creación. Contiene la URI del recurso creado. Es lo que permite a un cliente encadenar operaciones sin adivinar URLs.
  3. Se devuelve el recurso completo en el cuerpo, no solo el id: ahorra un GET inmediato y muestra los campos calculados por el servidor (fechaCreacion).

3.2. Ejecutar una acción

curl -i -X POST "https://api.tiendaaroma.example/v1/pedidos/ped_5001/pago" \
  -H "Authorization: Bearer <token>" \
  -H "Idempotency-Key: 5f3b9c2a-1d7e-4a44-9f30-8b1c2d3e4f50" \
  -H "Content-Type: application/json" \
  -d '{ "metodo": "tarjeta", "tokenTarjeta": "tok_visa_4242" }'
HTTP/1.1 201 Created
Content-Type: application/json
Location: https://api.tiendaaroma.example/v1/pedidos/ped_5001/pago

{
  "estado": "pagado",
  "importeEuros": 29.00,
  "metodo": "tarjeta",
  "referencia": "pay_7712",
  "fechaPago": "2026-03-14T10:32:00Z",
  "_links": {
    "self": { "href": "/v1/pedidos/ped_5001/pago" },
    "pedido": { "href": "/v1/pedidos/ped_5001" },
    "factura": { "href": "/v1/pedidos/ped_5001/factura" }
  }
}

3.3. POST no es idempotente

Es la característica que define a POST y la fuente de la mayoría de los incidentes reales:

# Ejecutado dos veces, crea DOS cafés distintos
curl -X POST .../v1/cafes -d '{"nombre": "Colombia Huila", ...}'   # → caf_002
curl -X POST .../v1/cafes -d '{"nombre": "Colombia Huila", ...}'   # → caf_003

Cuando eso sea inaceptable (pagos, pedidos), hay dos remedios: la cabecera Idempotency-Key (sección 10) o devolver 409 Conflict si detectas un duplicado de negocio. Tienda Aroma usa las dos cosas.

  1. PUT: reemplazo total

PUT dice: "en esta URI debe quedar exactamente esta representación". Es un reemplazo completo, no una fusión.

curl -i -X PUT "https://api.tiendaaroma.example/v1/cafes/caf_001" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "nombre": "Etiopía Yirgacheffe",
    "origen": "Etiopía",
    "tueste": "claro",
    "precioEuros": 15.20,
    "stock": 120,
    "notasCata": ["cítrico", "floral", "té negro"]
  }'
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "caf_001",
  "nombre": "Etiopía Yirgacheffe",
  "origen": "Etiopía",
  "tueste": "claro",
  "precioEuros": 15.20,
  "stock": 120,
  "notasCata": ["cítrico", "floral", "té negro"],
  "fechaCreacion": "2026-01-15T08:30:00Z"
}

El peligro de PUT: si el cliente quiere subir el precio y envía solo {"precioEuros": 15.20}, un PUT correcto deja el café sin nombre, sin origen, sin tueste, sin stock y sin notas de cata. Es el error de novato más caro de esta lección. Tienda Aroma lo mitiga rechazando con 400 los PUT a los que les falten campos obligatorios, pero la semántica sigue siendo "reemplaza todo".

PUT sí es idempotente: enviarlo diez veces deja el recurso exactamente igual que enviarlo una vez. Por eso es el método que usa RápidoEnvíos para actualizar el envío, cuyos reintentos son frecuentes:

PUT /v1/pedidos/ped_5001/envio HTTP/1.1
Content-Type: application/json
Authorization: Bearer <token de RápidoEnvíos>

{
  "estado": "en_reparto",
  "codigoSeguimiento": "RE-9981234",
  "fechaEstimada": "2026-03-16T12:00:00Z"
}

Un detalle poco conocido: PUT también puede crear un recurso si el cliente conoce la URI de antemano. Tienda Aroma lo usa exactamente en un sitio, las líneas de carrito:

PUT /v1/carritos/car_77/lineas/caf_002 HTTP/1.1
Content-Type: application/json

{ "cantidad": 3 }

Si la línea no existía se crea (201 Created); si existía se sustituye la cantidad (200 OK). Y es idempotente: pulsar tres veces "poner 3 unidades" deja 3 unidades, no 9. Compáralo con POST /lineas con {"cafeId": "caf_002", "cantidad": 1}, que suma una unidad cada vez que se ejecuta: las dos operaciones tienen sentido, pero significan cosas distintas y conviene tenerlo escrito en la documentación.

  1. PATCH: modificación parcial

PATCH envía solo lo que cambia. La pregunta interesante es en qué formato, porque PATCH no define uno: lo define el Content-Type.

5.1. JSON Merge Patch (RFC 7386)

El documento enviado se fusiona con el recurso. Un valor null significa "borra este campo".

PATCH /v1/cafes/caf_001 HTTP/1.1
Content-Type: application/merge-patch+json

{
  "precioEuros": 15.20,
  "stock": 95
}

Resultado: se cambian precioEuros y stock; todo lo demás se queda como estaba.

PATCH /v1/cafes/caf_001 HTTP/1.1
Content-Type: application/merge-patch+json

{ "notasCata": null }

Resultado: se elimina el campo notasCata.

La limitación de Merge Patch son los arrays: se reemplazan enteros, nunca se editan por posición. Para añadir una nota de cata hay que enviar la lista completa:

{ "notasCata": ["cítrico", "floral", "té negro", "bergamota"] }

Y el corolario incómodo: con Merge Patch no se puede poner un campo a null de verdad, porque null está reservado para "borrar".

5.2. JSON Patch (RFC 6902)

Es una lista de operaciones sobre el documento, expresadas con JSON Pointer.

PATCH /v1/cafes/caf_001 HTTP/1.1
Content-Type: application/json-patch+json

[
  { "op": "replace", "path": "/precioEuros", "value": 15.20 },
  { "op": "add",     "path": "/notasCata/-", "value": "bergamota" },
  { "op": "remove",  "path": "/notasCata/0" },
  { "op": "test",    "path": "/stock", "value": 120 }
]

Las operaciones son add, remove, replace, move, copy y test. path usa JSON Pointer (/notasCata/- significa "al final del array"). Si cualquier operación falla, no se aplica ninguna: es atómico.

La operación test es la joya escondida: "aplica esto solo si stock sigue valiendo 120". Es control de concurrencia optimista dentro del propio cuerpo, complementario al que se hace con If-Match y ETags (04-06).

5.3. Comparación

Criterio JSON Merge Patch (7386) JSON Patch (6902)
Content-Type application/merge-patch+json application/json-patch+json
Legibilidad Muy alta: parece el recurso Baja: hay que leer operaciones
Editar un elemento de array No (se reemplaza el array) Sí, por índice
Poner un campo a null Imposible (null = borrar) Sí (replace con value: null)
Condiciones previas No Sí, con test
Reordenar, mover, copiar No
Idempotencia Sí, siempre No siempre (add a /array/- acumula)
Facilidad para el cliente Muy alta Media
Adopción Mayoritaria Nichos (documentos complejos, Kubernetes)

5.4. La decisión de Tienda Aroma

JSON Merge Patch como formato oficial, con Content-Type: application/merge-patch+json. Razones: los recursos son planos y pequeños, los clientes son mayoritariamente frontends que ya tienen el objeto en memoria, la legibilidad de las peticiones facilita el soporte, y el 100 % de los casos de uso reales son "cambiar dos o tres campos". Los arrays de Tienda Aroma (notasCata, lineas) son cortos y reemplazarlos enteros no es un problema.

Además, se acepta application/json a secas y se trata como Merge Patch, porque es lo que envían muchas herramientas por defecto; un Content-Type distinto de esos dos se rechaza con 415 Unsupported Media Type. Que la API no admite JSON Patch queda escrito en la documentación, para que nadie lo intente.

  1. PUT frente a PATCH, y qué elige Tienda Aroma

Criterio PUT PATCH
Semántica Reemplaza el recurso completo Aplica cambios parciales
Campos ausentes Se borran o se ponen por defecto Se mantienen
Tamaño del cuerpo Todo el recurso Solo lo que cambia
Idempotente Siempre Con Merge Patch, sí
Riesgo de pisar cambios de otro Alto: envías campos que no querías tocar Bajo: solo tocas lo tuyo
Puede crear el recurso No (404 si no existe)
Uso típico Formularios completos, sincronización Ediciones puntuales

Asignación en el mapa de Tienda Aroma:

Recurso Método de actualización Motivo
/cafes/{id} PUT y PATCH El panel edita fichas completas; los ajustes de precio y stock son parciales
/clientes/{id} Solo PATCH Nunca se reemplaza un cliente entero: hay campos que el cliente no ve
/clientes/{id}/preferencias Solo PUT Son cuatro campos y el formulario los envía todos
/carritos/{id}/lineas/{cafeId} Solo PUT Fijar la cantidad debe ser idempotente
/pedidos/{id} Solo PATCH Solo hay campos muy concretos editables (dirección antes del envío)
/pedidos/{id}/envio Solo PUT RápidoEnvíos manda el estado completo y reintenta
/resenas/{id} Solo PATCH Se corrige el comentario; el estado se cambia con /aprobacion o /rechazo

  1. DELETE: borrado físico y lógico

curl -i -X DELETE "https://api.tiendaaroma.example/v1/carritos/car_77/lineas/caf_002" \
  -H "Authorization: Bearer <token>"
HTTP/1.1 204 No Content

7.1. Físico frente a lógico

Borrado físico Borrado lógico (soft delete)
Qué hace Elimina la fila Marca eliminado = true y la oculta
Reversible No
Auditoría e histórico Se pierden Se conservan
Integridad referencial Puede romper pedidos antiguos Intacta
Coste Ninguno Filtrar en todas las consultas, crecimiento de tablas

Tienda Aroma usa borrado lógico para los cafés (un pedido de 2025 debe poder seguir mostrando el café que se vendió) y para los clientes (obligaciones fiscales), y borrado físico para las líneas de carrito (datos efímeros sin valor histórico). Los pedidos no se borran nunca: se anulan con POST /pedidos/{id}/anulacion.

Punto clave: el borrado lógico es invisible en el contrato. Desde fuera, tras un DELETE /v1/cafes/caf_001, el café ya no aparece en /v1/cafes y GET /v1/cafes/caf_001 responde 404 (o 410, ver más abajo). Que por dentro la fila siga ahí es asunto de la capa de persistencia (03-05).

7.2. ¿Y si borro dos veces?

Aquí hay un debate clásico. Segundo DELETE sobre un recurso ya borrado:

  • 404 Not Found: literal. El recurso no existe ahora, y es la respuesta más común.
  • 204 No Content: pragmática. El objetivo del cliente ("que no exista") ya se cumple.

Ninguna de las dos rompe la idempotencia, y conviene entender bien por qué: idempotencia significa que el efecto sobre el servidor es el mismo tras una o N peticiones, no que el código de respuesta sea idéntico. Tras uno o cinco DELETE, el recurso está borrado: eso es idempotente.

Decisión de Tienda Aroma: 404, porque distingue "lo he borrado yo ahora" de "esto ya no estaba", información útil para depurar clientes con reintentos. Y para los cafés retirados del catálogo definitivamente se usa 410 Gone, que dice "existió y no volverá" (02-04).

DELETE no debe llevar cuerpo de petición. Si necesitas parámetros para borrar (un motivo, una fecha efectiva), es señal de que estás ante una acción, y las acciones se modelan como subrecurso con POST (02-02).

  1. HEAD y OPTIONS

8.1. HEAD

Idéntico a GET, pero el servidor devuelve solo las cabeceras. Sirve para comprobar existencia, tamaño o frescura sin descargar el cuerpo.

curl -I "https://api.tiendaaroma.example/v1/pedidos/ped_5001/factura"
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 48213
Last-Modified: Sat, 14 Mar 2026 10:33:00 GMT

Aroma Móvil lo usa para saber si merece la pena descargar una factura de 48 KB con la conexión actual. La regla de oro: HEAD debe devolver exactamente las mismas cabeceras que devolvería GET; si tu HEAD devuelve Content-Length: 0, está mal implementado.

8.2. OPTIONS

Pregunta qué se puede hacer con un recurso. La respuesta obligatoria es la cabecera Allow.

curl -i -X OPTIONS "https://api.tiendaaroma.example/v1/cafes/caf_001"
HTTP/1.1 204 No Content
Allow: GET, HEAD, PUT, PATCH, DELETE, OPTIONS
Accept-Patch: application/merge-patch+json

Accept-Patch es la forma estándar de anunciar qué formatos de PATCH acepta el recurso: aquí queda publicada, en el propio protocolo, la decisión de la sección 5.4.

El uso masivo de OPTIONS en la práctica no lo hacen las personas, sino los navegadores: es la petición de comprobación previa (preflight) de CORS, que la SPA de Tienda Aroma dispara antes de cada PATCH o DELETE con cabeceras personalizadas. Ese mecanismo completo se estudia en 04-05.

  1. Seguridad e idempotencia

Las dos propiedades más importantes de esta lección.

  • Seguro (safe): el método no modifica el estado del servidor. Es una lectura. Cualquier intermediario puede repetirlo, precargarlo o cachearlo sin pedir permiso.
  • Idempotente: ejecutarlo N veces deja el servidor en el mismo estado que ejecutarlo una vez. Ojo: el mismo estado, no la misma respuesta.
graph TD
    A["Cliente envía POST /pedidos/ped_5001/pago"] --> B["El servidor lo recibe<br/>y cobra 29,00 €"]
    B --> C["La respuesta se pierde:<br/>timeout de red"]
    C --> D{"¿El cliente reintenta?"}
    D -->|"Sin Idempotency-Key"| E["Segundo cobro de 29,00 €<br/><b>cliente cobrado dos veces</b>"]
    D -->|"Con Idempotency-Key"| F["El servidor reconoce la clave<br/>y devuelve la respuesta original<br/><b>un solo cobro</b>"]

Todo método seguro es idempotente; lo contrario no es cierto (DELETE es idempotente pero no seguro).

Por qué importa de verdad, con los tres escenarios de Tienda Aroma:

  1. Reintentos de RápidoEnvíos. Su cliente HTTP reintenta automáticamente ante 503 o timeout. Como actualiza el envío con PUT (idempotente), tres reintentos dejan el mismo envío. Si lo hubiéramos modelado como POST /pedidos/{id}/eventos-envio, tendríamos tres eventos duplicados.
  2. Timeouts de red en móvil. Aroma Móvil pierde cobertura justo después de enviar la petición. El cliente no sabe si el servidor la procesó. Solo puede reintentar sin miedo si el método es idempotente.
  3. Doble clic en "pagar". El caso más humano de todos. POST no es idempotente, así que el remedio no puede venir del método: viene de la sección siguiente.

Consecuencia de diseño: cuanto más idempotente sea tu API, más barato es operarla, porque los reintentos automáticos (del cliente, del balanceador, del gateway) dejan de ser peligrosos.

  1. Claves de idempotencia para el pago

Una clave de idempotencia es un identificador único que el cliente genera antes de enviar la petición y repite en cada reintento de esa misma operación lógica.

Flujo completo de /pedidos/ped_5001/pago:

# El cliente genera un UUID y lo guarda ANTES de enviar nada
CLAVE="5f3b9c2a-1d7e-4a44-9f30-8b1c2d3e4f50"

curl -i -X POST "https://api.tiendaaroma.example/v1/pedidos/ped_5001/pago" \
  -H "Authorization: Bearer <token>" \
  -H "Idempotency-Key: $CLAVE" \
  -H "Content-Type: application/json" \
  -d '{ "metodo": "tarjeta", "tokenTarjeta": "tok_visa_4242" }'

Lo que hace el servidor:

  1. Busca la clave. Si no existe, la registra junto con una huella del cuerpo, procesa el pago y guarda la respuesta durante 24 horas.
  2. Si existe y el cuerpo coincide: no vuelve a cobrar; devuelve la respuesta guardada, con Idempotent-Replay: true para que el cliente sepa que es una repetición.
  3. Si existe y el cuerpo es distinto: responde 422 Unprocessable Content con código clave_idempotencia_reutilizada. Es una protección contra errores del cliente: la misma clave no puede significar dos operaciones distintas.
  4. Si la petición original todavía se está procesando: responde 409 Conflict con operacion_en_curso y Retry-After: 2.

Respuesta del reintento:

HTTP/1.1 201 Created
Content-Type: application/json
Location: https://api.tiendaaroma.example/v1/pedidos/ped_5001/pago
Idempotent-Replay: true

{
  "estado": "pagado",
  "importeEuros": 29.00,
  "referencia": "pay_7712",
  "fechaPago": "2026-03-14T10:32:00Z"
}

Contrato de Tienda Aroma sobre Idempotency-Key:

Endpoint ¿Clave? Comportamiento sin clave
POST /pedidos Obligatoria 400 con clave_idempotencia_requerida
POST /pedidos/{id}/pago Obligatoria 400 con clave_idempotencia_requerida
POST /pedidos/{id}/anulacion Recomendada Se procesa; si ya está anulado, 409 pedido_ya_anulado
POST /cafes Opcional Se procesa (podría crear duplicados)
POST /cafes/{id}/resenas Opcional Se procesa

Y una segunda línea de defensa que no depende del cliente: el propio recurso protege su transición. Un segundo pago del mismo pedido, aunque venga con otra clave, encuentra el pedido en estado pagado y responde 409 Conflict con pedido_ya_pagado. La idempotencia protege de los reintentos técnicos; la máquina de estados protege de los errores lógicos. Hacen falta las dos.

  1. Operaciones en lote

Antes o después alguien pedirá "actualizar el stock de 200 cafés de una vez". Las opciones:

a) N peticiones individuales. Semánticamente perfecto, fácil de cachear y de reintentar. Con HTTP/2 y conexiones multiplexadas, 200 PATCH pequeños son mucho más viables de lo que la gente supone.

b) Un endpoint de lote.

POST /v1/cafes/actualizaciones-lote HTTP/1.1
Content-Type: application/json

{
  "operaciones": [
    { "id": "caf_001", "cambios": { "stock": 95 } },
    { "id": "caf_002", "cambios": { "stock": 0 } },
    { "id": "caf_999", "cambios": { "stock": 10 } }
  ]
}

Y aquí aparece el problema fundamental del lote: ¿qué código devuelves si una de las tres falla? No es 200, porque falló algo. No es 400, porque dos funcionaron. La respuesta habitual es 207 Multi-Status, un código que viene de WebDAV, con el detalle por elemento:

HTTP/1.1 207 Multi-Status
Content-Type: application/json

{
  "datos": [
    { "id": "caf_001", "estado": 200 },
    { "id": "caf_002", "estado": 200 },
    { "id": "caf_999", "estado": 404,
      "error": { "codigo": "cafe_no_encontrado", "mensaje": "No existe 'caf_999'.", "detalles": [] } }
  ],
  "total": 3
}

Riesgos que hay que tener presentes antes de aceptar un endpoint de lote:

  • Atomicidad ambigua: ¿es todo o nada, o parcial? Hay que decidirlo y documentarlo; ambas opciones son defendibles y la confusión es la que hace daño.
  • Se pierde la caché y el Location: es un POST opaco a un endpoint artificial.
  • Idempotencia complicada: reintentar el lote tras un fallo parcial requiere clave de idempotencia y saber qué se aplicó.
  • Timeouts y límites: un lote de 10.000 elementos tumba la petición; hay que fijar un máximo (Tienda Aroma: 100 operaciones) y responder 413 si se supera.
  • Verbo encubierto: actualizaciones-lote es un recurso inventado que no existe en el dominio. Es una concesión consciente, no un patrón a extender.

Decisión de Tienda Aroma: no hay endpoints de lote en la v1. El panel interno actualiza en paralelo con peticiones individuales. Si el volumen lo exige, se añadirá un solo endpoint de lote para stock, con las reglas anteriores escritas en la guía de estilo.

Errores Comunes y Consejos

  • Usar GET para operaciones que modifican. GET /borrar?id=res_101 es un desastre esperando a un rastreador. Nunca.
  • Usar POST para todo. Funciona, pero renuncias a la caché, a los reintentos seguros y a la mitad de la semántica de HTTP: es el nivel 1 de Richardson que ya rechazamos en 01-05.
  • Enviar un PUT parcial. El error clásico que borra medio recurso. Si vas a mandar tres campos, usa PATCH.
  • Devolver 200 al crear. La creación es 201 con Location. Un 200 obliga al cliente a rebuscar el id en el cuerpo.
  • Implementar PATCH sin decidir el formato. Sin Content-Type explícito, cada cliente supondrá algo distinto. Documenta application/merge-patch+json y rechaza el resto con 415.
  • Creer que idempotencia es "devuelve lo mismo". Es "deja el servidor igual". Un segundo DELETE puede responder 404 y seguir siendo idempotente.
  • Poner cuerpo en GET o en DELETE. Algunos intermediarios lo descartan silenciosamente y depurarlo es una pesadilla.
  • Consejo: pregunta siempre "¿qué pasa si esto se envía dos veces?". Aplícalo a cada endpoint nuevo antes de darlo por cerrado. Es la pregunta que más incidentes evita.
  • Consejo: la clave de idempotencia la genera el cliente antes de enviar, no después. Si se genera en el reintento, ya no sirve para nada.

Ejercicios

Ejercicio 1: elegir el método

Indica método, URI y por qué, para cada necesidad de Tienda Aroma:

  1. Aroma Móvil quiere saber si la factura de ped_5001 ya está disponible, sin descargarla.
  2. El panel corrige una errata en el nombre de caf_002.
  3. La SPA fija a 3 unidades la cantidad de caf_002 en el carrito car_77.
  4. Un moderador rechaza la reseña res_102 indicando el motivo.
  5. RápidoEnvíos comunica que ped_5001 ha salido a reparto.
  6. El panel retira caf_001 del catálogo conservando el histórico.
  7. Un cliente confirma su carrito y crea un pedido.

Ejercicio 2: PUT frente a PATCH

Este es el estado actual de caf_001:

{
  "id": "caf_001",
  "nombre": "Etiopía Yirgacheffe",
  "origen": "Etiopía",
  "tueste": "claro",
  "precioEuros": 14.50,
  "stock": 120,
  "notasCata": ["cítrico", "floral", "té negro"]
}

a) ¿Qué queda tras PUT /v1/cafes/caf_001 con cuerpo {"precioEuros": 15.20}, si el servidor no valida campos obligatorios? b) Escribe el PATCH con Merge Patch que suba el precio a 15,20 € y baje el stock a 95. c) Escribe el PATCH con Merge Patch que elimine el campo notasCata. d) Escribe el JSON Patch que añada la nota "bergamota" solo si el stock sigue siendo 120, y explica por qué eso no se puede hacer con Merge Patch.

Ejercicio 3: diseñar la idempotencia de una devolución

Tienda Aroma añade POST /v1/pedidos/{id}/devolucion, que genera una etiqueta de retorno y reembolsa el importe. Diseña su comportamiento respondiendo a:

  1. ¿Debe exigir Idempotency-Key? ¿Por qué?
  2. ¿Qué ocurre si llega dos veces la misma clave con el mismo cuerpo?
  3. ¿Qué ocurre si llega una devolución de un pedido que aún no se ha enviado?
  4. ¿Qué ocurre si llega una segunda devolución, con clave distinta, de un pedido ya devuelto?
  5. ¿Sería idempotente modelarlo como PUT /v1/pedidos/{id}/devolucion? ¿Qué se ganaría y qué se perdería?

Soluciones

Solución 1

# Método y URI Justificación
1 HEAD /v1/pedidos/ped_5001/factura Comprueba existencia y tamaño sin gastar datos: exactamente para lo que existe HEAD
2 PATCH /v1/cafes/caf_002 con {"nombre": "..."} Cambio parcial; con PUT habría que reenviar toda la ficha y arriesgarse a pisar otros campos
3 PUT /v1/carritos/car_77/lineas/caf_002 con {"cantidad": 3} "Fijar la cantidad" es reemplazo e idempotente; con POST se sumaría cada vez
4 POST /v1/resenas/res_102/rechazo con {"motivo": "..."} Acción con parámetros propios, modelada como subrecurso (02-02)
5 PUT /v1/pedidos/ped_5001/envio con el estado completo Singleton actualizado por un socio que reintenta: la idempotencia de PUT es imprescindible
6 DELETE /v1/cafes/caf_001 Borrado lógico por dentro; desde fuera el café desaparece del catálogo
7 POST /v1/pedidos con Idempotency-Key Creación en la colección, no idempotente por naturaleza: la clave evita pedidos duplicados

Solución 2

a) Un PUT literal reemplaza el recurso completo, así que quedaría:

{ "id": "caf_001", "precioEuros": 15.20 }

Sin nombre, sin origen, sin tueste, sin stock y sin notas de cata. El id sobrevive porque forma parte de la identidad, no del contenido enviado. Por eso Tienda Aroma valida los campos obligatorios y devuelve 400 datos_invalidos en lugar de destruir el recurso.

b)

PATCH /v1/cafes/caf_001 HTTP/1.1
Content-Type: application/merge-patch+json

{ "precioEuros": 15.20, "stock": 95 }

c)

PATCH /v1/cafes/caf_001 HTTP/1.1
Content-Type: application/merge-patch+json

{ "notasCata": null }

d)

PATCH /v1/cafes/caf_001 HTTP/1.1
Content-Type: application/json-patch+json

[
  { "op": "test", "path": "/stock", "value": 120 },
  { "op": "add",  "path": "/notasCata/-", "value": "bergamota" }
]

Con Merge Patch es imposible por dos razones acumuladas: no existe ninguna operación condicional (test), y los arrays se reemplazan enteros, así que "añadir al final" obliga a enviar la lista completa —lo que, además, pisaría cualquier nota añadida por otro usuario entre la lectura y la escritura. La alternativa de Tienda Aroma para el caso condicional no es JSON Patch, sino If-Match con ETag (04-06).

Solución 3

  1. Sí, obligatoria. Mueve dinero: un reintento por timeout no puede provocar dos reembolsos. Mismo criterio que /pago.
  2. No se reembolsa dos veces. El servidor devuelve la respuesta guardada de la primera ejecución con Idempotent-Replay: true y el mismo 201 y Location.
  3. 409 Conflict con un código de negocio nuevo, pedido_no_enviado: el pedido existe y la petición está bien formada, pero su estado actual no permite la transición. No es 400 (los datos son válidos) ni 404 (el pedido existe).
  4. 409 Conflict con pedido_ya_devuelto. La clave nueva no ayuda: es la máquina de estados del recurso la que rechaza la segunda transición. Es el ejemplo de por qué hacen falta las dos defensas de la sección 10.
  5. Sí sería idempotente, y ese es su atractivo: PUT /devolucion significaría "quiero que exista esta devolución con estos datos", y repetirlo dejaría el mismo estado. Se ganaría idempotencia sin cabecera adicional. Se perdería, en cambio, la semántica de acción con efectos (un PUT sugiere que se escribe un dato, no que se ejecuta un reembolso), la posibilidad de que el servidor asigne datos propios de la operación (referencia del reembolso, fecha) y la coherencia con /pago, /anulacion y /aprobacion, que ya usan POST. Tienda Aroma prioriza la coherencia: POST con clave de idempotencia.

Conclusión

Los métodos HTTP son el vocabulario de verbos de tu API y usarlos bien es lo que separa el nivel 1 del nivel 2 de Richardson. GET lee sin efectos y se puede cachear; POST crea y ejecuta acciones, y no es idempotente; PUT reemplaza por completo y sí lo es; PATCH modifica lo justo —en Tienda Aroma con JSON Merge Patch y Content-Type: application/merge-patch+json—; DELETE elimina, aunque por dentro sea un borrado lógico; y HEAD y OPTIONS dan información sin transferir el recurso. Por encima de todo quedan la seguridad y la idempotencia, que dejan de ser teoría en cuanto hay reintentos, timeouts o un doble clic: por eso el pago y la creación de pedidos exigen Idempotency-Key y, además, la máquina de estados del recurso rechaza las transiciones imposibles.

Justamente ahí hemos ido dejando cabos sueltos: 201 con Location, 204 sin cuerpo, 404 frente a 410, 409 cuando la transición no es posible, 415 cuando el Content-Type no se admite, 405 cuando el método no está permitido. Cada uno de esos números es una decisión de contrato. En la lección siguiente, 02-04 Códigos de estado HTTP, los recorreremos todos con criterio, construiremos un árbol de decisión para elegir el correcto, diseñaremos el cuerpo del error de Tienda Aroma frente al estándar application/problem+json y cerraremos el catálogo de códigos de error de negocio.

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