App Router no Next.js: Layout, Page e Route
1. Visão geral
App Router é o mecanismo de roteamento padrão do Next.js a partir da versão 13, substituindo gradualmente o Pages Router (pages/). É baseado na convenção de arquivos no diretório app/: page.tsx cria rotas, layout.tsx cria UI compartilhada, route.ts cria endpoints de API. Após ler este artigo, você saberá como organizar um projeto Next.js com App Router, escrever layouts aninhados, criar rotas dinâmicas e route handlers, além de como testar esses componentes.
2. Estrutura de diretórios básica
App Router usa pasta = segmento de URL. Cada pasta em app/ pode conter arquivos especiais que determinam o comportamento da rota.
app/
├── layout.tsx # layout raiz, obrigatoriamente contém <html>, <body>
├── page.tsx # rota "/"
├── about/
│ └── page.tsx # rota "/about"
├── products/
│ ├── layout.tsx # layout específico para /products/*
│ ├── page.tsx # rota "/products"
│ └── [id]/
│ └── page.tsx # rota "/products/123"
└── api/
└── orders/
└── route.ts # API "/api/orders"
layout.tsxenvolve todos ospage.tsxno mesmo nível ou mais profundos.page.tsxé o ponto final que realmente renderiza o conteúdo para uma URL.route.tsnão renderiza UI, retorna umaResponsecomo um endpoint REST.
Observação: Os nomes das pastas que não começam com letras minúsculas (por exemplo
Products) ainda funcionam, mas Next.js recomenda usar kebab-case ou camelCase consistentemente para evitar confusão em sistemas de arquivos sensíveis a maiúsculas/minúsculas.
3. Page — definindo rotas
O arquivo page.tsx exporta um componente React que funciona como a UI principal da rota. Por padrão, é um Server Component.
// app/products/page.tsx
import { ProductService } from "@/services/product-service";
export default async function ProductsPage() {
const products = await ProductService.findAll();
return (
<main>
<h1>Lista de produtos</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name} - {product.price}đ</li>
))}
</ul>
</main>
);
}
async function ProductsPagepermite usarawaitnos dados diretamente no componente, pois é um Server Component, sem necessidade deuseEffectougetServerSideProps.- Next.js renderiza automaticamente HTML no servidor e envia para o navegador com o conteúdo já pronto — ótimo para SEO e tempo de primeira exibição.
- A rota corresponde à URL
/products.
3.1. Rota dinâmica com params
// app/products/[id]/page.tsx
import { ProductService } from "@/services/product-service";
import { notFound } from "next/navigation";
type ProductPageProps = {
params: { id: string };
};
export default async function ProductDetailPage({ params }: ProductPageProps) {
const product = await ProductService.findById(params.id);
if (!product) {
notFound();
}
return <h1>{product.name}</h1>;
}
- A pasta
[id]cria um segmento dinâmico, Next.js injeta o valor real emparams.id. notFound()renderiza o arquivonot-found.tsxmais próximo em vez de derrubar a app — esta é a forma padrão de lidar com 404 no App Router.
4. Layout — UI compartilhada
Layout recebe a prop children que são as rotas filhas, mantém o estado intacto ao navegar entre páginas do mesmo layout (por exemplo, a sidebar não re-renderiza).
// app/products/layout.tsx
export default function ProductsLayout({ children }: { children: React.ReactNode }) {
return (
<div className="products-layout">
<aside>Filtro de produtos</aside>
<section>{children}</section>
</div>
);
}
childrenaqui é o conteúdo deapp/products/page.tsxouapp/products/[id]/page.tsx.- Ao navegar de
/productspara/products/123, a parte<aside>não sofre unmount/remount — esta é a principal vantagem do layout em comparação com importar manualmente um componente compartilhado em cada página.
4.1. Root layout é obrigatório
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Shop Nón Lá",
description: "Loja de demonstração do App Router",
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="pt-BR">
<body>
<header>Shop Nón Lá</header>
{children}
</body>
</html>
);
}
- Root layout deve conter
<html>e<body>pois é o layout de nível mais alto, envolvendo toda a aplicação. export const metadataé a forma de declarar<title>,<meta>de forma declarativa, Next.js injeta automaticamente no<head>— sem necessidade de manipulação manual do DOM.
Observação: Se você esquecer de criar
app/layout.tsx, Next.js reportará um erro de build imediatamente, pois toda rota precisa de pelo menos um root layout.
5. Route Groups e Segmentos Paralelos
Às vezes você quer agrupar rotas para organizar o código, mas não quer que esse agrupamento apareça na URL. Use route group com parênteses (name).
app/
├── (marketing)/
│ ├── layout.tsx # layout específico para páginas de marketing
│ ├── page.tsx # rota "/"
│ └── pricing/
│ └── page.tsx # rota "/pricing"
└── (dashboard)/
├── layout.tsx # layout específico para dashboard
└── settings/
└── page.tsx # rota "/settings"
(marketing)e(dashboard)não aparecem na URL, eles apenas ajudam a aplicar layouts diferentes para cada grupo de páginas./pricinge/settingsusam layouts diferentes, apesar de estarem ambos sobapp/.
6. Route Handlers — construindo APIs no App Router
O arquivo route.ts substitui pages/api/*.ts do Pages Router, exportando funções nomeadas de acordo com o método HTTP.
// app/api/orders/route.ts
import { NextRequest, NextResponse } from "next/server";
import { OrderService } from "@/services/order-service";
export async function GET() {
const orders = await OrderService.findAll();
return NextResponse.json(orders);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const order = await OrderService.create(body);
return NextResponse.json(order, { status: 201 });
}
GETePOSTsão dois exports separados, Next.js roteia automaticamente a requisição de acordo com o método HTTP para a função correta.NextResponse.json(...)defineContent-Type: application/jsonautomaticamente, você não precisa fazerJSON.stringifymanualmente.- Este endpoint é servido em
/api/orders, completamente independente da UI — sem renderização React.
6.1. Route handler dinâmico
// app/api/orders/[id]/route.ts
import { NextResponse } from "next/server";
import { OrderService } from "@/services/order-service";
export async function GET(_: Request, { params }: { params: { id: string } }) {
const order = await OrderService.findById(params.id);
if (!order) {
return NextResponse.json({ message: "Order not found" }, { status: 404 });
}
return NextResponse.json(order);
}
- Como em páginas dinâmicas, a pasta
[id]permite recuperar o valor da URL através deparams.id. - Retorna
status: 404explicitamente em vez de deixar o cliente adivinhar o erro.
7. Arquivos especiais: loading, error, not-found
App Router usa React Suspense e Error Boundary implicitamente através de arquivos de convenção, você não precisa escrever um boundary manualmente.
// app/products/loading.tsx
export default function ProductsLoading() {
return <p>Carregando lista de produtos...</p>;
}
// app/products/error.tsx
"use client";
export default function ProductsError({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<p>Erro: {error.message}</p>
<button onClick={reset}>Tentar novamente</button>
</div>
);
}
loading.tsxé exibido automaticamente enquantopage.tsxdo mesmo segmento está emawaitdos dados — sem necessidade deuseState(isLoading).error.tsxdeve ser um Client Component ("use client") porque usareset— uma função interativa do lado cliente para re-renderizar o segmento com erro.- Next.js envolve automaticamente o segmento com
<Suspense>e error boundary correspondente com base nesses arquivos.
8. Metadata e layouts aninhados
Cada segmento pode exportar metadata próprio, Next.js faz o merge seguindo a ordem do layout raiz até a página.
// app/products/[id]/page.tsx
import type { Metadata } from "next";
import { ProductService } from "@/services/product-service";
type Props = { params: { id: string } };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const product = await ProductService.findById(params.id);
return { title: product ? `${product.name} - Shop Nón Lá` : "Não encontrado" };
}
export default async function ProductDetailPage({ params }: Props) {
const product = await ProductService.findById(params.id);
return <h1>{product?.name}</h1>;
}
generateMetadataé uma função dinâmica que permite definir<title>com base nos dados reais do produto — útil para SEO de páginas de detalhe.- Metadata da página filha sobrescreve campos com o mesmo nome do layout pai, os outros campos permanecem intactos.
9. Testando page components
Com Server Components assíncronos, a forma mais simples de testar é chamar a função do componente diretamente e verificar o JSX retornado, ou usar React Testing Library após renderizar.
<!-- package.json (seção devDependencies, abreviada) -->
{
"devDependencies": {
"vitest": "^2.0.0",
"@testing-library/react": "^16.0.0"
}
}
// services/product-service.test.ts
import { describe, it, expect, vi } from "vitest";
import { ProductService } from "@/services/product-service";
import ProductsPage from "@/app/products/page";
import { render, screen } from "@testing-library/react";
vi.mock("@/services/product-service", () => ({
ProductService: {
findAll: vi.fn(),
},
}));
describe("ProductsPage", () => {
it("exibe a lista de produtos retornada do serviço", async () => {
(ProductService.findAll as any).mockResolvedValue([
{ id: "1", name: "Nón lá A", price: 50000 },
]);
const jsx = await ProductsPage();
render(jsx);
expect(screen.getByText("Nón lá A - 50000đ")).toBeInTheDocument();
});
});
ProductsPage()é chamado como uma função async comum porque é essencialmente uma função que retorna JSX — sem necessidade de framework de teste especial para Server Component.vi.mocksubstituiProductService.findAllpor um stub que retorna dados fictícios, ajudando o teste a não depender do banco de dados real.render(jsx)do Testing Library verifica se o JSX renderizado no DOM tem o conteúdo esperado.
10. Armadilhas comuns e quando não usar
- Esquecer
"use client"ao usar hooks comouseState/useEffect: Server Components não suportam esses hooks, Next.js reportará um erro de build claro. - Colocar lógica sensível de busca de dados (chave de API) em um Client Component: porque o código será incluído no pacote enviado ao navegador. Sempre priorize buscar dados em Server Components ou Route Handlers.
- Abusar de route groups apenas para “embelezar”: se não há uma real necessidade de layouts diferentes, route groups apenas deixam a estrutura de diretórios mais confusa.
- Misturar
pages/eapp/para a mesma rota: os dois roteadores não podem processar uma URL simultaneamente, Next.js priorizaráapp/e as rotas empages/serão ignoradas — fácil de causar confusão durante migrações graduais.
Observação: Se seu projeto está funcionando bem com Pages Router e você não precisa de Server Components, Streaming ou Server Actions, migrar imediatamente para App Router nem sempre é necessário. Avalie os benefícios antes de mudar todo o roteamento.
11. Conclusão
App Router organiza aplicações Next.js através de três tipos principais de arquivos: page.tsx define rotas, layout.tsx compartilha UI entre múltiplas rotas, e route.ts constrói APIs que não renderizam HTML. Combinados com arquivos de convenção como loading.tsx, error.tsx, not-found.tsx, você pode construir experiências de carregamento/erro suave sem precisar escrever boundaries manualmente. Ao projetar a estrutura de diretórios, priorize agrupar por layouts que realmente precisam ser compartilhados, evite route groups desnecessários e sempre verifique os limites entre Server e Client Components para não vazar lógica sensível para o navegador.