Next.js 16 — Guia prático para começar um projeto | Next.js
- Next.js 16 — Guia prático para começar um projeto | Next.js
Esta doc é para começar rápido com Next.js 16, usando o App Router, que hoje é o modelo principal do framework para criar aplicações React fullstack. O Next.js 16 usa recursos recentes do React, incluindo React 19.2 no App Router, e traz melhorias importantes em build, cache e performance. (Next.js)
Criando o projeto
pnpm create next-app@latest minha-app
cd minha-app
pnpm dev
A documentação oficial recomenda usar o create-next-app, porque ele já configura TypeScript, ESLint, estrutura inicial e aliases de importação. (Next.js)
Durante a criação, normalmente você verá opções parecidas com:
TypeScript? yes
ESLint? yes
Tailwind CSS? yes/no
App Router? yes
src directory? yes/no
Turbopack? yes
Import alias? @/*
Minha recomendação prática:
TypeScript: yes
ESLint: yes
Tailwind: yes, se for usar CSS utilitário
App Router: yes
src directory: yes
Turbopack: yes
Alias: @/*
Estrutura básica recomendada
src/
app/
layout.tsx
page.tsx
loading.tsx
error.tsx
not-found.tsx
api/
health/
route.ts
components/
ui/
layout/
features/
products/
components/
services/
types.ts
lib/
http.ts
env.ts
styles/
Sugestão prática:
| Pasta | Uso |
|---|---|
app/ | Rotas, páginas, layouts e APIs |
components/ | Componentes reutilizáveis |
features/ | Código separado por domínio |
lib/ | Funções utilitárias, clients, validações |
styles/ | CSS global ou arquivos auxiliares |
Página inicial
No App Router, a página inicial fica em:
src/app/page.tsx
Exemplo:
export default function HomePage() {
return (
<main>
<h1>Minha aplicação</h1>
<p>Projeto iniciado com Next.js 16.</p>
</main>
);
}
O arquivo page.tsx representa uma rota acessível no navegador.
src/app/page.tsx -> /
src/app/about/page.tsx -> /about
src/app/products/page.tsx -> /products
Layout global
O layout principal fica em:
src/app/layout.tsx
Exemplo:
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "Minha App",
description: "Projeto com Next.js 16",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="pt-BR">
<body>{children}</body>
</html>
);
}
Detalhe importante: o layout.tsx envolve as páginas filhas. Use ele para estrutura comum, como menu, providers, fontes e estilos globais.
Criando rotas
Para criar uma rota /dashboard:
src/app/dashboard/page.tsx
export default function DashboardPage() {
return <h1>Dashboard</h1>;
}
Para criar uma rota dinâmica /products/123:
src/app/products/[id]/page.tsx
type ProductPageProps = {
params: Promise<{
id: string;
}>;
};
export default async function ProductPage({ params }: ProductPageProps) {
const { id } = await params;
return <h1>Produto {id}</h1>;
}
No Next.js 15+ e 16, APIs como params, searchParams, cookies() e headers() passaram a ser assíncronas em vários contextos. Esse é um detalhe importante ao seguir exemplos antigos da internet.
Server Components e Client Components
Por padrão, componentes dentro do App Router são Server Components.
Exemplo de Server Component:
export default async function ProductsPage() {
const response = await fetch("https://api.exemplo.com/products");
const products = await response.json();
return (
<main>
<h1>Produtos</h1>
{products.map((product: any) => (
<p key={product.id}>{product.name}</p>
))}
</main>
);
}
Use Server Components para:
| Caso | Use Server Component? |
|---|---|
| Buscar dados no banco/API | Sim |
| Renderizar HTML estático/dinâmico | Sim |
| Acessar variáveis secretas | Sim |
Usar useState, useEffect, eventos de clique | Não |
Para usar estado, evento ou hook de browser, coloque "use client" no topo do arquivo:
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((current) => current + 1)}>
Contador: {count}
</button>
);
}
Regra prática:
Server Component: dados, renderização, segurança
Client Component: interação, estado, browser APIs
Buscando dados
A documentação oficial mostra que Server Components podem buscar dados usando fetch, ORM ou acesso direto ao banco. (Next.js)
Exemplo com fetch:
type Product = {
id: string;
name: string;
price: number;
};
async function getProducts(): Promise<Product[]> {
const response = await fetch("https://api.exemplo.com/products");
if (!response.ok) {
throw new Error("Erro ao buscar produtos");
}
return response.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<main>
<h1>Produtos</h1>
{products.map((product) => (
<article key={product.id}>
<h2>{product.name}</h2>
<p>R$ {product.price}</p>
</article>
))}
</main>
);
}
Cache: detalhe que você precisa ficar de olho
Cache é uma das partes mais importantes do Next.js moderno. No Next.js 16, existe o modelo de Cache Components, habilitado com cacheComponents: true no next.config.ts. A documentação atual separa esse modelo do modelo anterior de cache/revalidação. (Next.js)
Exemplo de next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;
Regra prática:
| Situação | Estratégia |
|---|---|
| Dados públicos e pouco mutáveis | Cache |
| Dashboard do usuário logado | Dinâmico |
| Dados financeiros em tempo real | Dinâmico ou revalidação curta |
| Blog, landing page, catálogo | Cache/revalidação |
| Dados com permissão por usuário | Cuidado extremo com cache |
No modelo anterior, ainda muito encontrado em projetos e tutoriais, você verá:
await fetch("https://api.exemplo.com/products", {
next: {
revalidate: 3600,
},
});
Isso revalida os dados a cada 3600 segundos no modelo tradicional de cache. (Next.js)
Para desabilitar cache:
await fetch("https://api.exemplo.com/user", {
cache: "no-store",
});
Use no-store quando o dado for sensível, personalizado por usuário ou precisar ser sempre atual.
Loading
Crie um arquivo:
src/app/products/loading.tsx
export default function Loading() {
return <p>Carregando produtos...</p>;
}
O loading.tsx é usado automaticamente enquanto a rota carrega.
Error boundary
Crie:
src/app/products/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<main>
<h1>Erro ao carregar produtos</h1>
<p>{error.message}</p>
<button onClick={reset}>
Tentar novamente
</button>
</main>
);
}
Detalhe importante: error.tsx precisa ser Client Component, por isso usa "use client".
Página 404
Crie:
src/app/not-found.tsx
export default function NotFound() {
return (
<main>
<h1>Página não encontrada</h1>
<p>A rota acessada não existe.</p>
</main>
);
}
Para disparar manualmente:
import { notFound } from "next/navigation";
export default async function ProductPage() {
const product = null;
if (!product) {
notFound();
}
return <h1>Produto</h1>;
}
Navegação entre páginas
Use o componente Link:
import Link from "next/link";
export function Header() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/products">Produtos</Link>
<Link href="/dashboard">Dashboard</Link>
</nav>
);
}
Para navegação via código em Client Component:
"use client";
import { useRouter } from "next/navigation";
export function GoToDashboardButton() {
const router = useRouter();
return (
<button onClick={() => router.push("/dashboard")}>
Ir para dashboard
</button>
);
}
API Routes
No App Router, APIs ficam em arquivos route.ts.
Exemplo:
src/app/api/health/route.ts
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({
status: "ok",
});
}
A URL será:
GET /api/health
Exemplo com POST:
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const body = await request.json();
return NextResponse.json({
received: body,
});
}
Server Actions
Server Actions permitem executar lógica no servidor diretamente a partir de formulário ou chamada controlada.
Exemplo simples:
async function createProduct(formData: FormData) {
"use server";
const name = formData.get("name");
console.log("Criando produto:", name);
}
export default function NewProductPage() {
return (
<form action={createProduct}>
<input name="name" placeholder="Nome do produto" />
<button type="submit">Salvar</button>
</form>
);
}
Use Server Actions para casos simples, como:
| Caso | Server Action é boa opção? |
|---|---|
| Enviar formulário simples | Sim |
| Criar item no banco | Sim |
| Atualizar preferência do usuário | Sim |
| Processamento complexo assíncrono | Melhor API/queue |
| Integração pública externa | Melhor API Route |
Variáveis de ambiente
Crie um arquivo:
.env.local
Exemplo:
DATABASE_URL="postgresql://user:pass@localhost:5432/app"
NEXT_PUBLIC_API_URL="https://api.exemplo.com"
JWT_SECRET="segredo"
Regra importante:
| Prefixo | Exposto no browser? |
|---|---|
DATABASE_URL | Não |
JWT_SECRET | Não |
NEXT_PUBLIC_API_URL | Sim |
Tudo que começa com NEXT_PUBLIC_ pode ir para o bundle do frontend. Nunca coloque segredo com esse prefixo.
Exemplo de uso no servidor:
const databaseUrl = process.env.DATABASE_URL;
Exemplo de uso no client:
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
Organização por feature
Para projeto que tende a crescer, evite colocar tudo em components.
Sugestão:
src/
features/
products/
components/
ProductCard.tsx
ProductForm.tsx
services/
get-products.ts
create-product.ts
types.ts
Exemplo:
// src/features/products/types.ts
export type Product = {
id: string;
name: string;
price: number;
};
// src/features/products/services/get-products.ts
import type { Product } from "../types";
export async function getProducts(): Promise<Product[]> {
const response = await fetch("https://api.exemplo.com/products", {
cache: "no-store",
});
if (!response.ok) {
throw new Error("Erro ao buscar produtos");
}
return response.json();
}
// src/app/products/page.tsx
import { getProducts } from "@/features/products/services/get-products";
export default async function ProductsPage() {
const products = await getProducts();
return (
<main>
<h1>Produtos</h1>
{products.map((product) => (
<p key={product.id}>{product.name}</p>
))}
</main>
);
}
Formulários
Para formulário simples:
async function save(formData: FormData) {
"use server";
const name = String(formData.get("name") ?? "");
if (!name) {
throw new Error("Nome é obrigatório");
}
console.log(name);
}
export default function Page() {
return (
<form action={save}>
<input name="name" />
<button type="submit">Salvar</button>
</form>
);
}
Para formulário mais complexo, use:
pnpm add react-hook-form zod @hookform/resolvers
Exemplo de schema:
import { z } from "zod";
export const productSchema = z.object({
name: z.string().min(1, "Nome é obrigatório"),
price: z.coerce.number().positive("Preço inválido"),
});
export type ProductFormData = z.infer<typeof productSchema>;
Chamando sua própria API
Em Server Component, evite chamar sua própria API HTTP sem necessidade.
Em vez disso:
// Evite, se está no mesmo projeto:
await fetch("http://localhost:3000/api/products");
Prefira chamar a função diretamente:
import { getProducts } from "@/features/products/services/get-products";
const products = await getProducts();
Motivo: você evita overhead HTTP desnecessário.
Autenticação
Para autenticação, caminhos comuns:
| Opção | Quando usar |
|---|---|
| Auth.js | Login social, credenciais, sessão |
| Clerk | Auth pronta como serviço |
| Lucia/implementação própria | Controle maior |
| API externa com JWT | Quando backend já existe |
Para projeto próprio com backend separado, geralmente o frontend Next.js consome sua API e armazena sessão/token com cuidado. Evite jogar JWT sensível em localStorage se puder usar cookie HTTP-only.
Middleware
O middleware roda antes da request chegar na rota.
Arquivo:
src/middleware.ts
Exemplo:
import { NextResponse type NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("token");
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};
Use middleware para:
| Caso | Bom uso? |
|---|---|
| Redirecionar usuário não logado | Sim |
| Adicionar headers simples | Sim |
| Validar regras leves | Sim |
| Consultar banco pesado | Não |
| Fazer lógica de domínio | Não |
Imagens
Use next/image:
import Image from "next/image";
export function Avatar() {
return (
<Image
src="/avatar.png"
alt="Avatar"
width={80}
height={80}
/>
);
}
Para imagem externa, configure domínio no next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.exemplo.com",
},
],
},
};
export default nextConfig;
Metadata e SEO
Em páginas estáticas:
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Produtos",
description: "Lista de produtos da aplicação",
};
export default function ProductsPage() {
return <h1>Produtos</h1>;
}
Metadata dinâmica:
import type { Metadata } from "next";
type Props = {
params: Promise<{
id: string;
}>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { id } = await params;
return {
title: `Produto ${id}`,
};
}
export default async function ProductPage({ params }: Props) {
const { id } = await params;
return <h1>Produto {id}</h1>;
}
CSS
Você pode usar:
| Opção | Quando usar |
|---|---|
| CSS Modules | Simples e isolado |
| Tailwind CSS | Produtividade alta |
| Styled Components | Design system específico |
| CSS global | Reset, variáveis e base |
Exemplo com CSS Module:
src/components/Button.module.css
.button {
padding: 8px 12px;
border-radius: 8px;
}
import styles from "./Button.module.css";
export function Button() {
return <button className={styles.button}>Salvar</button>;
}
Comandos principais
pnpm dev
Sobe o ambiente de desenvolvimento.
pnpm build
Gera build de produção.
pnpm start
Executa a aplicação em modo produção depois do build.
pnpm lint
Executa validações de lint.
Docker básico
Exemplo simples de Dockerfile:
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN corepack enable && pnpm build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["pnpm", "start"]
Para produção mais otimizada, depois você pode usar output: "standalone" no next.config.ts.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;
Checklist do que ficar de olho
| Ponto | Cuidado |
|---|---|
| Server vs Client Component | Não coloque "use client" em tudo |
| Cache | Entenda quando o dado pode ou não ser cacheado |
Variáveis NEXT_PUBLIC_ | Tudo com esse prefixo vai para o browser |
params e searchParams | Em versões recentes, trate como assíncrono |
| API interna | Não chame sua própria API via HTTP sem necessidade |
| Middleware | Não coloque lógica pesada nele |
| Formulários | Use Server Actions para casos simples |
| Autenticação | Prefira cookie HTTP-only para sessão sensível |
| Build | Sempre rode pnpm build antes de considerar pronto |
| Estrutura | Separe por domínio se o projeto for crescer |
Fluxo prático para começar sua ideia
- Crie o projeto:
pnpm create next-app@latest minha-app
- Crie as primeiras rotas:
src/app/page.tsx
src/app/dashboard/page.tsx
src/app/login/page.tsx
- Crie uma pasta de domínio:
src/features/tasks/
- Defina os tipos:
export type Task = {
id: string;
title: string;
status: "pending" | "doing" | "done";
};
- Crie uma função de busca:
export async function getTasks(): Promise<Task[]> {
return [
{
id: "1",
title: "Estudar Next.js",
status: "doing",
},
];
}
- Use na página:
import { getTasks } from "@/features/tasks/services/get-tasks";
export default async function DashboardPage() {
const tasks = await getTasks();
return (
<main>
<h1>Minhas tarefas</h1>
{tasks.map((task) => (
<article key={task.id}>
<h2>{task.title}</h2>
<p>{task.status}</p>
</article>
))}
</main>
);
}
Modelo mental simples
Pense assim:
app/ -> rotas da aplicação
page.tsx -> tela de uma rota
layout.tsx -> estrutura compartilhada
loading.tsx -> estado de carregamento
error.tsx -> erro da rota
route.ts -> endpoint de API
components/ -> componentes reutilizáveis
features/ -> regra e UI por domínio
lib/ -> utilitários técnicos
Recomendações iniciais
Para um projeto novo em Next.js 16, comece simples:
App Router
TypeScript
Tailwind, se quiser produtividade visual
Server Components por padrão
Client Components só quando precisar de interação
Server Actions para formulários simples
API Routes para integrações externas
Cache explícito e consciente
O erro mais comum é transformar tudo em Client Component e perder os benefícios do framework. Comece pelo servidor, desça para o client apenas quando precisar de estado, evento, hook ou API do navegador.