Contract de API & OpenAPI
Cómo se declaran los endpoints, cómo se genera la spec OpenAPI, y cuándo debés regenerar.
Cómo se registra un endpoint
Toda ruta declara su forma OpenAPI vía apiRoute(...) desde apps/server/src/infrastructure/http/openapi.ts:
import { apiRoute } from '@/infrastructure/http/openapi.js';
import { user } from '@app/contracts';
apiRoute({
method: 'post',
path: '/users',
tags: ['users'],
summary: 'Create a user',
body: user.CreateUserInput,
responses: {
201: { description: 'Created', schema: user.UserDTO },
409: { description: 'Email already exists' },
},
security: 'session',
});apiRoute registra el path con un registry Zod-to-OpenAPI. Una respuesta 422 de validation-failure se agrega por default a toda ruta, con la forma estándar { code, message, issues } — no la redeclares.
El mismo schema Zod valida la request vía middleware validateRequest({ body, params, query }). El drift de schema es imposible: hay un solo schema.
Generando la spec
bun run generate:apiEsto corre tres pasos:
- Arranca el container del server en modo spec-only y recorre el
openApiRegistrypara emitirapps/server/openapi.json. - Corre
openapi-typescriptpara producirapps/client/lib/api/openapi-types.ts. - Formatea ambos con prettier.
Ambos archivos están en git. El job api-types-fresh de CI re-corre el generador y falla el PR si cualquiera de los dos cambiaría. Es un gate duro, no un warning.
Cuándo debés regenerar
La regla del CLAUDE.md:
- Agregar, eliminar o renombrar una ruta.
- Cambiar un schema Zod de request o response (incluyendo agregar/eliminar campos).
- Modificar requisitos de permission/auth (la security scheme de OpenAPI los refleja).
- Agregar/eliminar valores en enums que fluyen por la API surface — ej. strings de permission en
packages/shared/src/permissions, enums de estado en aggregates expuestos vía DTOs.
Saltealo sólo si el cambio es pura infraestructura sin efecto visible en el controller (un index de Prisma, un helper privado). En la duda: regenerá. Es idempotente.
Workflow
- Hacé el cambio de endpoint o schema.
- Desde la raíz del repo:
bun run generate:api. git diff apps/server/openapi.json apps/client/lib/api/openapi-types.ts— confirmá que los cambios estén acotados a tu edit. Drift sorpresa significa que estás regenerando contra un tree sucio; limpialo antes de commitear.- Commiteá ambos archivos regenerados en el mismo commit (o como un commit
chore(<scope>): regenerate OpenAPIadjunto al mismo PR). - Push. CI re-corre el freshness check.
Consumiendo el client tipado
El frontend llama al server vía openapi-fetch:
import createClient from 'openapi-fetch';
import type { paths } from '@/lib/api/openapi-types';
export const api = createClient<paths>({
baseUrl: process.env.NEXT_PUBLIC_API_URL,
credentials: 'include', // cookies — ver /docs/es/architecture/contracts
});
const { data, error } = await api.GET('/api/users/{id}', {
params: { path: { id: userId } },
});Si el server cambia el path o la forma del body, esta llamada deja de compilar.
Swagger UI
/docs (en el server) sirve Swagger UI contra la spec viva. Útil para tirar pruebas en endpoints durante el desarrollo; no reemplaza al client tipado en código de la app.