Intención y efectos: más allá del “CRUD”
En una API REST mantenible, el verbo HTTP comunica la intención de la operación y acota sus efectos esperados (qué cambia y qué no). Elegir bien el verbo reduce ambigüedades, facilita reintentos seguros, mejora la observabilidad y evita “endpoints sorpresa” que rompen clientes.
Dos propiedades clave para diseñar con semántica correcta:
- Safe (seguro): no debe modificar estado del servidor (ej.: GET, HEAD).
- Idempotente: repetir la misma petición produce el mismo resultado final (ej.: PUT, DELETE; y PATCH solo si se diseña para serlo).
GET: leer representaciones sin efectos colaterales
Cuándo usarlo
- Recuperar una representación de un recurso o una colección.
- Ejecutar búsquedas/filtrados que no cambian estado.
Buenas prácticas
- No uses GET para acciones que cambian estado (por ejemplo, “/users/123/activate”). Eso rompe cachés, prefetching y reintentos.
- Si la consulta es compleja, sigue siendo GET si es lectura: usa parámetros de query o un recurso de búsqueda (ver sección de procesos asíncronos si la operación es pesada).
Ejemplos
GET /orders/123
Accept: application/jsonGET /orders?status=paid&from=2026-01-01&to=2026-01-31POST: crear en colección o disparar procesamiento no idempotente
Creación en una colección
POST a una colección suele significar: “crea un nuevo elemento bajo esta colección; el servidor decide el identificador”.
POST /orders
Content-Type: application/json
{
"customerId": "c_9",
"items": [{"sku":"A1","qty":2}]
}Respuesta típica:
HTTP/1.1 201 Created
Location: /orders/987
{ "id": "987", "status": "created" }Acciones puntuales (comandos) vs creación
POST también se usa para “procesar” cuando no encaja como actualización de un recurso existente o cuando la operación es inherentemente no idempotente (por ejemplo, “capturar pago” que puede fallar a mitad). Para mantener semántica REST, modela el comando como un subrecurso o como un recurso de operación (job) en vez de un verbo en la ruta.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
- Subrecurso de acción (cuando representa un evento/acción registrable):
POST /orders/987/captures
{ "amount": 120.00 }- Recurso de operación (recomendado para procesos largos):
POST /order-operations
{ "type": "capture", "orderId": "987", "amount": 120.00 }Esto evita rutas tipo /orders/987/capture (verbo en URL) y permite seguimiento del estado.
PUT: reemplazo completo (o upsert) con idempotencia
Intención
PUT significa: “establece el estado del recurso en esta URI a la representación enviada”. Es idempotente: enviar el mismo cuerpo varias veces deja el recurso igual.
Cuándo usarlo
- Actualizar un recurso conocido reemplazando su representación (o la parte que tu API define como representación completa).
- Crear con identificador elegido por el cliente (si tu dominio lo permite): PUT a una URI concreta.
Ejemplos
PUT /profiles/u_123
Content-Type: application/json
{
"displayName": "Ana",
"email": "ana@example.com",
"timezone": "Europe/Madrid"
}Si el recurso no existía y lo creas con PUT, responde 201 Created; si existía y lo reemplazas, 200 OK o 204 No Content.
Regla práctica para evitar ambigüedad
- Si el cliente envía un recurso “completo” según el contrato, usa PUT.
- Si el cliente envía “solo cambios”, usa PATCH.
PATCH: actualización parcial sin sorpresas
Problema que resuelve
PATCH aplica cambios parciales. El riesgo es la ambigüedad: ¿un campo ausente significa “no cambiar” o “borrar”? Con PUT, un campo ausente suele interpretarse como “no está en la representación” (y podría eliminarse). Con PATCH, un campo ausente debe significar “no tocar”.
Cuándo usar PATCH
- Cuando el cliente no puede o no debe enviar la representación completa.
- Cuando quieres minimizar conflictos por concurrencia y tamaño de payload.
Dos enfoques recomendables (elige uno y documenta)
1) JSON Patch (RFC 6902): operaciones explícitas
Ventaja: no hay ambigüedad; puedes add, replace, remove con rutas JSON.
PATCH /profiles/u_123
Content-Type: application/json-patch+json
[
{"op": "replace", "path": "/displayName", "value": "Ana G."},
{"op": "remove", "path": "/timezone"}
]2) Merge Patch (RFC 7396): objeto parcial con semántica de null
Ventaja: simple para clientes. Regla típica: propiedades ausentes = no cambiar; propiedades con null = eliminar/poner a null (según contrato).
PATCH /profiles/u_123
Content-Type: application/merge-patch+json
{
"displayName": "Ana G.",
"timezone": null
}Si usas Merge Patch, define claramente qué significa null para cada campo (borrar, desasociar, o valor permitido).
Cómo diseñar PATCH para ser idempotente (si te importa el reintento)
- Prefiere operaciones deterministas (replace a un valor) sobre operaciones relativas (incrementar).
- Si necesitas “incrementar”, modela un recurso de evento/ajuste (POST) en lugar de PATCH con delta.
Ejemplo no idempotente (evitar si habrá reintentos automáticos):
PATCH /accounts/a1
{ "balanceDelta": 10 }Alternativa mantenible:
POST /accounts/a1/adjustments
{ "amount": 10, "reason": "manual" }DELETE: eliminar el recurso (o marcarlo como eliminado)
Semántica
DELETE indica intención de eliminar. Es idempotente: repetir DELETE sobre el mismo recurso debería dejarlo “no existente” (o “eliminado”) sin efectos adicionales.
Respuestas típicas
204 No Contentsi se elimina y no devuelves cuerpo.200 OKsi devuelves representación del resultado.404 Not Foundo204en eliminaciones repetidas: elige una política consistente (muchas APIs devuelven 404 si el recurso ya no existe; otras prefieren 204 para ocultar existencia).
Borrado lógico
Si por requisitos no puedes borrar físicamente, DELETE puede representar “marcar como eliminado”. Mantén la semántica: tras DELETE, el recurso no debería comportarse como activo en GET estándar (puedes exponer vistas administrativas si aplica).
HEAD y OPTIONS: metadatos y descubrimiento cuando aportan valor
HEAD: igual que GET, sin cuerpo
Útil para:
- Comprobar existencia sin transferir payload.
- Validar caché con
ETag/Last-Modified. - Obtener tamaño aproximado con
Content-Length(si aplica).
HEAD /orders/123Respuesta esperable: mismos headers que GET (cuando sea posible), sin body.
OPTIONS: capacidades del endpoint
OPTIONS puede ayudar en:
- Descubrir métodos permitidos (header
Allow). - Soporte CORS (preflight del navegador).
- Clientes genéricos que negocian capacidades.
OPTIONS /orders/123HTTP/1.1 204 No Content
Allow: GET, PUT, PATCH, DELETE, OPTIONS, HEADNo conviertas OPTIONS en un “endpoint de documentación”; úsalo para capacidades operativas.
Guía práctica paso a paso para elegir el verbo correcto
Paso 1: ¿La operación modifica estado?
- No modifica: usa GET (o HEAD si solo necesitas metadatos).
- Modifica: continúa.
Paso 2: ¿Estás creando un nuevo elemento bajo una colección?
- Sí, el servidor asigna ID: POST a la colección.
- Sí, el cliente define la URI/ID: PUT a la URI del recurso.
- No: continúa.
Paso 3: ¿Reemplazo completo o cambio parcial?
- Reemplazo completo según contrato: PUT.
- Cambio parcial: PATCH (idealmente JSON Patch o Merge Patch).
Paso 4: ¿Es una acción/operación que no encaja como actualización de estado simple?
- Modela un subrecurso de eventos/acciones (POST) o un recurso de operación (job) si es largo o requiere seguimiento.
Paso 5: ¿Necesitas reintentos seguros?
- Prefiere PUT/DELETE (idempotentes) y PATCH diseñado idempotente.
- Si usas POST y habrá reintentos, considera idempotency keys (ver sección de reintentos).
Casos de borde: operaciones masivas (bulk)
Lecturas masivas
Usa GET con filtros/paginación. Si la consulta es pesada y puede tardar, considera un proceso asíncrono (job) en lugar de “GET que tarda minutos”.
Escrituras masivas: patrones comunes
Evita “PATCH a una colección” con semántica difusa. En su lugar, usa un recurso explícito de operación masiva.
| Necesidad | Patrón recomendado | Verbo |
|---|---|---|
| Crear muchos elementos | Crear un lote (batch) como recurso | POST |
| Actualizar muchos elementos | Job de actualización con criterios + cambios | POST |
| Eliminar muchos elementos | Job de borrado con criterios | POST |
Ejemplo: crear un job de actualización masiva:
POST /bulk-jobs
Content-Type: application/json
{
"type": "update",
"target": "orders",
"filter": {"status": "pending"},
"patch": {
"contentType": "application/merge-patch+json",
"document": {"status": "cancelled"}
}
}La respuesta devuelve el job para seguimiento:
HTTP/1.1 202 Accepted
Location: /bulk-jobs/bj_001
{ "id": "bj_001", "state": "queued" }Casos de borde: reintentos, duplicados e idempotencia
Qué puede pasar en producción
- El cliente reintenta por timeout, pero el servidor sí procesó la primera petición.
- Un proxy reenvía una petición.
- Un usuario hace doble clic.
Estrategias por verbo
- PUT/DELETE: diseñados para reintentos; asegúrate de que el resultado final sea estable.
- PATCH: hazlo determinista (replace/remove) o usa precondiciones.
- POST: si crea o ejecuta algo que no debe duplicarse, usa una Idempotency-Key (header) y almacena el resultado asociado durante una ventana de tiempo.
POST /orders
Idempotency-Key: 6f1c2a2b-2b0d-4c2d-9b0d-0f9b2a1c3d77
Content-Type: application/json
{ "customerId": "c_9", "items": [{"sku":"A1","qty":2}] }Regla práctica: si el cliente puede reintentar automáticamente, documenta qué endpoints soportan Idempotency-Key y qué respuestas se “reproducen” ante duplicados.
Procesos asíncronos sin violar semánticas
Cuándo necesitas asincronía
- Operaciones largas (exportaciones, recomputaciones, conciliaciones).
- Operaciones con colas o dependencias externas.
- Operaciones masivas.
Patrón: crear un recurso “job” con POST y consultar con GET
En vez de hacer un GET que dispara trabajo o un POST “/doSomething”, crea un job como recurso. POST expresa “crear una solicitud de procesamiento”.
POST /exports
Content-Type: application/json
{ "resource": "orders", "format": "csv", "filter": {"status":"paid"} }HTTP/1.1 202 Accepted
Location: /exports/ex_77
{ "id": "ex_77", "state": "running" }Luego:
GET /exports/ex_77HTTP/1.1 200 OK
{ "id": "ex_77", "state": "completed", "downloadUrl": "/exports/ex_77/file" }Cancelación y reintentos del job
- Cancelar:
DELETE /exports/ex_77(si semánticamente “eliminar el job” implica cancelarlo) oPATCHpara cambiar estado a cancelled si lo modelas así. - Reintentar: crea un nuevo job (POST) o expón un subrecurso de reintentos (POST /exports/ex_77/retries) si necesitas historial.
Tabla rápida de decisión
| Objetivo | Verbo | Notas de semántica |
|---|---|---|
| Leer recurso/colección | GET | Safe, cacheable |
| Leer solo metadatos | HEAD | Safe, sin body |
| Descubrir métodos/capacidades | OPTIONS | Allow, CORS |
| Crear en colección (ID servidor) | POST | No idempotente por defecto |
| Crear/establecer recurso en URI conocida | PUT | Idempotente |
| Reemplazar representación | PUT | Contrato de “completo” |
| Actualizar parcialmente | PATCH | Evitar ambigüedad (JSON Patch/Merge Patch) |
| Eliminar | DELETE | Idempotente |
| Operación larga/masiva | POST | Crear job + GET para estado |