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.tsx envolve todos os page.tsx no mesmo nível ou mais profundos.
  • page.tsx é o ponto final que realmente renderiza o conteúdo para uma URL.
  • route.ts não renderiza UI, retorna uma Response como 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 ProductsPage permite usar await nos dados diretamente no componente, pois é um Server Component, sem necessidade de useEffect ou getServerSideProps.
  • 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 em params.id.
  • notFound() renderiza o arquivo not-found.tsx mais 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>
  );
}
  • children aqui é o conteúdo de app/products/page.tsx ou app/products/[id]/page.tsx.
  • Ao navegar de /products para /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.
  • /pricing e /settings usam layouts diferentes, apesar de estarem ambos sob app/.

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 });
}
  • GET e POST sã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(...) define Content-Type: application/json automaticamente, você não precisa fazer JSON.stringify manualmente.
  • 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 de params.id.
  • Retorna status: 404 explicitamente 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 enquanto page.tsx do mesmo segmento está em await dos dados — sem necessidade de useState(isLoading).
  • error.tsx deve ser um Client Component ("use client") porque usa reset — 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.mock substitui ProductService.findAll por 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 como useState/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/ e app/ para a mesma rota: os dois roteadores não podem processar uma URL simultaneamente, Next.js priorizará app/ e as rotas em pages/ 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.

Next.jsApp RouterReactServer ComponentsRouting