Agregar un endpoint
Agregar una route + schema Zod + registración en apiRoute + permission gate + regen.
Estás agregando un único endpoint a un módulo existente (ej. GET /api/users/me/preferences). Para un módulo nuevo, ver agregar una nueva feature.
1. Definir el 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-exportá desde el barrel de contracts.
2. Agregar el use case (o extender una 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 });
}
}Registralo en bootstrap/container.ts.
3. Cablear la 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,
// Sin requirePermission — leer tus propias preferencias siempre está 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);
},
);Montá los middlewares correctos
| Caso de uso | Cadena de middleware |
|---|---|
| Lectura pública | sólo readLimiter |
| Lectura autenticada | authMiddleware → readLimiter |
| Operación admin | authMiddleware → writeLimiter → requirePermission('resource:action') |
| Self-or-admin (ej. PATCH /users/:id) | authMiddleware → writeLimiter → requirePermissionOrSelf('users:update', (r) => r.params.id) |
| Valida un body | sumar validateRequest({ body: ContractSchema }) |
| Feature premium | sumar requireActiveSubscription |
Los cuatro tier limiters (readLimiter, writeLimiter, authIpLimiter, authEmailLimiter) están memoizados — todo router obtiene el mismo handler, así que el modo memoria y el modo Redis comparten buckets entre routers.
4. Regenerar tipos
bun run generate:apiChequeá que el diff esté acotado:
git diff apps/server/openapi.json apps/client/lib/api/openapi-types.tsCommiteá ambos archivos en el mismo cambio que la route.
5. Usalo desde el client
import { api } from '@/lib/api/client';
const { data, error } = await api.GET('/api/users/me/preferences');El path, el método, y el tipo de response están todos chequeados contra el openapi-types.ts regenerado. Si renombraste el path entre generar la spec y escribir la llamada, la llamada no compila.
6. Test
Un test challenging ejercita el path que regresaría. Para un endpoint que gateá por permissions, eso es "user sin grant obtiene 403, user con grant obtiene 200". Para un endpoint que valida body, "campo faltante devuelve 422 con los issues correctos". Evitá tests que sólo chequean status codes en el happy path — esos no fallan cuando la lógica de negocio regresa.