SaaS Starter

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:api

Isso roda três passos:

  1. Inicia o container do server em modo spec-only e percorre o openApiRegistry para emitir apps/server/openapi.json.
  2. Roda openapi-typescript para produzir apps/client/lib/api/openapi-types.ts.
  3. 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

  1. Faça a mudança de endpoint ou schema.
  2. Da raiz do repo: bun run generate:api.
  3. 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.
  4. Commit ambos os arquivos regenerados no mesmo commit (ou como um commit chore(<scope>): regenerate OpenAPI anexado ao mesmo PR).
  5. 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.

Nesta página