Adicionar um endpoint
Adicionar uma route + schema Zod + registro em apiRoute + permission gate + regen.
Você está adicionando um único endpoint a um módulo existente (ex. GET /api/users/me/preferences). Para um módulo novo, veja adicionar uma nova feature.
1. Definir o contract
packages/contracts/src/user.ts:
export const PreferencesSchema = z.object({
locale: z.enum(['en', 'es', 'pt']),
emailDigest: z.boolean(),
});
export type Preferences = z.infer<typeof PreferencesSchema>;Re-exporte do barrel de contracts.
2. Adicionar o use case (ou estender uma query)
// apps/server/src/modules/iam/application/use-cases/get-preferences.ts
export class GetPreferences implements UseCase<{ userId: UserId }, Preferences> {
constructor(private readonly users: UserRepository) {}
async execute({ userId }: { userId: UserId }): Promise<Result<Preferences, DomainError>> {
const user = await this.users.findById(userId);
if (!user) return err(new NotFoundError('User'));
return ok({ locale: user.locale, emailDigest: user.emailDigest });
}
}Registre em bootstrap/container.ts.
3. Cabear a route
// apps/server/src/modules/iam/interfaces/http/users.routes.ts
import { apiRoute } from '@/infrastructure/http/openapi.js';
import { PreferencesSchema } from '@app/contracts';
apiRoute({
method: 'get',
path: '/users/me/preferences',
tags: ['users'],
responses: {
200: { description: 'OK', schema: PreferencesSchema },
},
security: 'session',
});
router.get(
'/me/preferences',
authMiddleware,
readLimiter,
// Sem requirePermission — ler suas próprias preferências é sempre permitido.
async (req, res) => {
const result = await getPreferences.execute({ userId: req.session.userId });
if (result.isErr()) return sendError(res, result.error);
res.json(result.value);
},
);Monte os middlewares corretos
| Caso de uso | Cadeia de middleware |
|---|---|
| Leitura pública | apenas readLimiter |
| Leitura autenticada | authMiddleware → readLimiter |
| Operação admin | authMiddleware → writeLimiter → requirePermission('resource:action') |
| Self-or-admin (ex. PATCH /users/:id) | authMiddleware → writeLimiter → requirePermissionOrSelf('users:update', (r) => r.params.id) |
| Valida um body | adicionar validateRequest({ body: ContractSchema }) |
| Feature premium | adicionar requireActiveSubscription |
Os quatro tier limiters (readLimiter, writeLimiter, authIpLimiter, authEmailLimiter) são memoizados — todo router recebe o mesmo handler, então modo memória e modo Redis compartilham buckets entre routers.
4. Regenerar tipos
bun run generate:apiCheque que o diff está escopado:
git diff apps/server/openapi.json apps/client/lib/api/openapi-types.tsCommit ambos os arquivos no mesmo change que a route.
5. Use a partir do client
import { api } from '@/lib/api/client';
const { data, error } = await api.GET('/api/users/me/preferences');O path, o método, e o tipo de response são todos checados contra o openapi-types.ts regenerado. Se você renomeou o path entre gerar a spec e escrever a chamada, a chamada não compila.
6. Test
Um teste challenging exercita o path que regrediria. Para um endpoint que gateia por permissions, isso é "usuário sem grant pega 403, usuário com grant pega 200". Para um endpoint que valida body, "campo faltando retorna 422 com os issues certos". Evite testes que só checam status codes no happy path — esses não falham quando a lógica de negócio regride.