REST no es un estándar con validador: es un estilo arquitectónico que cada equipo interpreta a su manera, y ahí nacen las APIs donde POST /getUsers convive con DELETE /borrar?id=3. Las convenciones de este artículo no son capricho estético — son contratos que reducen fricción, bugs y preguntas de soporte a la mitad.
Recursos, no acciones
El principio rector: tus URLs nombran sustantivos, los verbos HTTP expresan la acción.
GET /articulos ← listar
POST /articulos ← crear
GET /articulos/42 ← leer uno
PUT /articulos/42 ← reemplazar completo
PATCH /articulos/42 ← actualizar parcial
DELETE /articulos/42 ← eliminar
Comparado con el antipatrón RPC-sobre-HTTP (POST /obtenerArticulos, POST /eliminarArticulo?id=42), ganas uniformidad predecible: quien conoce un recurso sabe operarlo entero sin documentación adicional.
Reglas derivadas que evitan discusiones infinitas:
- Plural siempre (
/articulos, no/articulo). - Jerarquía para anidación máxima de dos niveles:
/autores/7/articuloses legible;/autores/7/articulos/42/comentarios/9/likesya pide subrecurso raíz (/likes?comentario=9). - Filtrado y orden por query string, jamás por ruta:
/articulos?autor=7&estado=publicado&orden=-fecha&pagina=2. - Sin verbos en rutas ni extensiones (.json) ni mayúsculas.
Códigos de estado: el contrato semántico
Devolver 200 OK con { "error": "algo fue mal" } rompe clientes, caches, monitores y retries automáticos. El código es parte del mensaje. Los imprescindibles:
| Código | Cuándo |
|---|---|
| 200 OK | Éxito con cuerpo (lecturas, actualizaciones) |
| 201 Created | Recurso creado; añade cabecera Location del nuevo recurso |
| 204 No Content | Éxito sin cuerpo (borrado, PUT completo) |
| 400 Bad Request | Sintaxis/validación errónea del cliente |
| 401 Unauthorized | No autenticado (el nombre engaña: es "unauthenticated") |
| 403 Forbidden | Autenticado pero sin permiso |
| 404 Not Found | Recurso inexistente — también cuando no quieres revelar existencia |
| 409 Conflict | Conflicto de estado (email duplicado, versión obsoleta) |
| 422 Unprocessable | Semánticamente inválido (JSON válido, datos imposibles) |
| 429 Too Many Requests | Rate limit excedido |
| 500 | Error tuyo, nunca del cliente |
El sistema completo está detallado en nuestra guía de códigos HTTP; aquí basta la disciplina: 4xx = culpa del cliente (no reintentes igual), 5xx = culpa del servidor (retry con backoff tiene sentido).
Errores útiles: el formato que sí ayuda
Un error bien diseñado ahorra tickets de soporte:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "La petición contiene campos inválidos",
"details": [
{ "field": "email", "issue": "formato inválido" },
{ "field": "fecha_nacimiento", "issue": "no puede ser futura" }
],
"request_id": "req_8f3k2"
}
}
Consistencia en TODOS los endpoints, request_id trazable en logs, y mensajes que digan qué corregir — nunca stack traces ni detalles internos (superficie de reconocimiento).
Idempotencia: la propiedad que habilita reintentos
Idempotente = repetir la petición produce el mismo resultado. GET, PUT y DELETE lo son naturalmente; POST no (dos POST = dos recursos). Por eso los reintentos automáticos tras timeout son seguros en unos métodos y peligrosos en otros.
Para pagos y operaciones críticas vía POST existe el patrón Idempotency-Key: el cliente genera una clave única por operación y la envía en cabecera; tu servidor detecta repetición y devuelve el resultado original en vez de ejecutar doble cargo. Stripe popularizó el patrón y debería ser default mental en cualquier API que mueva dinero o estado crítico.
Paginación, filtros y orden desde el día uno
Añadir paginación después de lanzar rompe consumidores. Diseña antes:
GET /articulos?page=2&limit=25&sort=-creado,titulo
- Offset/limit: simple, pero salta elementos si hay inserciones concurrentes.
- Cursor (
?cursor=eyJpZCI6MTAwfQ): estable bajo escritura concurrente, ideal para feeds infinitos; opaco para el cliente. - Devuelve siempre metadatos (
total,next_cursor) o cabeceras (Link: rel="next").
Y limita el limit máximo server-side: ?limit=1000000 es un ataque de recursos disfrazado de query param.
Versionado: decide antes de necesitarlo
Tres estrategias, una recomendación:
- En la ruta (
/v1/articulos): visible, cacheable, trivial de enrutar. La más común y suficiente para casi todos. - Cabecera (
Accept: application/vnd.miapi.v2+json): purista, invisible en logs de acceso. - Sin versión: solo si tu API es interna y efímera.
Cuando lances v2: v1 sigue funcionando intacto (breaking changes = nueva versión), comunicala con fechas de deprecación y cabecera Sunset. Romper consumidores silenciosamente quema la confianza que toda API necesita.
Seguridad transversal
Autenticación por tokens Bearer (OAuth2/OIDC según contexto — flujo completo aquí), HTTPS obligatorio sin excepciones, rate limiting por clave/IP (por qué y cómo se combina con CORS correctamente configurado), y validación de entrada en el borde con esquemas estrictos (Zod, Pydantic): confiar en que "el cliente manda bien los datos" es la puerta de entrada de medio internet roto.
Preguntas frecuentes
¿GraphQL mata a REST? Conviven: GraphQL brilla con clientes móviles que necesitan composiciones exactas y odian over-fetching; REST sigue imbatible en simplicidad, caché HTTP y ecosistema. Muchas plataformas ofrecen ambos.
¿HATEOAS es obligatorio para ser REST? El purismo dice sí; la práctica, no. APIs maduras con hipervínculos son raras; nadie te invalidará por REST pragmático con buen uso de verbos, estados y recursos.
¿Cómo documento? OpenAPI/Swagger como fuente de verdad generada del código o viceversa. Documentación desincronizada es peor que ninguna: automatiza o muere en tickets.
Consulta al instante qué significa cualquier código de respuesta con nuestra guía de códigos HTTP, gratis y directamente en tu navegador.