Skip to main content

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:

PastaUso
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:

CasoUse Server Component?
Buscar dados no banco/APISim
Renderizar HTML estático/dinâmicoSim
Acessar variáveis secretasSim
Usar useState, useEffect, eventos de cliqueNã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çãoEstratégia
Dados públicos e pouco mutáveisCache
Dashboard do usuário logadoDinâmico
Dados financeiros em tempo realDinâmico ou revalidação curta
Blog, landing page, catálogoCache/revalidação
Dados com permissão por usuárioCuidado 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>;
}

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:

CasoServer Action é boa opção?
Enviar formulário simplesSim
Criar item no bancoSim
Atualizar preferência do usuárioSim
Processamento complexo assíncronoMelhor API/queue
Integração pública externaMelhor 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:

PrefixoExposto no browser?
DATABASE_URLNão
JWT_SECRETNão
NEXT_PUBLIC_API_URLSim

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çãoQuando usar
Auth.jsLogin social, credenciais, sessão
ClerkAuth pronta como serviço
Lucia/implementação própriaControle maior
API externa com JWTQuando 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:

CasoBom uso?
Redirecionar usuário não logadoSim
Adicionar headers simplesSim
Validar regras levesSim
Consultar banco pesadoNão
Fazer lógica de domínioNã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çãoQuando usar
CSS ModulesSimples e isolado
Tailwind CSSProdutividade alta
Styled ComponentsDesign system específico
CSS globalReset, 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

PontoCuidado
Server vs Client ComponentNão coloque "use client" em tudo
CacheEntenda quando o dado pode ou não ser cacheado
Variáveis NEXT_PUBLIC_Tudo com esse prefixo vai para o browser
params e searchParamsEm versões recentes, trate como assíncrono
API internaNão chame sua própria API via HTTP sem necessidade
MiddlewareNão coloque lógica pesada nele
FormuláriosUse Server Actions para casos simples
AutenticaçãoPrefira cookie HTTP-only para sessão sensível
BuildSempre rode pnpm build antes de considerar pronto
EstruturaSepare por domínio se o projeto for crescer

Fluxo prático para começar sua ideia

  1. Crie o projeto:
pnpm create next-app@latest minha-app
  1. Crie as primeiras rotas:
src/app/page.tsx
src/app/dashboard/page.tsx
src/app/login/page.tsx
  1. Crie uma pasta de domínio:
src/features/tasks/
  1. Defina os tipos:
export type Task = {
id: string;
title: string;
status: "pending" | "doing" | "done";
};
  1. Crie uma função de busca:
export async function getTasks(): Promise<Task[]> {
return [
{
id: "1",
title: "Estudar Next.js",
status: "doing",
},
];
}
  1. 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.