Una API consistente se aprende una vez y se usa para siempre. Cada endpoint debería ser predecible sin necesidad de leer documentación.
GET /api/v1/users 200 — recurso(s) POST /api/v1/users 201 — recurso creado PUT /api/v1/users/42 200 — recurso actualizado PATCH /api/v1/users/42 200 — recurso actualizado DELETE /api/v1/users/42 204 — sin contenido ✅ /api/v1/users ❌ /api/v1/user /api/v1/usuarios ✅ /api/v1/users/42/orders ❌ /api/v1/users/42/orders/17/items/3 ✅ /api/v1/payment-methods ❌ /api/v1/paymentMethods /api/v1/payment_methods ✅ DELETE /api/v1/users/42 ❌ POST /api/v1/users/42/delete ✅ /api/v1/users/42/orders ❌ /api/v1/orders?userId=42 ✅ /api/v1/orders?status=paid&sort=-createdAt ❌ /api/v1/orders/paid/desc ✅ POST /api/v1/orders/83/cancel ❌ POST /api/v1/cancel-order/83 ✅ POST /api/v1/users/search { "query": {...} } ❌ GET /api/v1/users?q[name][like]=... 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.
{
"data": { ... },
"meta": {
"requestId": "req_abc123",
"timestamp": "2026-05-14T10:30:00Z"
}
} {
"data": [ ... ],
"meta": {
"page": 1,
"perPage": 20,
"total": 142,
"totalPages": 8,
"requestId": "req_abc123"
}
} {
"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"
}
} data, errores bajo error. requestId en meta para tracear requests en logs. UUID generado en el primer middleware. meta de paginación: page, perPage, total, totalPages. details con cada campo que falló y por qué. ?debug=1. // ❌ 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": {...} } El status code correcto + el error code correcto = el cliente puede automatizar su respuesta sin intervención humana.
Usá error.code para que el cliente pueda tomar decisiones programáticas — no parsees el mensaje.
VALIDATION_ERROR → 400/422 AUTHENTICATION_ERROR → 401 AUTHORIZATION_ERROR → 403 NOT_FOUND → 404 CONFLICT → 409 RATE_LIMIT_ERROR → 429 INTERNAL_ERROR → 500 4 reglas para errores:
{ "error": "..." }. Si hay error, el status code debe reflejarlo.error.message es para el usuario final. En desarrollo, podés incluir stack con flag.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.
/api/v2/). page integer default: 1 Número de página (1-indexed) perPage integer default: 20 Ítems por página. Máximo 100. 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.