# Regla: Diseño de APIs REST

Esta regla es OBLIGATORIA para toda API REST expuesta, ya sea pública o interna.
Una API mal diseñada es difícil de versionar, de mantener y de consumir.
El costo de corregir un contrato de API en producción es enormemente más alto
que diseñarlo bien desde el inicio. Ningún endpoint se considera listo si viola
cualquiera de los puntos aquí listados.

---

## Versionado obligatorio con prefijo de URL

Toda API debe incluir el número de versión en la URL desde el primer endpoint.

- El versionado va en el prefijo de la URL como número entero:
  `/v1/`, `/v2/`, `/v3/`, etc.
- Formato correcto:
  ```
  https://api.miapp.com/v1/usuarios
  https://api.miapp.com/v1/pedidos/123/items
  ```
- Formato incorrecto:
  ```
  https://api.miapp.com/usuarios          (sin versión — imposible de versionar después)
  https://api.miapp.com/v1.2/usuarios     (versión minor en URL — innecesariamente granular)
  https://api.miapp.com/usuarios?v=1      (versión en query param — rompe el caching)
  ```
- El versionado en headers (`API-Version: 1`) se permite como mecanismo secundario
  pero NUNCA como el único — los logs, el caching y los proxies operan sobre URLs.
- Al lanzar una nueva versión major (`/v2/`), mantener `/v1/` funcional con un
  período de deprecación documentado de mínimo 6 meses.
- Comunicar la deprecación en los headers de respuesta:
  `Deprecation: true`, `Sunset: [fecha]`, `Link: </v2/usuarios>; rel="successor-version"`

---

## Respuestas consistentes con envelope estándar

Todas las respuestas de la API deben seguir el mismo formato de envelope.

### Respuesta exitosa con un objeto

```json
{
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "nombre": "Juan Pérez",
    "email": "juan@ejemplo.com"
  },
  "meta": {
    "request_id": "req_abc123",
    "timestamp": "2026-03-25T14:30:00Z",
    "version": "1.0"
  }
}
```

### Respuesta exitosa con lista (paginada)

```json
{
  "data": [...],
  "meta": {
    "request_id": "req_abc123",
    "timestamp": "2026-03-25T14:30:00Z",
    "total": 150,
    "page": 1,
    "per_page": 20,
    "has_next": true,
    "has_prev": false
  }
}
```

### Respuesta de error

```json
{
  "errors": [
    {
      "code": "VALIDATION_ERROR",
      "message": "El campo 'email' no tiene formato válido",
      "field": "email",
      "detail": "El valor 'juan-sin-arroba' no es una dirección de correo electrónico"
    }
  ],
  "meta": {
    "request_id": "req_abc123",
    "timestamp": "2026-03-25T14:30:00Z"
  }
}
```

- NUNCA devolver `{"success": true, "data": [...]}` — usar el status HTTP para indicar éxito.
- NUNCA devolver `{"error": "algo falló"}` sin código de error y detalle.
- El campo `meta.request_id` es OBLIGATORIO — permite correlacionar logs con errores reportados por clientes.
- El campo `data` puede ser `null` en respuestas 204 No Content.
- En FastAPI: crear un schema `APIResponse[T]` genérico y usarlo en todos los endpoints.
- En Express/NestJS: crear un interceptor o middleware que envuelva todas las respuestas.

---

## Paginación obligatoria en endpoints de listado

Ningún endpoint que devuelva una colección puede hacerlo sin paginación.

- Todo endpoint que devuelva un array DEBE soportar paginación. Sin excepción.
  Sin paginación, la base de datos crece y el endpoint eventualmente colapsa.

### Paginación basada en cursor (preferida para listas grandes o en tiempo real)

```
GET /v1/pedidos?cursor=eyJpZCI6MTIzfQ==&limit=20
```

Ventajas: consistente bajo inserciones concurrentes, eficiente para grandes datasets.

```json
{
  "data": [...],
  "meta": {
    "next_cursor": "eyJpZCI6MTQzfQ==",
    "prev_cursor": "eyJpZCI6MTAzfQ==",
    "has_next": true,
    "has_prev": true,
    "limit": 20
  }
}
```

### Paginación por offset (aceptable para datasets pequeños y UIs con número de página)

```
GET /v1/productos?page=3&per_page=20
```

```json
{
  "data": [...],
  "meta": {
    "page": 3,
    "per_page": 20,
    "total": 150,
    "total_pages": 8,
    "has_next": true,
    "has_prev": true
  }
}
```

- El `per_page` máximo es 100 registros. NUNCA permitir devolver registros ilimitados.
- El `per_page` por defecto es 20 si no se especifica.
- Si el cliente pide un `per_page` mayor al máximo, devolver el máximo silenciosamente
  o devolver 400 Bad Request con mensaje explicativo. Documentar cuál de las dos.
- NUNCA usar `limit=0` para devolver todos los registros — eso rompe el propósito de la paginación.

---

## Filtros y ordenamiento estandarizados

Los parámetros de filtrado y ordenamiento siguen convenciones uniformes en toda la API.

### Filtros

```
GET /v1/pedidos?estatus=pendiente&cliente_id=abc123&fecha_desde=2026-01-01&fecha_hasta=2026-03-31
```

- Los filtros van como query parameters en GET. NUNCA en el body de un GET.
- Los nombres de los filtros coinciden con los nombres de campo del recurso.
- Para rangos: usar sufijos `_desde` y `_hasta` (o `_min` / `_max` para números).
- Para múltiples valores del mismo campo: repetir el parámetro o usar coma como separador.
  Documentar cuál se usa. No mezclar ambas convenciones:
  ```
  ?estatus=activo&estatus=pendiente    (repetición — más estándar)
  ?estatus=activo,pendiente            (coma — más compacto)
  ```
- Los filtros que no existen o tienen valores inválidos devuelven 400 Bad Request
  con un mensaje que indica exactamente cuál parámetro es inválido.

### Ordenamiento

```
GET /v1/pedidos?sort=fecha_creacion&order=desc
```

- Parámetro `sort`: nombre del campo por el que ordenar.
- Parámetro `order`: `asc` (por defecto) o `desc`.
- Para ordenamiento multi-campo:
  ```
  GET /v1/pedidos?sort=estatus,fecha_creacion&order=asc,desc
  ```
- Si se pide ordenar por un campo que no existe: 400 Bad Request.
- Si se pide ordenar por un campo que no es indexado y la tabla tiene >10k registros,
  documentar esta limitación y devolver un error descriptivo.

---

## HTTP status codes correctos

El status code es la primera línea de comunicación del resultado. Usarlo correctamente.

```
200 OK              — GET exitoso, PUT/PATCH exitoso con body en respuesta
201 Created         — POST exitoso que crea un recurso. Incluir header Location con URL del nuevo recurso
204 No Content      — DELETE exitoso, PUT/PATCH exitoso sin body
400 Bad Request     — Error de validación, parámetro inválido, body malformado
401 Unauthorized    — No autenticado (falta token o token inválido)
403 Forbidden       — Autenticado pero sin permiso para este recurso/acción
404 Not Found       — El recurso no existe
409 Conflict        — Conflicto de estado (ej: email duplicado, stock insuficiente)
410 Gone            — El recurso existió pero fue eliminado permanentemente
422 Unprocessable Entity — La sintaxis es válida pero la semántica falla (ej: fecha de inicio > fecha de fin)
429 Too Many Requests    — Rate limit alcanzado
500 Internal Server Error — Error interno no esperado (con request_id para seguimiento)
503 Service Unavailable  — Servicio temporalmente no disponible (mantenimiento, dependencia caída)
```

Errores comunes a EVITAR:
- NUNCA devolver 200 con `{"success": false}` — usar el status code correcto.
- NUNCA devolver 500 para errores de validación de input del cliente — esos son 400/422.
- NUNCA devolver 404 cuando el problema es falta de permisos — eso es 403.
  (Excepción: cuando revelar la existencia del recurso es un problema de seguridad)
- NUNCA devolver 401 cuando el usuario está autenticado pero no tiene permiso — eso es 403.

---

## Rate limiting obligatorio en endpoints públicos

Todo endpoint accesible sin autenticación o con autenticación débil debe tener rate limiting.

- Los endpoints públicos (sin autenticación) tienen rate limiting estricto:
  Máximo 60 requests por minuto por IP como punto de partida.
  Ajustar según el caso de uso real.
- Los endpoints autenticados tienen rate limiting por usuario/token:
  Máximo 1000 requests por minuto por usuario autenticado.
- Los endpoints de autenticación (login, registro, recuperación de contraseña)
  tienen rate limiting especialmente estricto:
  Máximo 5 intentos por minuto por IP, con bloqueo temporal de 15 minutos al superar.
- Comunicar el rate limiting en headers de respuesta:
  ```
  X-RateLimit-Limit: 60
  X-RateLimit-Remaining: 45
  X-RateLimit-Reset: 1711379400
  Retry-After: 30   (solo en respuestas 429)
  ```
- La respuesta al superar el rate limit es siempre 429 Too Many Requests con body:
  ```json
  {
    "errors": [{
      "code": "RATE_LIMIT_EXCEEDED",
      "message": "Demasiadas solicitudes. Intenta de nuevo en 30 segundos.",
      "detail": "Límite: 60 solicitudes por minuto"
    }]
  }
  ```
- El rate limiting se implementa en el gateway o proxy (nginx, Kong, AWS API Gateway),
  no en la lógica de aplicación. La lógica de aplicación es el último recurso.

---

## CORS configurado explícitamente

El navegador solo permite requests cross-origin si el servidor lo autoriza explícitamente.

- La lista de orígenes permitidos se define por ambiente y se configura desde
  variables de entorno, NUNCA hardcodeada en el código:
  ```python
  # FastAPI
  origins = os.getenv("CORS_ORIGINS", "").split(",")
  app.add_middleware(CORSMiddleware, allow_origins=origins, ...)
  ```
- NUNCA usar `allow_origins=["*"]` en producción. Esto permite que cualquier sitio
  haga requests a la API en nombre del usuario.
- `allow_credentials=True` solo cuando sea necesario (cuando se usan cookies de sesión).
  Incompatible con `allow_origins=["*"]`.
- Los métodos permitidos deben ser los mínimos necesarios:
  APIs de solo lectura: `["GET", "OPTIONS"]`
  APIs completas: `["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]`
- Los headers permitidos deben listarse explícitamente si no se usan los estándar.
- El preflight request (OPTIONS) debe responder correctamente antes de que el browser
  envíe el request real.
- En APIs públicas de lectura sin credenciales, `allow_origins=["*"]` puede ser
  aceptable. Documentar explícitamente por qué se decidió así.

---

## Documentación OpenAPI siempre actualizada

La documentación es parte del contrato de la API. Si está desactualizada, no es documentación.

- La especificación OpenAPI debe generarse desde el código, no escribirse manualmente.
  En FastAPI: se genera automáticamente desde los schemas y decoradores.
  En Express: usar `swagger-jsdoc` o `tsoa`.
- Cada endpoint debe tener:
  - `summary`: descripción corta de qué hace
  - `description`: detalles, casos edge, consideraciones de negocio
  - `tags`: agrupación por dominio (`Usuarios`, `Pedidos`, `Auth`)
  - Todos los parámetros documentados con tipo, formato y si son requeridos
  - Todos los posibles status codes de respuesta con su schema
  - Ejemplos de request y response para los casos principales
- Los schemas de request y response deben documentar:
  - Qué campos son requeridos vs opcionales
  - Restricciones de longitud, formato y dominio de valores
  - Descripciones en español claras para cada campo
- La documentación está disponible en `/docs` (Swagger UI) y `/redoc` en ambientes
  de desarrollo y staging. En producción, solo si la API es pública.
- Los cambios de API sin actualizar la documentación no pasan code review.
- Los schemas de OpenAPI se validan en CI para detectar documentación rota.

---

## Error responses con código, mensaje y detalle

Los errores deben ser diagnósticables por el cliente sin acceso a los logs del servidor.

Cada error response incluye:

```json
{
  "errors": [
    {
      "code": "CÓDIGO_EN_SNAKE_CASE_MAYÚSCULAS",
      "message": "Mensaje legible por humanos en español",
      "field": "nombre_del_campo",
      "detail": "Información adicional para debugging"
    }
  ],
  "meta": {
    "request_id": "req_abc123",
    "timestamp": "2026-03-25T14:30:00Z"
  }
}
```

- `code`: identificador de máquina para el tipo de error. En SCREAMING_SNAKE_CASE.
  Ejemplos: `VALIDATION_ERROR`, `RESOURCE_NOT_FOUND`, `INSUFFICIENT_STOCK`,
  `EMAIL_ALREADY_EXISTS`, `INVALID_CREDENTIALS`, `RATE_LIMIT_EXCEEDED`.
- `message`: mensaje en lenguaje natural, legible por el usuario final si aplica.
  Sin jerga técnica. NUNCA exponer stack traces o nombres de tablas de BD.
- `field`: nombre del campo que causó el error (solo para errores de validación).
  Si el error no está asociado a un campo específico, omitir este campo.
- `detail`: información técnica adicional para el desarrollador cliente. Puede ser
  más técnico que `message`. Opcional.
- Para errores de validación con múltiples campos inválidos, devolver TODOS los errores
  en el array, no solo el primero. El cliente necesita corregir todo de una vez.
- Para errores 500: el mensaje es genérico ("Error interno del servidor").
  El `request_id` permite correlacionar con los logs del servidor donde está el detalle real.
  NUNCA exponer detalles del error interno al cliente en producción.
- Los códigos de error deben estar documentados en la spec de OpenAPI.

---

## Nomenclatura de recursos y endpoints

Las URLs deben ser predecibles, consistentes y orientadas a recursos, no a acciones.

- Los recursos se expresan en sustantivos en plural, en español (o inglés, pero consistente):
  `/v1/usuarios`, `/v1/pedidos`, `/v1/productos`
- Las acciones CRUD mapean a métodos HTTP, no a verbos en la URL:
  ```
  GET    /v1/pedidos           — listar pedidos
  POST   /v1/pedidos           — crear pedido
  GET    /v1/pedidos/123       — obtener pedido específico
  PUT    /v1/pedidos/123       — reemplazar pedido completo
  PATCH  /v1/pedidos/123       — actualizar campos específicos del pedido
  DELETE /v1/pedidos/123       — eliminar pedido
  ```
- Los sub-recursos se expresan anidando en la URL:
  ```
  GET  /v1/pedidos/123/items   — items del pedido 123
  POST /v1/pedidos/123/items   — agregar item al pedido 123
  ```
- Para acciones que no mapean limpiamente a CRUD, usar sub-recursos orientados
  a la acción como nombre:
  ```
  POST /v1/pedidos/123/cancelar   — cancelar el pedido 123
  POST /v1/usuarios/123/activar   — activar cuenta del usuario 123
  ```
  Preferir esto a meter `?action=cancelar` en query params.
- Los IDs en las URLs deben ser UUIDs o IDs opacos. NUNCA IDs secuenciales que
  exponen el volumen de datos del sistema.
- Las URLs son case-insensitive por convención, pero usar siempre minúsculas con guiones:
  `/v1/tipos-de-pago` no `/v1/TiposDePago` ni `/v1/tipos_de_pago`.

---

## Checklist antes de exponer un endpoint nuevo

- [ ] La URL sigue la convención de recursos y tiene el prefijo `/v{N}/`
- [ ] El método HTTP es el correcto para la operación (GET no modifica datos)
- [ ] La respuesta usa el envelope estándar `{data, meta}` o `{errors, meta}`
- [ ] El endpoint de listado tiene paginación implementada
- [ ] El status code de respuesta es el correcto para cada caso
- [ ] Los errores de validación devuelven 400/422 con todos los campos inválidos
- [ ] El endpoint tiene autenticación si maneja datos no públicos
- [ ] El endpoint está documentado en OpenAPI con todos los parámetros y responses
- [ ] El rate limiting está configurado (si es público o de autenticación)
- [ ] El CORS está configurado correctamente para los orígenes esperados
- [ ] Los IDs expuestos son UUIDs u opacos, no IDs secuenciales
