SaaS Starter

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 usoCadeia de middleware
Leitura públicaapenas readLimiter
Leitura autenticadaauthMiddlewarereadLimiter
Operação adminauthMiddlewarewriteLimiterrequirePermission('resource:action')
Self-or-admin (ex. PATCH /users/:id)authMiddlewarewriteLimiterrequirePermissionOrSelf('users:update', (r) => r.params.id)
Valida um bodyadicionar validateRequest({ body: ContractSchema })
Feature premiumadicionar 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:api

Cheque que o diff está escopado:

git diff apps/server/openapi.json apps/client/lib/api/openapi-types.ts

Commit 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.

Nesta página