server-arquitetura
Arquitetura do backend Smaug (server/) — camadas Clean Architecture, regra de dependência, alias @src, nomenclatura de arquivos e o roteiro end-to-end para adicionar um agregado novo. Leia antes de cr
SKILL.md.claude/skills/server-arquitetura/SKILL.md
GitHubArquitetura do server
Quando usar esta skill
Ao adicionar um recurso novo ao backend, ao mover código entre camadas, ou sempre que a dúvida for "em que pasta isso vai".
Camadas
server/src/
domain/ entities, value-objects, use-cases, ports (repositórios e serviços de auth), errors
application/ services (orquestram use cases), dtos (Zod + response), ports (Logger), csv
infrastructure/ repositórios Prisma, adapters de auth (argon2/jose/crypto), servidor Express,
container, env + authConfig (Zod), Pino
presentation/ controllers, routes, middlewares (authenticate, origem, rate limit, validação, log, erro)
Regras de dependência (validadas por .dependency-cruiser.js, severidade error):
domaineapplicationnunca importaminfrastructurenempresentation.auth-crypto-only-in-infra:argon2ejosesó são importados emserver/src/infrastructure/(ficam atrás das portasPasswordHasher/AccessTokenService).http-middleware-libs-only-in-presentation:cookie-parsereexpress-rate-limitsó são importados emserver/src/presentation/.
Furo conhecido: o .dependency-cruiser.js não tem options.tsConfig, então o depcruise não
resolve o alias @src/* — e todo import de server/src é por alias. As regras de camada
(no-domain-to-infra, no-application-to-infra) na prática não pegam esses imports; só as regras
que miram node_modules/ (as duas de auth acima) funcionam de verdade. Não conte com o
validate:deps para flagrar um domain → infrastructure: a disciplina é sua e da revisão.
Fluxo de uma requisição:
route → middleware (authenticate, validateRequest) → controller → service → use case → port → repositório Prisma
↓
entidade de domínio
O domínio não conhece Express, Prisma nem Zod. MonthlyCompetence, entidades e use cases usam só
tipos nativos do TypeScript (princípio III da constituição).
Convenções de arquivo e import
Nomes: kebab-case + sufixo de papel.
| Papel | Arquivo | Classe/função |
|---|---|---|
| Entidade | one-time-revenue.entity.ts |
OneTimeRevenue |
| Value object | monthly-competence.value-object.ts |
MonthlyCompetence |
| Porta | one-time-revenue.repository.ts |
interface OneTimeRevenueRepository |
| Repositório Prisma | prisma-one-time-revenue.repository.ts |
PrismaOneTimeRevenueRepository |
| Caso de uso | create-one-time-revenue.use-case.ts |
CreateOneTimeRevenueUseCase |
| Service | one-time-revenue.service.ts |
OneTimeRevenueService |
| DTO | one-time-revenue.dto.ts |
schemas + tipos |
| Controller | one-time-revenue.controller.ts |
OneTimeRevenueController |
| Rotas | one-time-revenue.routes.ts |
createOneTimeRevenueRoutes() |
| Middleware | authenticate.middleware.ts |
authenticate() (factory) |
| Porta de serviço | password-hasher.ts (sem sufixo) |
interface PasswordHasher |
| Adapter | argon2-password-hasher.ts |
Argon2PasswordHasher |
Casos de uso de listagem pluralizam: list-one-time-revenues.use-case.ts →
ListOneTimeRevenuesUseCase.
Imports: sempre pelo alias @src/*. Não existe nenhum import relativo em server/src/.
import { OneTimeRevenue } from "@src/domain/entities/one-time-revenue.entity";
Sufixo .ts no import: allowImportingTsExtensions está ligado, mas o sufixo só aparece nos
16 arquivos de bootstrap/infraestrutura (main.ts, container.ts, config/env.ts,
http/server.ts, logging/logger.ts, database/config.ts, database.provider.ts,
in-memory.repository.ts, os três adapters de infrastructure/auth/, routes/index.ts,
health.controller.ts, e os middlewares de log, erro e rate limit). Em código de feature,
importe sem extensão. Na dúvida, copie o vizinho do mesmo arquivo.
Barris: só existe um — presentation/routes/index.ts. Não crie outros.
Anatomia de um agregado (gabarito: recurring-expense)
Uma fatia completa tem exatamente estes arquivos:
domain/entities/recurring-expense.entity.ts
domain/entities/recurring-expense-version.entity.ts
domain/ports/recurring-expense.repository.ts
domain/use-cases/recurring-expense/{create,get,list-recurring-expenses,update,terminate,delete}-recurring-expense.use-case.ts
domain/errors/domain-error.ts (os erros novos entram aqui)
application/dtos/recurring-expense.dto.ts
application/services/recurring-expense.service.ts
infrastructure/database/repositories/prisma-recurring-expense.repository.ts
presentation/controllers/recurring-expense.controller.ts
presentation/routes/recurring-expense.routes.ts
infrastructure/config/container.ts (a fatia é acrescentada)
presentation/routes/index.ts (a montagem é acrescentada)
tests/unit/... espelhando cada arquivo acima + tests/integration/presentation/recurring-expense.test.ts
A fatia de autenticação foge do gabarito em três pontos, porque o que ela precisa de fora não é
só persistência: portas de serviço em domain/ports/ sem sufixo .repository
(password-hasher.ts, access-token-service.ts, refresh-token-generator.ts,
auth-event-logger.ts), os adapters correspondentes em infrastructure/auth/, e a configuração
tipada authConfig (infrastructure/config/auth-config.ts, exportada por env.ts) que o
container injeta nos adapters e no HTTP — nenhum adapter lê env.
Checklist — adicionar um agregado novo, na ordem
- Prisma: modelo em
server/prisma/schema.prisma+ migration nos dois dialetos (npm run --prefix server migrate:new -- <nome>, depoismigrate:deployaplica; não existe script de seed). - Entidade (e entidade de versão, se o agregado for versionado) — skill
server-entidade-dominio. - Erros de domínio no catálogo — skill
server-erros-dominio. - Porta em
domain/ports/— skillserver-repositorio. - Casos de uso, um arquivo por verbo — skill
server-caso-de-uso. - DTO (schemas Zod +
XResponseDto) — skillserver-servico-dto. - Service orquestrando os use cases e mapeando para DTO — mesma skill.
- Repositório Prisma implementando a porta — skill
server-repositorio. - Controller + rotas — skill
server-http. - Container: acrescente a fatia e exporte o controller — skill
server-di-container. routes/index.ts: monte o sub-router antes dos catch-alls/revenuese/expenses.- Testes: unit espelhando cada arquivo + um arquivo de integração — skill
server-testes. - Frontend: se houver código de erro novo, acrescente à tabela de
web/infra/api-error.ts. npm run validate:deps && npm run --prefix server typecheck && npm run --prefix server test.
Contrato HTTP
Rotas montadas na raiz, sem prefixo /api (o /api existe só no web, que repassa para a API
pelo route handler web/app/api/[...path]/route.ts):
/health aberta, 200 ou 503 com status "degraded"
/auth aberta: register, login, refresh, logout, logout-all (este exige Bearer)
/users \ GET /users/me
/expenses/categories |
/expenses/one-time |
/expenses/installment | atrás de authenticate(authenticateRequestUseCase):
/expenses/recurring | Authorization: Bearer <access token> → req.userId/req.sessionId
/revenues/one-time |
/revenues/fixed |
/data /
/revenues catch-all de consulta — DEPOIS dos específicos (também autenticado)
/expenses catch-all de consulta — DEPOIS dos específicos (também autenticado)
Um único authenticateMiddleware é criado em routes/index.ts e reaproveitado nas montagens.
O sub-router de /auth liga cookieParser() e requireTrustedOrigin(env.CORS_ORIGIN) só para
si, e recebe os rate limiters (createAuthRateLimiters) e os dois middlewares de autenticação por
opções.
Ambiente
infrastructure/config/env.ts chama dotenv.config(), faz safeParse no import e chama
process.exit(1) se falhar. Obrigatórias sem default: DATABASE_PROVIDER
(postgresql|sqlite|memory), DATABASE_URL, NODE_ENV, e as de autenticação JWT_SECRET,
JWT_KEY_ID, PASSWORD_PEPPERS (id:base64,…), PASSWORD_PEPPER_CURRENT_ID; TRUST_PROXY é
obrigatória fora de development/test ("true" é recusado). Com default: PORT (3000),
LOG_LEVEL (info), CORS_ORIGIN (http://localhost:3001, normalizado sem barra final), os TTLs
(ACCESS_TOKEN_TTL_SECONDS 600, REFRESH_TOKEN_TTL_SECONDS 604800,
SESSION_MAX_LIFETIME_SECONDS 2592000, REFRESH_REUSE_GRACE_SECONDS 30), o custo do Argon2
(ARGON2_MEMORY_KIB/TIME_COST/PARALLELISM, com pisos OWASP fora de test), o cookie
(AUTH_COOKIE_SECURE, ligado fora de dev/test; AUTH_COOKIE_PATH /api/auth) e os
AUTH_RATE_LIMIT_*. As regras entre variáveis ficam em checkAuthEnv (auth-config.ts), e
env.ts exporta também authConfig (buildAuthConfig(env)), a configuração já montada que o
container injeta. Lista comentada em server/.env.example.
Não faça
- Não importe
infrastructure/presentationa partir dedomainouapplication. - Não use tipo gerado do Prisma, tipo do Express ou schema do Zod dentro de
domain/. - Não crie barril novo (
index.tsde re-export). - Não use import relativo em
server/src/. - Não instancie service/repositório fora do
container.ts. - Não leia
envdentro de adapter, use case ou middleware: receba a fatia deauthConfigpelo construtor/factory. - Não importe
argon2/josefora deinfrastructure/nemcookie-parser/express-rate-limitfora depresentation/.