---
name: api-rest-diseno
description: Diseño de APIs REST idiomáticas. Recursos, verbos HTTP, códigos de estado, paginación, filtrado, versionado, HATEOAS y principios de diseño orientado al consumidor.
version: "1.0.1"
evolved: true
evolved-from: "5.0.3"
evolved-at: "2026-04-23"
evolved-by: "aprender"
evolved-note: "Gotcha nuevo: parser detecta estructura pero 0 contenido → 422 es peor UX que fallback con advertencia"
herramientasPermitidas: [Read, Grep]
exclusiones:
  - "No cargar para implementación de endpoints FastAPI o Django — el diseño aquí es agnóstico de framework; para implementación cargar `fastapi-experto`."
  - "No cargar para APIs GraphQL (queries, mutations, subscriptions) — GraphQL tiene convenciones propias de diseño; cargar `graphql-experto`."
  - "No cargar para WebSockets o APIs basadas en eventos — este skill es exclusivamente para APIs request-response HTTP/REST."
  - "No cargar para documentación OpenAPI/Swagger — el skill define principios de diseño, no cómo escribir la especificación YAML/JSON."
evolvable: true  # default para skill estandar
---
# Diseño de APIs REST

## Cuándo NO cargar

- La tarea es implementar los endpoints en FastAPI o Django — cargar `fastapi-experto`; este skill define principios de diseño agnósticos de framework.
- La API es GraphQL (queries, mutations, subscriptions) — cargar `graphql-experto`.
- El protocolo es WebSocket o la API es event-driven — este skill es exclusivamente para APIs HTTP request-response.
- El trabajo es escribir la especificación OpenAPI/Swagger — este skill define principios, no la sintaxis YAML de la especificación.

## Principios fundamentales

Una buena API REST es **predecible**, **consistente** y **centrada en el consumidor**.
Si el desarrollador que la consume puede intuir cómo funciona un endpoint nuevo antes
de leer la documentación, la API está bien diseñada.

---

## Nomenclatura de recursos

```
# BIEN: sustantivos en plural, kebab-case
GET  /facturas
GET  /facturas/{id}
GET  /facturas/{id}/items
GET  /usuarios/{id}/preferencias

# MAL: verbos en la URI
GET  /obtener-facturas
POST /crear-factura
```

### Recursos anidados: máximo 2-3 niveles

Anidar solo cuando la relación es de pertenencia fuerte:

```
# BIEN: items no existen sin factura
GET  /facturas/{factura_id}/items

# Preferir filtro sobre anidamiento excesivo
GET  /pedidos?usuario_id={id}   # En lugar de /usuarios/{id}/pedidos
```

---

## Verbos HTTP y su semántica

| Verbo    | Semántica                          | Idempotente | Body req. |
|----------|------------------------------------|-------------|-----------|
| GET      | Obtener recurso (sin efectos)      | Sí          | No        |
| POST     | Crear recurso o acción             | No          | Sí        |
| PUT      | Reemplazar recurso completo        | Sí          | Sí        |
| PATCH    | Actualización parcial              | No*         | Sí        |
| DELETE   | Eliminar recurso                   | Sí          | No        |

### Acciones que no son CRUD

```
POST /facturas/{id}/cancelar
POST /facturas/{id}/timbrar
POST /usuarios/{id}/activar
```

---

## Códigos de estado HTTP

### 2xx — Éxito
- `200 OK` — GET, PUT, PATCH exitoso con body
- `201 Created` — POST exitoso (incluir Location header)
- `204 No Content` — DELETE exitoso, o PUT sin body
- `202 Accepted` — Tarea asíncrona aceptada

### 4xx — Error del cliente
- `400 Bad Request` — Body malformado o parámetros inválidos
- `401 Unauthorized` — Sin autenticación o token inválido
- `403 Forbidden` — Autenticado pero sin permisos
- `404 Not Found` — Recurso no existe
- `409 Conflict` — Estado incompatible (duplicado, etc.)
- `422 Unprocessable` — Sintaxis correcta, semántica inválida
- `429 Too Many Requests` — Rate limit (incluir Retry-After)

### 5xx — Error del servidor
- `500 Internal Server Error` — Nunca revelar detalles en producción
- `503 Service Unavailable` — Mantenimiento o sobrecarga

---

## Formato de errores consistente

```json
{
  "error": {
    "codigo": "VALIDACION_FALLIDA",
    "mensaje": "Los datos proporcionados son inválidos.",
    "detalles": [
      {
        "campo": "fecha",
        "mensaje": "La fecha no puede ser anterior a hoy."
      },
      {
        "campo": "monto",
        "mensaje": "El monto debe ser mayor a cero."
      }
    ],
    "request_id": "550e8400-e29b-41d4-a716-446655440000"
  }
}
```

---

## Paginación

Toda colección DEBE tener paginación. Dos estrategias:
- **Cursor** (recomendada): consistente bajo inserciones, eficiente para grandes datasets.
- **Offset**: simple, útil para UIs con número de página.

Para formatos de respuesta completos de ambas estrategias, ver [recursos/referencia-api.md](recursos/referencia-api.md).

---

## Filtrado y ordenamiento — resumen

```
GET /facturas?estatus=emitida&fecha_desde=2026-01-01  # filtros
GET /facturas?orden=-fecha,folio                       # ordenamiento
GET /facturas?campos=id,folio,monto_total              # proyección
```

Para convenciones completas de filtrado y query parameters, ver [recursos/referencia-api.md](recursos/referencia-api.md).

---

## Versionado

Obligatorio con prefijo en URL: `/v1/`, `/v2/`. Deprecar con mínimo 6 meses de anticipación usando headers `Deprecation` y `Sunset`.

Para detalles de estrategias de versionado (URI vs header) y deprecación, ver [recursos/referencia-api.md](recursos/referencia-api.md).

---

## HATEOAS y Headers

HATEOAS (links a recursos relacionados) es opcional en APIs internas, recomendado en APIs públicas. Headers importantes: Location, Cache-Control, ETag, X-RateLimit-*, Idempotency-Key.

Para ejemplos completos, ver [recursos/referencia-api.md](recursos/referencia-api.md).

---

## Gotchas / Errores comunes no obvios

**`PATCH` con cuerpo vacío `{}` es válido semánticamente pero algunos clientes lo rechazan**: un `PATCH {}` significa "actualiza el recurso sin cambiar ningún campo" — es idempotente y debería retornar `200` con el estado actual. Pero algunos frameworks de backend lanzan error 400 si el body tiene validación de `required fields`. Causa: confusión entre "campo requerido para crear" y "campo requerido para actualizar parcialmente". Fix: los schemas de `PATCH` deben tener TODOS los campos como opcionales (`Optional` en Pydantic) — el `required` solo aplica a `POST`/`PUT`.

**`204 No Content` en DELETE de un recurso ya eliminado (idempotencia) vs `404`**: si el cliente borra el mismo recurso dos veces, la primera retorna `204` y la segunda debería retornar `404` o `204`. DELETE es idempotente en el resultado (el recurso no existe) pero no necesariamente en el código de respuesta. Causa: idempotencia en REST significa que múltiples requests idénticos tienen el mismo efecto en el servidor, no que retornan el mismo código HTTP. Fix: retornar `404` en el segundo DELETE es semánticamente correcto y más informativo; documentar explícitamente en la API el comportamiento.

**Paginación por offset retorna resultados inconsistentes si hay inserciones concurrentes**: `GET /items?offset=20&limit=10` puede devolver un item que ya apareció en la página anterior si se insertó un item nuevo con id < el ítem en offset=20 entre las dos peticiones. Causa: offset-based pagination no tiene un "anchor" — el conjunto de datos cambia entre páginas. Fix: para listas que se actualizan frecuentemente, usar cursor-based pagination con `?after=<cursor>` donde el cursor es un valor estable (timestamp + id) que ancla la posición en el resultado.

**El `Location` header en respuestas `201 Created` es obligatorio en REST pero frecuentemente omitido**: POST exitoso sin `Location: /v1/recursos/nuevo-id` obliga al cliente a parsear el body para conocer la URL del recurso creado, acoplando el cliente a la estructura del body. Causa: se considera opcional porque el cliente "ya puede construir la URL". Fix: SIEMPRE incluir `Location` en `201 Created` — `response.headers["Location"] = f"/v1/facturas/{nueva_factura.id}"` es una línea de código que hace la API self-describing.

**Endpoint que parsea input estructurado y encuentra "estructura pero 0 contenido" debe preferir fallback con advertencia sobre 422**: un endpoint que extrae ítems desde un documento estructurado puede detectar los bloques marcadores (secciones, encabezados) pero 0 ítems válidos dentro por diferencia de formato. Retornar lista vacía que el endpoint traduce a 422 fuerza al frontend a mostrar un error y al usuario a diagnosticar por su cuenta. Fix preferido: emitir **1 ítem "genérico" con el texto completo + advertencia explícita** en la respuesta 200. El usuario recibe algo accionable y ve en advertencias qué falló. El 422 debe reservarse para input fundamentalmente inválido (archivo corrupto, magic number incorrecto), no para "el parser no pudo inferir la estructura del input válido". Patrón general: cuando un endpoint detecta los marcadores estructurales del documento pero 0 contenido extraíble dentro de ellos, responder 200 con un ítem fallback + advertencia `"se detectaron N bloques pero 0 ítems válidos — revisar formato del documento"`, no 422.

## Checklist de diseño de un nuevo endpoint

- [ ] El URI usa sustantivos en plural, sin verbos.
- [ ] El verbo HTTP refleja la semántica correcta.
- [ ] El código de estado es el apropiado para cada caso.
- [ ] Los errores siguen el formato estándar con campo `codigo` y `detalles`.
- [ ] Las colecciones tienen paginación (nunca devolver listas sin límite).
- [ ] Los filtros usan query parameters, nunca el body en GET.
- [ ] El endpoint está versionado bajo `/v{N}/`.
- [ ] La documentación OpenAPI está actualizada.
- [ ] Hay validación de entrada que devuelve 400 con detalles de campo.
- [ ] El endpoint maneja el caso de recurso no encontrado con 404.
