Playbooks

API Design

Salir

Convenciones REST

Una API consistente se aprende una vez y se usa para siempre. Cada endpoint debería ser predecible sin necesidad de leer documentación.

Verbos HTTP

GET Leer recurso(s). Sin side effects. Idempotente.
GET /api/v1/users 200 — recurso(s)
POST Crear recurso. No idempotente.
POST /api/v1/users 201 — recurso creado
PUT Reemplazar recurso completo. Idempotente.
PUT /api/v1/users/42 200 — recurso actualizado
PATCH Actualizar parcial. Idempotente.
PATCH /api/v1/users/42 200 — recurso actualizado
DELETE Eliminar recurso. Idempotente.
DELETE /api/v1/users/42 204 — sin contenido

Estructura de URLs

Recursos en plural, en inglés
✅ /api/v1/users ❌ /api/v1/user /api/v1/usuarios
Jerarquía lógica, máximo 2 niveles
✅ /api/v1/users/42/orders ❌ /api/v1/users/42/orders/17/items/3
kebab-case para recursos compuestos
✅ /api/v1/payment-methods ❌ /api/v1/paymentMethods /api/v1/payment_methods
Sin verbos en la URL — el método HTTP ya es el verbo
✅ DELETE /api/v1/users/42 ❌ POST /api/v1/users/42/delete
Sub-recursos para relaciones, no query params
✅ /api/v1/users/42/orders ❌ /api/v1/orders?userId=42
Query params para filtros, orden, paginación
✅ /api/v1/orders?status=paid&sort=-createdAt ❌ /api/v1/orders/paid/desc

Acciones no-CRUD

Acciones no-CRUD como sub-recurso
✅ POST /api/v1/orders/83/cancel ❌ POST /api/v1/cancel-order/83
Búsquedas complejas con POST + body
✅ POST /api/v1/users/search { "query": {...} } ❌ GET /api/v1/users?q[name][like]=...

Formato de respuesta estándar

Toda respuesta sigue el mismo envelope. El cliente nunca tiene que adivinar si la respuesta es exitosa o un error — la estructura se lo dice.

Respuesta exitosa (recurso único)

{
  "data": { ... },
  "meta": {
    "requestId": "req_abc123",
    "timestamp": "2026-05-14T10:30:00Z"
  }
}

Respuesta exitosa (lista)

{
  "data": [ ... ],
  "meta": {
    "page": 1,
    "perPage": 20,
    "total": 142,
    "totalPages": 8,
    "requestId": "req_abc123"
  }
}

Respuesta de error

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "El campo email es requerido.",
    "details": [
      { "field": "email", "reason": "required" }
    ]
  },
  "meta": {
    "requestId": "req_abc123",
    "timestamp": "2026-05-14T10:30:00Z"
  }
}

Reglas del envelope

1 Siempre usá el envelope: respuesta exitosa bajo data, errores bajo error.
2 Incluí requestId en meta para tracear requests en logs. UUID generado en el primer middleware.
3 Listas siempre con meta de paginación: page, perPage, total, totalPages.
4 Errores de validación incluyen array details con cada campo que falló y por qué.
5 Nunca expongas stack traces ni errores internos en producción. En desarrollo, bajo un flag ?debug=1.
6 Timestamps en ISO 8601 UTC. Nada de formatos locales ni relativos ("hace 3 horas").

Antes vs Después

// ❌ Mal — respuestas inconsistentes
GET /users/42    → { "id": 42, "name": "Ana" }
GET /users/99    → { "error": "No encontrado" }
POST /users      → { "success": true, "user": {...} }

// ✅ Bien — envelope consistente
GET /users/42    → { "data": { "id": 42, "name": "Ana" }, "meta": {...} }
GET /users/99    → { "error": { "code": "NOT_FOUND", ... }, "meta": {...} }
POST /users      → { "data": { "id": 43, "name": "Luis" }, "meta": {...} }

Status Codes & Errores

El status code correcto + el error code correcto = el cliente puede automatizar su respuesta sin intervención humana.

Cheat sheet de HTTP status codes

200 OK
GET, PUT, PATCH exitosos
201 Created
POST exitoso. Devolvé el recurso creado + header Location
204 No Content
DELETE exitoso. Body vacío.
400 Bad Request
Request malformado, JSON inválido, campo requerido faltante
401 Unauthorized
Token ausente, expirado o inválido
403 Forbidden
Token válido pero sin permisos para este recurso
404 Not Found
Recurso no existe. No confundir con 403 — el 404 no revela si existe
409 Conflict
Violación de unicidad, estado inválido para la operación
422 Unprocessable
Validación de negocio: saldo insuficiente, fecha vencida, etc.
429 Too Many
Rate limit. Incluí header Retry-After.
500 Internal
Error inesperado. Nunca a propósito. Si ves esto, hay un bug.
503 Unavailable
Mantenimiento, dependencia caída. Incluí Retry-After.

Códigos de error en el body

Usá error.code para que el cliente pueda tomar decisiones programáticas — no parsees el mensaje.

VALIDATION_ERROR → 400/422
Datos de entrada inválidos. Siempre con details[] por campo.
AUTHENTICATION_ERROR → 401
Token inválido, expirado o ausente.
AUTHORIZATION_ERROR → 403
No tenés permisos para esta acción sobre este recurso.
NOT_FOUND → 404
El recurso solicitado no existe.
CONFLICT → 409
El recurso ya existe o el estado no permite la operación.
RATE_LIMIT_ERROR → 429
Excediste el límite de requests. Esperá y reintentá.
INTERNAL_ERROR → 500
Error del servidor. Nosotros lo investigamos, vos reintentá.

4 reglas para errores:

Versionado

La mejor estrategia de versionado es la que no necesita existir. Diseñá APIs que no rompan. Cuando inevitablemente necesités un breaking change, versioná en la URL.

URL prefix /api/v1/users ✅ Usamos este
Visible, cache-friendly, sin magic headers. El más simple para el consumidor.
Header Accept: application/vnd.api.v1+json ❌ Evitar
Invisible en la URL, difícil de debuggear, rompe el caché de CDNs.
Query param /api/users?version=1 ❌ Evitar
Fácil de olvidar, no semántico, interfiere con filtros reales.
1. Nuevo endpoint con breaking change → nueva versión de API (/api/v2/).
2. Agregar campo a la respuesta → no rompe. Misma versión.
3. Cambiar tipo de un campo existente → rompe. Nueva versión.
4. Eliminar endpoint → deprecarlo en v1 con warning header por 6 meses antes de borrarlo.
5. Mantené v1 funcionando mientras v2 existe. Convivir al menos 6 meses.
6. Documentá v1 como deprecated con un link a la migración.

Paginación

Query params

page integer default: 1 Número de página (1-indexed)
perPage integer default: 20 Ítems por página. Máximo 100.

Meta en respuesta

page Página actual
perPage Ítems por página
total Total de ítems (sin paginar)
totalPages Total de páginas

Filtros y orden

sort ?sort=-createdAt Orden. Prefijo - para descendente. Múltiple: ?sort=-createdAt,name
status ?status=active Filtro por igualdad exacta
q ?q=fundamental Búsqueda textual (full-text search). Simple, sin operadores.
from / to ?from=2026-01-01&to=2026-01-31 Rango de fechas. ISO 8601. from inclusive, to exclusive.