api-contract-review
Revisa los contratos de la API REST en cuanto a semántica HTTP, compatibilidad hacia atrás y coherencia de las respuestas. Úsala cuando el usuario solicite «revisar la API», «comprobar endpoints» o «r
SKILL.md.claude/skills/api-contract-review/SKILL.md
GitHubAPI Contract Review — Runcriticon
Audita controllers y la spec api/openapi.yaml contra los patrones del proyecto: lenguaje ubicuo en castellano, Either<{Modulo}Error, T> → HTTP vía toResponse(), @Authorize en handlers, y política de versionado del MVP (ADR-0001 D10). Cruce: ADR-0001 D10, ADR-0008 D11, ADR-0009 D12/D13, ADR-0012 D14/D19.
Cuándo usar
- Antes de publicar cambios en
api/openapi.yamlo en cualquier*Controller.kt. - Al revisar un PR con nuevos endpoints o modificación de DTOs.
- Cuando el usuario pide «revisar la API», «comprobar endpoints» o «compatibilidad hacia atrás».
Para crear o editar la spec OpenAPI, usar
openapi-spec-editor.
Problemas bloqueantes
| Problema | Síntoma | Cruce |
|---|---|---|
| Breaking sin etiquetar | Eliminar/renombrar campo/endpoint en PR normal | ADR-0001 D10 — requiere PR propia con label breaking-api |
Handler sin @Authorize |
Handler público sin @Authorize ni @NoAuthRequired |
ADR-0009 D13 — ArchUnit lo detecta en CI |
| Entity leak | Clase @Entity o domain en el body de respuesta |
ADR-0008 D11 — solo DTOs en api/ |
Either aplanado a mano |
when (result) en el controller |
El patrón es result.toResponse() — el when va en toResponse |
| Anglicismo en ruta/DTO | /api/workouts, class WorkoutDto |
ADR-0008 — lenguaje ubicuo en castellano; usar sesion, plan, alumno |
| 403 con detalle de motivo | Body 403 describe por qué se deniega | Security leak — el body de 403 debe ser neutro |
Either → HTTP: el contrato de status codes
Toda la traducción de error a HTTP vive en la extensión toResponse(). El controller solo llama:
@GetMapping("/{id}")
@Authorize("PLAN:LEER")
fun obtener(@PathVariable id: UUID): ResponseEntity<*> =
servicio.obtener(PlanId(id)).toResponse()
El when exhaustivo sobre {Modulo}Error está en toResponse(), no en el controller. Revisar que el controller no tenga:
when (result) { is Either.Left -> ... is Either.Right -> ... }inlinetry/catchsobre el caso de uso- Llamadas a
ResponseEntity.status(...)directas
Mapeo canónico Either → HTTP
{Modulo}Error |
HTTP | Notas |
|---|---|---|
NotFound |
404 | Solo si el recurso es propio del club; si es de otro club → 404 también (no 403, para no filtrar) |
Forbidden |
403 | Body neutro: {"code":"FORBIDDEN"} — sin detallar el motivo |
InvalidInput(campo, motivo) |
400 | Body incluye field y message |
Conflict |
409 | |
ProjectionStale |
503 | Autorización fail-closed (ADR-0009 D9) |
Shape del error 4xx
{
"code": "PLAN_NOT_FOUND",
"field": null,
"message": "El plan solicitado no existe",
"details": []
}
code: legible por máquina; el frontend lo traduce — nunca muestramessagedirectamente al usuario.field: presente en errores de validación (InvalidInput),nullen el resto.- El body de 403 es siempre neutro — no filtrar si el objeto existe pero pertenece a otro club.
Semántica HTTP
Solo verificar lo que el modelo no puede inferir del contexto: el proyecto no usa @ResponseStatus en las clases de dominio, el status correcto sale de toResponse(). Confirmar que:
POSTde creación devuelve201 CreatedconLocationheader.DELETEdevuelve204 No Content.GETno tiene efectos secundarios — si cambia estado, debe serPOST/PATCH.PUT/PATCHson idempotentes tal como están implementados.
Versionado de API (ADR-0001 D10)
Sin /v1/ en el MVP — único consumidor es la SPA del monorepo, se despliega junto al backend. No marcar como problema la ausencia de prefijo de versión: es la decisión vigente.
Lo que sí se revisa:
- Cambios breaking (eliminar campo/endpoint, cambiar tipo, renombrar, cambiar status code) van en PR propia con label
breaking-apiy análisis de impacto — nunca mezclados con cambios aditivos. - La spec
api/openapi.yamldebe cambiar en el mismo PR que la implementación. - Cambios aditivos son tolerantes: el cliente generado ignora campos nuevos.
Disparador de reapertura: cuando aparezca un segundo consumidor desacoplado del ciclo de release de la SPA.
Checklist de revisión
Autorización y seguridad
- Cada handler público tiene
@Authorize("RECURSO:ACCION")o@NoAuthRequired(justificacion = "...")(ADR-0009 D13) - Body de 403 es neutro; el recurso de otro club devuelve 404, no 403 (ADR-0009 D12)
- Sin stack traces en el body de respuesta de errores
Diseño del contrato
- Rutas y DTOs en lenguaje ubicuo castellano (ADR-0008):
/planes/{id}/publicar,class PlanDto,alumnoId - Solo DTOs en
infrastructure/rest/dto/expuestos en la API — nada de clases dedomain -
toResponse()centraliza la traducciónEither → HTTP; el controller no hacewhensobre elEither - Colecciones con paginación si pueden crecer más de ~50 elementos
Compatibilidad
- Sin cambios breaking mezclados con cambios aditivos (ADR-0001 D10)
- PR de breaking lleva label
breaking-apiy análisis de impacto - La spec
api/openapi.yamlrefleja los cambios del PR
Coherencia con la spec
- Los tipos, nombres de campo y status codes del controller coinciden con
api/openapi.yaml - Ningún endpoint implementado sin spec, ni spec sin implementación
Formato del informe
## API Contract Review — {fichero / PR}
### ❌ Bloqueantes
- Handler `POST /planes/{id}/publicar` sin `@Authorize` (ADR-0009 D13)
### ⚠️ Advertencias
- DTO `PlanResponse` expone campo `entrenadorId` no declarado en openapi.yaml
### ✅ Sin problemas
- Semántica HTTP, versionado, shape de errores
### Conclusión
REQUIERE CAMBIOS / CONTRATO CORRECTO