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
- Los métodos de un vistazo
- GET: leer sin efectos
- POST: crear y ejecutar acciones
- PUT: reemplazo total
- PATCH: modificación parcial
- PUT frente a PATCH, y qué elige Tienda Aroma
- DELETE: borrado físico y lógico
- HEAD y OPTIONS
- Seguridad e idempotencia
- Claves de idempotencia para el pago
- Operaciones en lote
- 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 | Sí | Sí | Sí |
HEAD |
Como GET, solo cabeceras | No | No | Sí | Sí |
OPTIONS |
Qué se puede hacer aquí | No | Opcional | Sí | Sí |
POST |
Crear subordinado o ejecutar acción | Sí | Sí | No | No |
PUT |
Reemplazar por completo | Sí | Sí (o 204) | No | Sí |
PATCH |
Modificar parcialmente | Sí | Sí (o 204) | No | Depende |
DELETE |
Eliminar | No (normalmente) | Opcional (o 204) | No | Sí |
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.
- 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).
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
GETpor su cuenta. Si necesitas registrar la visita, hazlo fuera de la semántica del recurso o con unPOSTexplí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
GETsobre una colección vacía es200con{"datos": [], "total": 0}, no404. La colección existe aunque no tenga elementos. - Un
GETsobre un elemento inexistente es404con 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": []
}
}
- 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:
- El identificador lo asigna el servidor. El cliente no envía
id; si lo envía, se rechaza con400. Locationes obligatoria en toda creación. Contiene la URI del recurso creado. Es lo que permite a un cliente encadenar operaciones sin adivinar URLs.- Se devuelve el recurso completo en el cuerpo, no solo el id: ahorra un
GETinmediato 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_003Cuando 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.
- 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:
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.
- 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.
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:
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 | Sí |
| 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.
- 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 | Sí | 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 |
- 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>"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 | Sí |
| 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).
- 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.
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Length: 48213
Last-Modified: Sat, 14 Mar 2026 10:33:00 GMTAroma 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.
HTTP/1.1 204 No Content
Allow: GET, HEAD, PUT, PATCH, DELETE, OPTIONS
Accept-Patch: application/merge-patch+jsonAccept-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.
- 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:
- Reintentos de RápidoEnvíos. Su cliente HTTP reintenta automáticamente ante
503o timeout. Como actualiza el envío conPUT(idempotente), tres reintentos dejan el mismo envío. Si lo hubiéramos modelado comoPOST /pedidos/{id}/eventos-envio, tendríamos tres eventos duplicados. - 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.
- Doble clic en "pagar". El caso más humano de todos.
POSTno 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.
- 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:
- Busca la clave. Si no existe, la registra junto con una huella del cuerpo, procesa el pago y guarda la respuesta durante 24 horas.
- Si existe y el cuerpo coincide: no vuelve a cobrar; devuelve la respuesta guardada, con
Idempotent-Replay: truepara que el cliente sepa que es una repetición. - Si existe y el cuerpo es distinto: responde
422 Unprocessable Contentcon códigoclave_idempotencia_reutilizada. Es una protección contra errores del cliente: la misma clave no puede significar dos operaciones distintas. - Si la petición original todavía se está procesando: responde
409 Conflictconoperacion_en_cursoyRetry-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.
- 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 unPOSTopaco 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
413si se supera. - Verbo encubierto:
actualizaciones-lotees 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
GETpara operaciones que modifican.GET /borrar?id=res_101es un desastre esperando a un rastreador. Nunca. - Usar
POSTpara 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
PUTparcial. El error clásico que borra medio recurso. Si vas a mandar tres campos, usaPATCH. - Devolver
200al crear. La creación es201conLocation. Un200obliga al cliente a rebuscar el id en el cuerpo. - Implementar
PATCHsin decidir el formato. SinContent-Typeexplícito, cada cliente supondrá algo distinto. Documentaapplication/merge-patch+jsony rechaza el resto con415. - Creer que idempotencia es "devuelve lo mismo". Es "deja el servidor igual". Un segundo
DELETEpuede responder404y seguir siendo idempotente. - Poner cuerpo en
GETo enDELETE. 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:
- Aroma Móvil quiere saber si la factura de
ped_5001ya está disponible, sin descargarla. - El panel corrige una errata en el nombre de
caf_002. - La SPA fija a 3 unidades la cantidad de
caf_002en el carritocar_77. - Un moderador rechaza la reseña
res_102indicando el motivo. - RápidoEnvíos comunica que
ped_5001ha salido a reparto. - El panel retira
caf_001del catálogo conservando el histórico. - 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:
- ¿Debe exigir
Idempotency-Key? ¿Por qué? - ¿Qué ocurre si llega dos veces la misma clave con el mismo cuerpo?
- ¿Qué ocurre si llega una devolución de un pedido que aún no se ha enviado?
- ¿Qué ocurre si llega una segunda devolución, con clave distinta, de un pedido ya devuelto?
- ¿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:
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)
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
- Sí, obligatoria. Mueve dinero: un reintento por timeout no puede provocar dos reembolsos. Mismo criterio que
/pago. - No se reembolsa dos veces. El servidor devuelve la respuesta guardada de la primera ejecución con
Idempotent-Replay: truey el mismo201yLocation. 409 Conflictcon 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 es400(los datos son válidos) ni404(el pedido existe).409 Conflictconpedido_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.- Sí sería idempotente, y ese es su atractivo:
PUT /devolucionsignificarí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 (unPUTsugiere 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,/anulaciony/aprobacion, que ya usanPOST. Tienda Aroma prioriza la coherencia:POSTcon 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
- ¿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
