Este documento é o padrão técnico do projeto. Ele existe para que qualquer pessoa do time consiga implementar uma issue sem precisar decidir de novo "como autentico essa rota?" ou "onde isso deve morar no monorepo?". Se você notar o código real divergindo do que está descrito aqui, atualize este arquivo no mesmo PR — ele deve sempre refletir o padrão vigente, não o ideal.
Contexto de arquitetura mais detalhado (motivação das escolhas, trade-offs, decisões em
aberto) está em arquitetura-tecnica.md no repositório project-management. Este arquivo é
o "como fazer", aquele é o "por que decidimos assim".
Turborepo + npm workspaces.
apps/
api/ # NestJS — toda a lógica de negócio e acesso a dados vive aqui
web/ # Next.js (App Router) — só UI. Não acessa o banco, só chama apps/api via HTTP
packages/
auth/ # Guards, strategies e helpers de autenticação/autorização (usado só pela api)
database/ # schema.prisma, migrations, client Prisma exportado (usado só pela api)
events/ # event emitter interno + tipos de evento (usado só pela api)
shared-types/ # DTOs/interfaces compartilhados entre api e web — contrato da API vive aqui
ui/ # componentes de UI compartilhados (usado só pela web)
domains/
roadmaps/ # trilhas
curation/ # curadoria
feed/
gamification/
certification/
content/ # conteúdos educacionais
profile/
search/
Regra de organização por domínio: dentro de apps/api/src/, cada domínio dos Requisitos
Funcionais vira um módulo Nest próprio (auth/, roadmaps/, curation/, feed/,
gamification/, certification/, content/, profile/, search/), seguindo o modelo já
existente em apps/api/src/auth/ (*.controller.ts, *.service.ts, *.module.ts, dto.ts).
Regra de negócio pesada ou reaproveitável entre módulos vai para o package correspondente em
packages/domains/<dominio> — módulos "finos" na api chamando lógica de domínio nesses
packages, não o contrário.
Nunca:
- Lógica de negócio ou acesso ao Prisma dentro de
apps/web. - Um domínio importando o client Prisma diretamente sem passar pela camada de serviço do
módulo — sempre acesse dados através do
*.service.tsdo domínio dono da entidade.
O padrão de autenticação já está implementado em packages/auth e apps/api/src/auth/.
Nenhuma issue de implementação deve reinventar isso — deve reutilizar.
-
Login/registro por e-mail e senha:
AuthServiceemapps/api/src/auth/auth.service.ts, senha com hash viahashPassword/comparePassword(bcrypt,packages/auth/src/password.ts). Nunca compare senha manualmente nem armazene em texto plano. -
Emissão de token: JWT assinado em
AuthService.buildResponse, payload{ sub: userId, role }. Não crie um segundo mecanismo de sessão/token — se precisar de mais claims no payload, altere esse ponto único. -
Toda rota que exige usuário autenticado deve usar o guard já existente:
import { JwtAuthGuard } from '@aprendaufu/auth'; @UseGuards(JwtAuthGuard) @Get('me') getProfile(@Req() req) { return req.user; // { userId, role } — populado pelo JwtStrategy }
-
Toda rota restrita por papel (ex.: só professor pode validar conteúdo) combina o guard de auth com
RolesGuard+ decorator@Roles:import { JwtAuthGuard, RolesGuard, Roles } from '@aprendaufu/auth'; @UseGuards(JwtAuthGuard, RolesGuard) @Roles('teacher') @Post(':id/validar') validar(@Param('id') id: string) { ... }
JwtAuthGuardsempre vem antes deRolesGuardna lista —RolesGuarddepende dereq.userjá estar populado. -
Os papéis (
Role) são definidos no enum do Prisma (packages/database/prisma/schema.prisma). Se um requisito precisar de um papel novo, adicione ao enum ali — não crie strings de papel soltas no código do domínio. -
OAuth (Google / conta institucional .edu.br): ainda não implementado. Ao implementar, siga o mesmo padrão — nova
PassportStrategyempackages/auth, exportada porpackages/auth/src/index.ts, consumida peloAuthModule. Não crie um fluxo de auth paralelo fora depackages/auth. -
Frontend: nunca chame
fetchdiretamente para a API. Use o wrapperapps/web/src/lib/api-client.ts(apiClient.get/post), que já anexa o headerAuthorization: Bearer <token>automaticamente a partir do que está salvo porsaveSession(apps/web/src/lib/auth-client.ts). Se seu endpoint tiver um método HTTP que oapiClientainda não expõe (put,delete), adicione lá — não implemente uma chamada solta no componente.- Nota de segurança conhecida: o token hoje é salvo em
localStorage, não em cookie httpOnly (diferente do que estava emarquitetura-tecnica.md). É o padrão vigente — siga-o por consistência — mas isso significa que uma XSS na aplicação rouba o token. Por isso a sanitização de qualquer conteúdo renderizado que venha de outro usuário (bio, comentário, nome de trilha, etc.) não é opcional. Ver checklist de segurança abaixo.
- Nota de segurança conhecida: o token hoje é salvo em
O ValidationPipe global (apps/api/src/main.ts) já está configurado com whitelist: true —
ou seja, qualquer campo do body que não estiver declarado no DTO é descartado
silenciosamente, e campos sem decorator de validação não são validados.
Isso significa que toda rota que recebe body precisa de uma classe DTO com decorators do
class-validator, seguindo o padrão de apps/api/src/auth/dto.ts:
import { IsEmail, MinLength, IsOptional, IsIn } from 'class-validator';
import type { CreateRoadmapDto } from '@aprendaufu/shared-types';
export class CreateRoadmapBody implements CreateRoadmapDto {
@MinLength(3)
titulo: string;
@IsIn(['tecnologia', 'ciencia', 'educacao'])
categoria: string;
}Regras:
- O tipo do DTO (a interface, sem os decorators) é definido em
packages/shared-typese importado tanto pelaapi(que implementa a classe com validação) quanto pelaweb(que usa o tipo para tipar oapiClient.post<T>(...)). Isso é o contrato de API do projeto — se um endpoint novo não tem tipo emshared-types, frontend e backend vão divergir sem o TypeScript avisar. Nenhuma issue de implementação está completa sem atualizar esse package. - Nunca use
anyou@Body() body: Record<string, any>num controller — sempre uma classe DTO tipada. - IDs recebidos por
@Paramque se referem a outra entidade devem ser validados quanto à existência (404) antes de qualquer efeito colateral.
Aplicar a toda rota nova antes de abrir o PR:
- Rota protegida por
JwtAuthGuardquando exige usuário logado (ver seção 2) — rotas públicas (catálogo, perfil público, verificação de certificado) são exceção explícita, não omissão. -
RolesGuard+@Roles(...)quando a ação é restrita a um papel (ex.: curadoria é só professor). - Autorização de dono do recurso verificada quando aplicável —
JwtAuthGuardgarante que alguém está logado, não que é o dono do recurso. Editar o próprio perfil, por exemplo, precisa compararreq.user.userIdcom o id do recurso, não só checar autenticação. - Body validado por DTO com
class-validator(seção 3) — nunca confiar em campo vindo do cliente sem decorator de validação. - Nenhum dado sensível retornado nas respostas —
passwordHashnunca deve sair deAuthService/UserService; monte um DTO de resposta explícito em vez de retornar a entidade do Prisma direto. - Conteúdo gerado por usuário (bio, comentário, título de trilha indicada, nota de curadoria) é tratado como não confiável ao ser exibido no frontend — ver nota de XSS na seção 2.
- Erros retornados ao cliente não vazam detalhe interno (stack trace, query SQL) — use as
exceções do NestJS (
UnauthorizedException,ForbiddenException,NotFoundException,ConflictException) como noAuthService.
Rate limiting global ainda não está implementado no projeto — se a sua issue for explicitamente sobre isso, trate como decisão de arquitetura nova (discutir antes, não decidir sozinho dentro da issue).
- Schema único em
packages/database/prisma/schema.prisma. Nomes de campo emcamelCaseno Prisma, mapeados parasnake_caseno Postgres via@map/@@map(siga o padrão já usado emUser/AuthAccount). - Toda alteração de schema vem com a migration gerada (
prisma migrate dev) no mesmo PR — nunca editar uma migration já commitada. - Só a
apiacessa o Prisma. Se dois domínios precisam da mesma entidade, o dono do domínio expõe um método de serviço; não importe o client Prisma direto num módulo que não é dono da tabela. - Consulte
modelo-de-dados-proposto.md(no repoproject-management) antes de criar uma entidade nova — é a proposta de modelo já derivada dos requisitos; se sua issue exigir uma tabela que não está lá, atualize aquele documento também.
Duas camadas, conforme arquitetura-tecnica.md:
- Síncrono in-process — reações imediatas dentro do mesmo request (ex.: concluir parada →
atualizar progresso → checar XP). Usa
@nestjs/event-emitter, tipos e nomes de evento centralizados empackages/events. - Outbox — eventos que precisam sobreviver a uma falha do processo (ex.: emissão de
certificado, e-mail). Grave na tabela
eventosna mesma transação da mudança de negócio; não dispare o efeito colateral (gerar PDF, enviar e-mail) diretamente no request.
Todo evento novo — síncrono ou outbox — é declarado em packages/events (nome + payload
tipado) antes de ser publicado ou consumido. Não use strings soltas como nome de evento.
- Páginas em
apps/web/src/app/<rota>/page.tsx(App Router). Componentes de UI reutilizáveis entre domínios vão parapackages/ui; componentes específicos de uma tela ficam emapps/web/src/components/. - Toda chamada à API passa pelo
apiClient(seção 2) e é tipada com o DTO deshared-types— nuncafetchcru, nuncaanyna resposta. - Estado de sessão (token, usuário logado) só é lido/escrito via
apps/web/src/lib/auth-client.ts.
- Unitário: Jest em
apps/api(regras de negócio, principalmente as RNs do documento de requisitos), Vitest emapps/web. - E2E (Playwright): reservado para os fluxos críticos — login, concluir trilha até emitir certificado, validar conteúdo na curadoria. Não é esperado E2E para toda issue.
- PR que adiciona lógica de negócio nova (serviço, guard, cálculo) vem com teste unitário cobrindo o caminho feliz e pelo menos um caso de erro/borda.
Antes de pedir revisão, confirme:
- Segue a organização por domínio da seção 1 (nada de lógica de negócio em
apps/web). - Checklist de segurança da seção 4 aplicado, se a issue envolve rota nova.
- DTOs adicionados/atualizados em
packages/shared-typespara qualquer campo novo de request ou response. - Migration do Prisma incluída, se o schema mudou.
- Eventos novos declarados em
packages/events, se aplicável. -
npm run lintenpm run testpassam na raiz do monorepo. - Critérios de aceite da issue foram todos verificados manualmente (rodando a aplicação), não só "o código compila".
npm install # na raiz — instala para todos os workspaces
npm run dev # sobe api (porta 3001) e web (porta 3000) via turboVariáveis de ambiente: copie apps/api/.env.example para apps/api/.env e preencha
DATABASE_URL e JWT_SECRET; apps/web/.env.local já aponta para a API local por padrão.