Início de projeto com Next.js
A base é mais barata de estabelecer no primeiro dia: depois, cada regra precisa ser introduzida em cima de código que a viola. Se o template frontend-starter existir — comece copiando o template: nele, os passos abaixo já estão feitos e verificados, e este documento serve como explicação das decisões e checklist de auditoria.
Este documento define a ordem dos passos e o conjunto mínimo. Como escrever o código em si — no skill fsd-frontend (se existir no projeto), autorização — em frontend-auth-session, verificação pré-commit — em anti-slop-review.
Configurações prontas — em references/configs.md, estrutura Next + FSD com exemplos de código — em references/next-structure.md.
Prioridade das regras. Instruções explícitas do usuário e arquivos agent do projeto (AGENTS.md, CLAUDE.md) são mais importantes que este documento. Versões de bibliotecas mudam: antes de copiar uma configuração, verifique-a com a documentação da versão instalada e não afirme que ela funciona até testar.
Fatos sobre Next.js — da documentação no pacote, não deste documento nem da memória. A partir da versão 16.2, a documentação está em node_modules/next/dist/docs/, e o AGENTS.md do projeto faz referência a ela. Em caso de divergência com os exemplos aqui, a documentação da versão instalada é a correta.
Notações: MUST — não pode ser violado; SHOULD — sim por padrão, desvio requer justificativa.
1. Decisões antes do código
Cada decisão da tabela é registrada no README (seção "Decisões") ou em ADR, com justificativa. Desvio do valor padrão sem justificativa registrada — não é feito.
| Pergunta | Padrão | Quando diferente |
|---|---|---|
| Next.js ou SPA | Next.js, se precisar de pelo menos um: SSR/SEO, BFF (segredos, HttpOnly-cookie), route handlers | Aplicação interna atrás de login sem SEO e sem BFF → Vite + React Router: build e deploy mais simples |
| Roteador | App Router | Pages Router — apenas em projeto existente |
| Monorepo | Não, uma aplicação | Segunda aplicação ou pacote realmente apareceu → pnpm workspaces; Nx/Turborepo — quando build ou CI começam a ficar lentos |
| Multi-Zones, micro-frontends | Não | Equipes independentes com releases independentes |
| Dados do servidor | Server Components para carregamento inicial; TanStack Query para dados interativos do cliente e mutações | Aplicação puramente cliente → apenas TanStack Query |
| Textos e i18n | Módulo de tradução desde o primeiro dia (next-intl ou similar), mesmo para um único locale, se o projeto seguir fsd-frontend | Decisão explícita "sem i18n" registrada no README |
| Gerenciador de pacotes | pnpm, versão em packageManager | — |
| Deploy | Decidir imediatamente: Vercel, servidor Node (output: "standalone") ou Docker | Disso dependem env, cache, CI |
MUST: não adicionar tecnologia "para o futuro". Monorepo, segundo roteador, Storybook, gerenciador de estado, MSW "como tipo de backend" aparecem quando têm um primeiro consumidor real.
2. Ordem dos passos
- Criar o projeto (
pnpm create next-app@latest): TypeScript, ESLint, Tailwind, App Router. FixarpackageManager,engines.node(Next 16 requer Node 20.9+) e.nvmrc. - Remover imediatamente o template do gerador: página inicial, SVG de
public/, README template, estilos de demonstração. Substituir README pelo esqueleto dereferences/configs.md. Um template deixado "para depois" chega à produção.AGENTS.mdeCLAUDE.mddocreate-next-appnão são removidos: o bloco entre<!-- BEGIN:nextjs-agent-rules -->e<!-- END:nextjs-agent-rules -->aponta os agentes para a documentação da versão instalada,next devo mantém sozinho. Regras próprias do projeto são escritas fora desses marcadores. - TypeScript estrito (seção 4 de
references/configs.md) e scripttypecheck(tsc --noEmit). - Estrutura FSD sobre Next (seção 3 abaixo).
- Linter e formatação: ESLint flat config do
eslint-config-next+ regras type-aware + linter de limites + Prettier. Next 16 removeunext linte não faz lint emnext build— o linter é executado como script separado e no CI. - Configuração de env (seção 4).
- Modelo de erros e cliente de API em
shared/api(seção 5). - Provedores na camada
_app: QueryClient, tema, i18n, serviços globais (toast, confirm) como porta emshared+ implementação em_app. - Base de UI (seção 6).
- Testes: Vitest + Testing Library + MSW, Playwright para e2e (seção 7).
- Git hooks: husky + lint-staged (eslint --fix, prettier) + commitlint.
- CI: lint, typecheck, test, build em cada push e PR (seção 8).
- Segurança padrão (seção 9).
- README preenchido com o que já é verdade (seção 10), e Definition of Done cumprido (seção 11).
Passos 1–3 — commit separado "scaffold": assim o histórico mostra o que o gerador fez e o que você fez.
3. FSD sobre o roteamento Next
Conforme o guia oficial do FSD para App Router: a pasta de serviço Next app/ — na raiz do projeto, as camadas FSD — em src/, e as camadas app e pages recebem o prefixo _ para não confundir com as pastas de serviço Next.
app/ roteamento Next: apenas arquivos de rota
├── layout.tsx layout raiz → provedores de src/_app
├── (auth)/sign-in/page.tsx export { SignInPage as default } from "@/_pages/sign-in"
└── api/auth/login/route.ts route handler (BFF), fino
proxy.ts Next 16+ (antes middleware.ts), junto com app/
src/
├── _app/ camada app: provedores, estilos globais, implementações de serviços
├── _pages/ camada pages
├── widgets/ (se o projeto os usar)
├── features/
├── entities/
└── shared/{ui,api,lib,config}
MUST:
- Arquivos em
app/são finos: reexportação da página desrc/_pages,generateMetadata, parsing deparams/searchParams(no Next 15+ éPromise, precisa deawait) e passagem como props. Sem lógica neles. - Route handlers e Server Actions são finos: analisar e validar entrada → chamar função de
src→ retornar resposta. Cada um deles é um ponto de entrada público: verifica autorização por conta própria, não confia emproxy.ts. - Não criar
src/appesrc/pages: Next procura pastas de serviço tanto na raiz quanto emsrc, e uma pasta com esse nome pode facilmente se tornar um segundo roteador. - Linter de limites conhece os prefixos: padrões
src/_app,src/_pages/*em vez desrc/app,src/pages/*(config —fsd-frontend,references/conventions.md, seção 11). "use client"é colocado o mais baixo possível na árvore. Página é servidora por padrão, as clientes são folhas com interatividade.- Módulos com segredos começam com
import "server-only", módulos comwindow— comimport "client-only".
Detalhado e com código: references/next-structure.md.
4. Configuração de ambiente
MUST:
- Variáveis são lidas em dois módulos:
shared/config/env.server.ts(import "server-only") eshared/config/env.client.ts. Esquema —zod, validação na importação: erro de configuração cai na inicialização, não na primeira requisição. No resto do código,process.envé proibido (regra de linter). NEXT_PUBLIC_*são embutidos no bundle no build e apenas com acesso diretoprocess.env.NEXT_PUBLIC_X: sem desestruturação eprocess.env[name]. Não é possível construir uma imagem para vários ambientes com diferentes valores públicos — esses valores são lidos no servidor e passados para baixo.- Segredos — apenas sem o prefixo
NEXT_PUBLIC_. .env.examplecontém cada variável com comentário e valor seguro;.env*em.gitignore, exceto.env.example. O build no CI usa valores de.env.example.
5. Modelo de erros e cliente de API
MUST:
- Erros são normalizados em
AppErrorcomkinduma vez, na fronteira deshared/api(modelo —fsd-frontend,references/conventions.md, seção 1). Acima deshared/api, ninguém analisaResponse, status e textos de erro. - Um
request()sobrefetch(ou cliente gerado, envolvido da mesma forma): propagasignal, traduz erro de rede paranetwork, status HTTP parakind, valida corpo da resposta com esquema. Exemplo —references/next-structure.md. - Tipo de erro — pelo código: status,
extensions.code(GraphQL responde 200 também para erros),codeno corpo. Se o backend retorna apenas texto — mapeamento por texto em um mapeador, com comentário de justificativa e testes. - Resposta do backend é mapeada para tipo de domínio na fronteira; DTOs não vão para a UI.
- Dados do usuário não são cacheados entre usuários: nada de
"use cache"/unstable_cachepara respostas que dependem da sessão. - Cliente é gerado a partir do esquema (OpenAPI, GraphQL), se o esquema existir. Esquema ou código gerado ficam no repositório: o CI não deve acessar o backend ao vivo.
SHOULD: interface sobre o transporte (AuthApi + fábrica) — apenas quando já tem um segundo consumidor (fsd-frontend, references/state-and-data.md, 2.1). Valor de configuração do tipo type: "mock" que lança "não implementado" não é criado.
6. Base de UI
- Tokens de design — variáveis CSS em
:root(e para tema escuro), conectadas ao Tailwind v4 via@theme. Nos componentes — apenas tokens; valores HEX crus e arbitrários (text-[#3a3a3a]) — exceção única com comentário. - Classes existem. Componentes copiados de shadcn/ui e exemplos vêm com seus próprios tokens (
bg-card,text-muted-foreground). Ou crie esses tokens, ou substitua as classes — senão o componente fica silenciosamente sem estilos. - Primitivas
shared/uisão construídas sobre biblioteca headless (Radix, React Aria) para acessibilidade; variantes —cva, junção de classes —cn(clsx+tailwind-merge). - Fontes —
next/font, imagens —next/imagecom dimensões,images.remotePatternslistados explicitamente. - Storybook — quando o kit de UI é reutilizado por várias aplicações ou equipes, não antes.
7. Testes
- Vitest + Testing Library + jsdom + MSW com
onUnhandledFrame: "error"(MSW 3; no MSW 2 —onUnhandledRequest). Config —references/configs.md. - Vitest não renderiza
asyncServer Components. Lógica deles deve ser extraída para funções e testada com testes unitários; a página em si — em e2e. - Playwright para 3–10 caminhos críticos. No CI, e2e rodam contra backend previsível (servidor mock, ambiente com BD de teste), não contra ambiente compartilhado onde testes alteram dados de outros.
- O primeiro teste é escrito no dia zero: ele verifica que a infraestrutura de testes funciona e define um padrão.
8. CI
MUST: em cada push para branch principal e em cada PR: lint, typecheck, test, build. Template do GitHub Actions — references/configs.md. Para repositórios públicos, Actions é gratuito; para privados, há uma cota mensal de minutos.
- Build no CI usa
.env.example, sem segredos. - e2e — job separado, quando há backend isolado; até lá, marcação explícita no README explicando por que não estão no CI.
- Badge do CI no README aparece junto com o workflow.
9. Segurança padrão
poweredByHeader: false; cabeçalhos viaheaders()emnext.config.ts:X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin,X-Frame-Options: DENY(ouframe-ancestorsno CSP). CSP com nonce — tarefa separada, mas a decisão "existe / não existe e por quê" é registrada imediatamente.- Server Actions e route handlers — endpoints públicos: validação de entrada com esquema e verificação de permissões em cada um.
dangerouslySetInnerHTML— apenas com sanitizador e comentário de justificativa.- Dados pessoais e tokens não são logados.
- Autorização — conforme
frontend-auth-session.
10. README
Esqueleto — references/configs.md. Regra única: no README apenas o que é verdade para o código neste commit. Cada seção é atualizada no mesmo commit que o código que ela descreve. Planos — em seção separada "Próximos passos", com nota de que não foram implementados.
11. Definition of Done para início
- Template do gerador removido; README — esqueleto preenchido com a verdade.
-
packageManager,engines.node,.nvmrcfixados. - TS strict;
typecheck,lint,test,buildpassam localmente (executados, não presumidos). - Estrutura FSD com prefixos
_app/_pages; linter de limites detecta importação para cima. - Env através de dois módulos com esquema;
process.envfora deles proibido por linter;.env.examplecompleto. -
AppErrorerequest()emshared/api, cobertos por testes (rede, status, esquema inválido). - Provedores em
_app; QueryClient não compartilhado entre usuários no servidor. - Tokens de design em variáveis CSS; todas as classes usadas existem.
- Vitest + MSW + Playwright configurados, pelo menos um teste vivo de cada.
- Git hooks e commitlint funcionando.
- CI verde.
- Cabeçalhos de segurança e
poweredByHeader: false. - Decisões da seção 1 registradas.
12. Quando estiver em dúvida
- Entre "adicionar agora" e "adicionar quando precisar" — quando precisar.
- Entre "configurar manualmente" e "deixar como gerado pelo create-next-app" — deixe, enquanto não houver motivo para mudar, e registre o motivo quando mudar.
- Entre "uma aplicação" e "monorepo" — uma aplicação.
- Entre "config de memória" e "config da documentação da versão atual" — da documentação.