Contract de API & OpenAPI
Como endpoints se declaram, como a spec OpenAPI é gerada, e quando você precisa regenerar.
Como um endpoint se registra
Toda rota declara sua forma OpenAPI via apiRoute(...) de 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 o path em um registry Zod-to-OpenAPI. Uma response 422 de validation-failure é adicionada por default em toda rota, com a forma padrão { code, message, issues } — não a redeclare.
O mesmo schema Zod valida a request via middleware validateRequest({ body, params, query }). Drift de schema é impossível: há apenas um schema.
Gerando a spec
bun run generate:apiIsso roda três passos:
- Inicia o container do server em modo spec-only e percorre o
openApiRegistrypara emitirapps/server/openapi.json. - Roda
openapi-typescriptpara produzirapps/client/lib/api/openapi-types.ts. - Formata ambos com prettier.
Ambos os arquivos estão no git. O job api-types-fresh do CI roda novamente o generator e falha o PR se qualquer um dos dois mudaria. É um gate duro, não um warning.
Quando você precisa regenerar
A regra do CLAUDE.md:
- Adicionar, remover ou renomear uma rota.
- Mudar um schema Zod de request ou response (incluindo adicionar/remover campos).
- Modificar requisitos de permission/auth (a security scheme do OpenAPI os reflete).
- Adicionar/remover valores em enums que fluem pela API surface — ex. strings de permission em
packages/shared/src/permissions, enums de status em aggregates expostos via DTOs.
Pule apenas se a mudança for pura infraestrutura sem efeito visível no controller (um index Prisma, um helper privado). Na dúvida: regenere. É idempotente.
Workflow
- Faça a mudança de endpoint ou schema.
- Da raiz do repo:
bun run generate:api. git diff apps/server/openapi.json apps/client/lib/api/openapi-types.ts— confirme que as mudanças estão escopadas ao seu edit. Drift surpresa significa que você está regenerando contra uma tree suja; limpe antes de commitar.- Commit ambos os arquivos regenerados no mesmo commit (ou como um commit
chore(<scope>): regenerate OpenAPIanexado ao mesmo PR). - Push. CI roda novamente o freshness check.
Consumindo o client tipado
O frontend chama o server via 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/pt/architecture/contracts
});
const { data, error } = await api.GET('/api/users/{id}', {
params: { path: { id: userId } },
});Se o server muda o path ou a forma do body, essa chamada deixa de compilar.
Swagger UI
/docs (no server) serve Swagger UI contra a spec viva. Útil para fuçar nos endpoints durante o desenvolvimento; não substitui o client tipado em código da app.