Next.js em produção
Use para qualquer código em um app Next.js 16 App Router. Regras específicas de segurança (auth em actions, CSP, headers, taint) estão em nextjs-security; regras de container em docker-nextjs. A skill integrada next-best-practices do Claude Code cobre convenções gerais de arquivos, metadados, otimização de imagens e fontes em profundidade; esta skill adiciona apenas o que é específico para como construímos e entregamos.
Linha de base: Next 16.4, React 19.3, Turbopack (padrão para dev e build), cacheComponents: true com partialPrefetching: true, output: "standalone", typedRoutes: true. O next.config.ts do template é a fonte da verdade.
Regras
-
Componentes Servidor por padrão;
"use client"apenas para estado, efeitos, APIs de navegador ou manipuladores de eventos. Coloque a diretiva na menor folha que precisar dela. Não a adicione porque um arquivo importa nuvui: as partes stateful do nuvui carregam sua própria diretiva (veja a skillnuvui,rules/server-components.md).// Errado: página inteira é cliente por causa de um manipulador "use client"; export default function Page() { … <Select onValueChange={…} /> … } // Certo: a página permanece no servidor, o manipulador vive em uma folha export default async function Page() { const people = await getPeople(); return <PeopleFilter people={people} />; // people-filter.tsx tem "use client" }Props que cruzam para um componente cliente devem ser serializáveis: sem funções (exceto Server Actions), sem instâncias de classe.
-
Faça fetch no servidor, no componente que precisa dos dados. Sem fetch com
useEffect, sem route handler apenas para alimentar sua própria página. Desduplique por requisição comcache()do React; armazene em cache entre requisições com a regra 3. -
Cache com
"use cache"+cacheLife+cacheTag; todo o resto é dinâmico. Com Cache Components, nada é armazenado em cache a menos que você diga, efetchnão tem cache implícito.import { cacheLife, cacheTag } from "next/cache"; export async function getInvoice(id: InvoiceId) { "use cache"; cacheLife("minutes"); cacheTag(`invoice:${id}`); return db.invoice.findUnique({ where: { id } }); }Não use a configuração antiga de segmento (
export const dynamic,revalidate,fetchCache): ela gera erro sob Cache Components. Argumentos e valores fechados se tornam parte da chave de cache, então passe ids, não objetos de requisição inteiros. Detalhes, os perfis e"use cache: private": references/caching.md. -
Invalide com
updateTagem Server Actions,revalidateTag(tag, "max")em outros lugares.updateTag(tag)proporciona consistência de leitura-após-escrita e funciona apenas dentro de uma Server Action. De um webhook ou route handler userevalidateTag(tag, "max")(stale-while-revalidate) ourevalidateTag(tag, { expire: 0 }). A versão com um argumentorevalidateTag(tag)está obsoleta. -
Empacote dados em tempo de requisição dentro de
<Suspense>, ou a build falha.cookies(),headers(),searchParams,connection()e I/O não armazenado em cache fora de um limite Suspense interrompem o pré-renderização com "dados não armazenados em cache ou em tempo de execução durante pré-renderização". Dê a ele um limite com um fallback skeleton, armazene em cache, ou (apenas layout raiz, veja regra 14) marque a rota como bloqueante comexport const instant = false. Todosparams,searchParams,cookies(),headers()são assíncronos: useawaitneles. -
Toda Server Action valida sua entrada e verifica autorização por conta própria. Uma Server Action é um endpoint POST público independentemente da página em que está escrita. Analise
FormDatacom o schema detypescript-production(valibot), depois autorize, depois mutação, depoisupdateTag. Retorne um resultado tipado para falhas esperadas; lance apenas para bugs. O conjunto completo de regras está emnextjs-security."use server"; export async function renameProject(_: State, form: FormData): Promise<State> { const session = await requireSession(); // 401 se ausente const input = v.safeParse(RenameProject, Object.fromEntries(form)); if (!input.success) return { ok: false, errors: flatten(input.issues) }; await assertCanEdit(session, input.output.projectId); // 403 se não autorizado await db.project.update(…); updateTag(`project:${input.output.projectId}`); return { ok: true }; } -
Route handlers são para outros clientes (webhooks, mobile, health), não para suas próprias páginas. Coloque-os em
app/api/. Valide o corpo como uma ação, retorneResponse.json(...)e defina códigos de status intencionalmente. -
Todo app tem
GET /api/health, dinâmico e sem dependências. OHEALTHCHECKdo Docker e as probes do Kubernetes o acessam. Deve responder sem tocar no banco de dados, para que uma dependência lenta não reinicie todos os pods.await connection()impede que seja pré-renderizado em um arquivo estático. Está no template emsrc/app/api/health/route.ts;proxy.tsexclui/api/healthdo seu matcher. -
error.tsx,loading.tsxenot-found.tsxna raiz, e por segmento onde a UX difere.error.tsxé um componente cliente; registre através deonRequestErroreminstrumentation.ts, mostre uma tentativa comreset, nunca a mensagem de erro. ChamenotFound()para registros ausentes em vez de renderizar um estado vazio. -
NEXT_PUBLIC_*é incorporado na build; todo o resto é lido em tempo de execução no servidor. Uma imagem é promovida de main para produção inalterada, então um valorNEXT_PUBLIC_é o mesmo em todos os ambientes. Coloque apenas constantes de build lá (nome do app, um Sentry DSN público que é o mesmo em todos os lugares). Configuração por ambiente (URLs de API, feature flags, segredos) é umprocess.env.Xno lado do servidor, lido em um Componente Servidor ou ação, e passado para o cliente como props se necessário. Valide env uma vez emsrc/env.ts(valibot) e importe de lá. Nunca use a chaveenvemnext.config.ts: ela é inlinada em bundles do cliente. -
Marque módulos apenas do servidor com
import "server-only". Qualquer coisa que leia segredos, converse com o BD ou importe um SDK com credenciais. Uma importação cliente disso falha na build em vez de vazar. -
after()para trabalho que não deve atrasar a resposta (logs de auditoria, análises, aquecimento de cache). Leiacookies()/headers()antes de chamarafter, não dentro dele. Ele executa no mesmo processo, então o pod precisa de seu período de graça SIGTERM (o chart defineterminationGracePeriodSeconds). -
Instrumente com
src/instrumentation.ts:registerOTel+onRequestError. OpenTelemetry via@vercel/otel, exportador configurado pelas variáveis de ambiente padrãoOTEL_EXPORTER_OTLP_*em tempo de execução. Veja references/observability.md. -
Mantenha
output: "standalone"e nunca importe de fora do workspace. O runner Docker copia apenas.next/standalone,.next/staticepublic. No monorepo,server.jsestá em.next/standalone/apps/<app>/server.js; a raiz do workspace é encontrada a partir depnpm-lock.yaml, então não definaoutputFileTracingRootouturbopack.roota menos que a build avise. Detalhes de self-hosting (multi-replica, chaves Server Action,deploymentId): references/self-hosting.md. -
O layout raiz lê o nonce CSP, então o app renderiza por requisição. O
proxy.tsdo template define um CSP baseado em nonce esrc/app/layout.tsxtemexport const instant = false. Isso é uma troca deliberada: nonces e shells estáticos pré-renderizados não podem coexistir, e para apps de negócios logados um CSP estrito vence. O cache de dados com"use cache"ainda funciona. Não remova nenhuma das partes sem ler a regra 4 denextjs-security. -
Estilos: SCSS BEM global, importado pelo componente. Turbopack compila Sass com o pacote
sass;sassOptions.loadPaths: ["src/styles"]faz@use "abstracts"funcionar de qualquer profundidade. Sem imports~, semsassOptions.functions. Regras emscss-bem. -
Imagens através de
next/image, fontes através denext/font.sharpvem com Next para o otimizador; a imagem do runner não precisa de pacote extra. Imagens remotas precisam deimages.remotePatternscom um hostname exato. -
Verifique o tamanho do bundle com
pnpm next analyze(analisador do Turbopack;@next/bundle-analyzeré apenas webpack). Carregue componentes cliente pesados (editor nuvui, mapas, gráficos abaixo da dobra) comnext/dynamic. -
Use rotas tipadas e tipos gerados.
typedRoutes: trueverifica<Link href>;PageProps<"/invoices/[id]">eLayoutPropssão globais apósnext typegen(executado automaticamente pornext dev/next build). Não escreva tiposparamsmanualmente. -
Sem
next lint. Foi removido na versão 16. Biome faz lint (vejabiome); TypeScript verifica comtsc --noEmitpor pacote.
Verificar
pnpm typecheck
pnpm lint
pnpm build # turbo run build: falha em erros de pré-renderização, erros de tipo, erros Sass
pnpm --filter <app> start & # então:
curl -fsS localhost:3000/api/health
Fontes
- https://nextjs.org/docs/app/guides/upgrading/version-16
- https://nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents
- https://nextjs.org/docs/app/guides/migrating-to-cache-components
- https://nextjs.org/docs/app/api-reference/directives/use-cache
- https://nextjs.org/docs/app/api-reference/functions/revalidateTag
- https://nextjs.org/docs/app/api-reference/functions/updateTag
- https://nextjs.org/docs/messages/blocking-prerender-dynamic
- https://nextjs.org/docs/app/api-reference/functions/after
- https://nextjs.org/docs/app/guides/open-telemetry
- https://nextjs.org/docs/app/api-reference/config/next-config-js/output
- https://nextjs.org/docs/app/guides/self-hosting
- https://nextjs.org/docs/app/guides/environment-variables
- https://nextjs.org/docs/app/guides/sass
- https://nextjs.org/docs/app/guides/package-bundling
- https://nextjs.org/docs/app/api-reference/file-conventions/proxy