Next.js: Roteamento por Sistema de Arquivos do App Router
Última atualização: 2026-08-26
O roteamento por sistema de arquivos é como a forma como os livros são categorizados em uma biblioteca—os nomes dos arquivos determinam o caminho da URL, e a estrutura de diretórios serve como sua tabela de roteamento, sem necessidade de qualquer configuração manual.
1. O Que Você Vai Aprender
- O princípio fundamental do roteamento por sistema de arquivos: O nome do arquivo é a URL
- As Seis Convenções de Arquivo: page, layout, loading, error, not-found, route
- Roteamento Dinâmico
[id]Modo de Parâmetro Único - Rotas Catch-all
[...slug]e Catch-all Opcional[[...catchAll]] - Definição e Uso de Rotas com a API Route Handlers
2. Uma História Real de um Gerente Técnico
(1) Ponto de Dor: A configuração manual de roteamento é caótica
Bob é o líder técnico em uma empresa de e-commerce, e sua equipe mantém um React SPA de 50 páginas. Toda vez que uma nova página é adicionada, os desenvolvedores precisam configurar manualmente três arquivos:
"Nossa tabela de configuração de roteamento
routes.jstem 300 linhas. Na semana passada, Xiao Ming adicionou uma nova página de detalhes de produto, mas esqueceu de registrarpath: '/products/:id'na tabela de roteamento, resultando em um erro 404 que passou despercebido por duas horas inteiras após a página entrar no ar."
A equipe de Bob encontrou os seguintes problemas de gerenciamento de roteamento:
| Problema | Impacto | Frequência |
|---|---|---|
| Configuração de roteamento ausente | 404 após o deploy | 1–2 vezes por mês |
| Tratamento Manual de Rotas Aninhadas | Código complexo, hierarquia de rotas confusa | Cada nova página |
| Páginas de Erro 404/500 Dispersas | Inconsistente, experiência do usuário ruim | Múltiplas páginas |
| Manutenção Separada de Rotas de API | Um conjunto de rotas para o front-end e outro para o back-end | Cada API |
(2) Soluções para Roteamento por Sistema de Arquivos do Next.js
O nome do arquivo é a URL, e a estrutura de diretórios é a tabela de roteamento—não há mais necessidade de configuração de roteamento
react-router-dom.
src/app/
├── page.tsx # → /
├── about/
│ └── page.tsx # → /about
├── products/
│ ├── page.tsx # → /products
│ └── [id]/
│ ├── page.tsx # → /products/1, /products/2
│ └── reviews/
│ └── page.tsx # → /products/1/reviews
└── api/
└── products/
└── route.ts # → /api/products (GET/POST)
(3) Resultados
| Dimensão | Antes (React Router) | Depois (Roteamento por Sistema de Arquivos) |
|---|---|---|
| Passos para Adicionar uma Página | 3 passos (criar componente + configurar roteamento + importar) | 1 passo (criar um arquivo) |
| Manutenção da Configuração de Roteamento | Tabela de roteamento de 300 linhas | Zero linhas, determinado automaticamente pelo diretório |
| Erros 404 ao Lançar | 1–2 vezes por mês | 0 vezes |
| Gerenciamento Unificado de Erros 404/500 | Importação manual | Convenções de arquivo entram em vigor automaticamente |
3. Princípios do Roteamento por Sistema de Arquivos
(1) O nome do arquivo é a URL
graph TB
subgraph "src/app/ Estrutura de Diretórios"
A[page.tsx] --> B[/]
C[about/page.tsx] --> D[/about]
E[products/page.tsx] --> F[/products]
G[products/promo/page.tsx] --> H[/products/promo]
I[products/id/page.tsx] --> J[/products/:id]
K[dashboard/settings/page.tsx] --> L[/dashboard/settings]
end
style A fill:#d4edda
style C fill:#d4edda
style E fill:#d4edda
| Nome do Arquivo | URL Correspondente | Descrição |
|---|---|---|
app/page.tsx |
/ |
Início |
app/about/page.tsx |
/about |
Página Estática |
app/blog/page.tsx |
/blog |
Lista de Blog |
app/blog/[id]/page.tsx |
/blog/1 |
Roteamento Dinâmico |
app/dashboard/settings/page.tsx |
/dashboard/settings |
Aninhado Multi-Nível |
(2) Disposições nos Seis Principais Documentos
| Nome do Arquivo | Finalidade | Obrigatório? | Comportamento de Renderização |
|---|---|---|---|
page.tsx |
Componente de Página (Conteúdo UI) | ✅ | Server Component (padrão) |
layout.tsx |
Contêiner de Layout (Estado Persistente) | ✅ Layout raiz | Não remontar durante a navegação |
loading.tsx |
Tela de esqueleto de carregamento | Opcional | Suspense fallback |
error.tsx |
UI de Limite de Erro | Opcional | Capturar Erros de Componentes Filhos |
not-found.tsx |
Página 404 | Opcional | notFound() Gatilho |
route.ts |
Endpoint de API | Opcional | Execução no Lado do Servidor |
▶ Exemplo: Criando Sua Primeira Página
Saída:
Diagrama: page.tsx; /; about/page.tsx; /about; products/page.tsx; /products.
// ============================================
// Criar Página /about
// Documento:src/app/about/page.tsx
// Basta criar um arquivo, Registro Automático de Rota
// ============================================
export default function AboutPage() {
return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold">Sobre o TaskFlow</h1>
<p className="mt-4 text-gray-600 leading-relaxed">
TaskFlow é uma plataforma colaborativa de gerenciamento de projetos que ajuda
equipes a planejar, acompanhar e entregar projetos com eficiência. Construído com
Next.js 16 e React 19, oferece atualizações em tempo real, colaboração
perfeita e segurança de nível empresarial.
</p>
<div className="mt-8 grid grid-cols-3 gap-4">
<div className="p-4 bg-blue-50 rounded-lg text-center">
<div className="text-2xl font-bold text-blue-600">10K+</div>
<div className="text-sm text-gray-500">Usuários Ativos</div>
</div>
<div className="p-4 bg-green-50 rounded-lg text-center">
<div className="text-2xl font-bold text-green-600">50K+</div>
<div className="text-sm text-gray-500">Projetos</div>
</div>
<div className="p-4 bg-purple-50 rounded-lg text-center">
<div className="text-2xl font-bold text-purple-600">99.9%</div>
<div className="text-sm text-gray-500">Tempo de Atividade</div>
</div>
</div>
</div>
);
}
Saída:
Conteúdo: Sobre o TaskFlow | TaskFlow é uma plataforma colaborativa | 10K+ | Usuários Ativos
Saída:
Visite http://localhost:3000/about Veja:
Sobre o TaskFlow
TaskFlow é uma plataforma colaborativa de gerenciamento de projetos...
10K+ Usuários Ativos | 50K+ Projetos | 99.9% Tempo de Atividade
4. Roteamento Dinâmico
(1) Roteamento Dinâmico de Parâmetro Único [id]
graph LR
A[app/products] --> B[page.tsx → /products]
A --> C[[id]]
C --> D[page.tsx → /products/1]
C --> E[/products/2]
style B fill:#d4edda
style D fill:#cce5ff
| Modo de Arquivo | Exemplo de URL | Valor params |
|---|---|---|
app/blog/[slug]/page.tsx |
/blog/hello-world |
{ slug: 'hello-world' } |
app/products/[id]/page.tsx |
/products/42 |
{ id: '42' } |
app/users/[userId]/settings/page.tsx |
/users/123/settings |
{ userId: '123' } |
▶ Exemplo: Página Dinâmica de Detalhes do Produto
Saída:
Diagrama: app/products; page.tsx → /products; [id; page.tsx → /products/1; /products/2.
// ============================================
// Página de Detalhes do Produto — Roteamento Dinâmico [id]
// Documento:src/app/products/[id]/page.tsx
// ============================================
// No Next.js 16, params é uma Promise (assíncrono)
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await fetch(`https://fakestoreapi.com/products/${id}`);
return (
<div className="container mx-auto p-8">
<div className="flex gap-8">
<img
src={product.image}
alt={product.title}
className="w-80 h-80 object-contain"
/>
<div>
<h1 className="text-2xl font-bold">{product.title}</h1>
<p className="text-xl text-green-600 font-bold mt-2">
${product.price}
</p>
<p className="text-sm text-gray-500 mt-1">
Categoria: {product.category}
</p>
<p className="mt-4 text-gray-700">{product.description}</p>
</div>
</div>
</div>
);
}
Saída:
O componente do servidor busca e renderiza dados.
Saída:
Visite /products/1 Veja:
Fjallraven - Foldsack No. 1 Backpack
$109.95
Categoria: men's clothing
Sua mochila perfeita para viagens de um dia e caminhadas...
(2) Rotas Catch-all [...slug]
graph TB
A[app/docs] --> B[[...slug]]
B --> C[page.tsx]
C --> D[/docs/getting-started]
C --> E[/docs/guides/installation]
C --> F[/docs/api/authentication/overview]
style C fill:#cce5ff
| Modo de Arquivo | URL | params.slug |
|---|---|---|
app/docs/[...slug]/page.tsx |
/docs |
404 (Nível 1 é obrigatório) |
app/docs/[...slug]/page.tsx |
/docs/getting-started |
['getting-started'] |
app/docs/[...slug]/page.tsx |
/docs/guides/installation |
['guides', 'installation'] |
app/docs/[...slug]/page.tsx |
/docs/a/b/c |
['a', 'b', 'c'] |
▶ Exemplo: Página de Documentação Catch-all
Saída:
Diagrama: app/docs; [...slug; page.tsx; /docs/getting-started; /docs/guides/installation; /docs/api/authentication/overview.
// ============================================
// Catch-all — Páginas de Documentos Multi-Nível
// Documento:src/app/docs/[...slug]/page.tsx
// ============================================
export default async function DocsPage({
params,
}: {
params: Promise<{ slug: string[] }>;
}) {
const { slug } = await params;
return (
<div className="container mx-auto p-8">
<nav className="text-sm text-gray-500 mb-4">
Início / Docs / {slug.join(" / ")}
</nav>
<h1 className="text-3xl font-bold">Docs: {slug.join(" > ")}</h1>
<div className="mt-8 p-6 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="font-medium">Você está visualizando documentação para:</p>
<ul className="mt-2 list-disc list-inside">
{slug.map((segment, index) => (
<li key={index}>
Nível {index + 1}: <code className="bg-gray-100 px-2 py-0.5 rounded">{segment}</code>
</li>
))}
</ul>
</div>
<p className="mt-4 text-gray-600">
Profundidade total: {slug.length} níveis
</p>
</div>
);
}
Saída:
DocsPage renderiza sua UI.
Saída:
Visite /docs/getting-started/installation Veja:
Início / Docs / getting-started / installation
Docs: getting-started > installation
Você está visualizando documentação para:
- Nível 1: getting-started
- Nível 2: installation
Profundidade total: 2 níveis
(3) Catch-all Opcional [[...catchAll]]
graph TB
A[app/categories] --> B[[[...catchAll]]]
B --> C[page.tsx]
C --> D[/categories]
C --> E[/categories/electronics]
C --> F[/categories/electronics/phones]
style C fill:#d4edda
style D fill:#d4edda
| Modo de Arquivo | URL | params.catchAll |
|---|---|---|
app/categories/[[...catchAll]]/page.tsx |
/categories |
undefined |
app/categories/[[...catchAll]]/page.tsx |
/categories/electronics |
['electronics'] |
app/categories/[[...catchAll]]/page.tsx |
/categories/electronics/phones |
['electronics', 'phones'] |
▶ Exemplo: Página de Categoria Catch-all Opcional
Saída:
Diagrama: app/categories; [[...catchAll; page.tsx; /categories; /categories/electronics; /categories/electronics/phones.
// ============================================
// Catch-all Opcional — Página de Navegação de Categoria
// Documento:src/app/categories/[[...categories]]/page.tsx
// Tanto /categories quanto /categories/electronics entram em vigor
// ============================================
export default async function CategoriesPage({
params,
}: {
params: Promise<{ categories?: string[] }>;
}) {
const { categories } = await params;
const allProducts = await fetch("https://fakestoreapi.com/products");
const categoriesList = [...new Set(allProducts.map(p => p.category))];
const filteredProducts = categories
? allProducts.filter(p => p.category === categories[0])
: allProducts;
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">
{categories ? `Categoria: ${categories[0]}` : "Todas as Categorias"}
</h1>
{!categories && (
<div className="flex gap-2 mb-8 flex-wrap">
{categoriesList.map(cat => (
<a
key={cat}
href={`/categories/${encodeURIComponent(cat)}`}
className="px-4 py-2 bg-gray-100 rounded-full hover:bg-blue-100"
>
{cat}
</a>
))}
</div>
)}
<div className="grid grid-cols-4 gap-6">
{filteredProducts.slice(0, 8).map(p => (
<div key={p.id} className="border rounded-lg p-4 hover:shadow-lg">
<img src={p.image} alt={p.title} className="h-40 mx-auto" />
<p className="mt-2 font-medium text-sm truncate">{p.title}</p>
<p className="text-green-600 font-bold">${p.price}</p>
</div>
))}
</div>
</div>
);
}
Saída:
A busca de dados do lado do servidor renderiza a lista de itens.
Saída:
Visite /categories Veja:
Botões de Todas as Categorias(electronics, jewelery, men's clothing, women's clothing)
Visite /categories/electronics Veja:
Categoria: electronics
Mostrar apenas produtos eletrônicos(8 Cards de Produto)
5. loading.tsx e error.tsx
(1) loading.tsx — Carregar a tela de esqueleto
graph LR
A[Navegação do Usuário] --> B[loading.tsx<br/>Exibição de Wireframe]
B --> C[page.tsx<br/>Dados Prontos]
C --> D[Página Completa]
style B fill:#f8d7da
style C fill:#d4edda
▶ Exemplo: Carregando uma tela de esqueleto
Saída:
Diagrama: Navegação do Usuário; loading.tsx Exibição de Wireframe; page.tsx Dados Prontos; Página Completa.
// ============================================
// Carregando a tela de esqueleto — Exibida quando os dados da página estão sendo recuperados
// Documento:src/app/products/loading.tsx
// ============================================
export default function ProductsLoading() {
return (
<div className="container mx-auto p-8">
<div className="h-8 w-48 bg-gray-200 rounded animate-pulse mb-6" />
<div className="grid grid-cols-4 gap-6">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="border rounded-lg p-4">
<div className="h-40 bg-gray-200 rounded animate-pulse" />
<div className="h-4 bg-gray-200 rounded mt-2 animate-pulse" />
<div className="h-4 w-16 bg-gray-200 rounded mt-2 animate-pulse" />
</div>
))}
</div>
</div>
);
}
Saída:
Renderiza uma lista de itens usando .map().
Saída:
Ao visitar /products, exibe até que o carregamento de dados esteja completo:
8 Cartões de placeholder cinza(Animação com Pulsos)
Após o carregamento dos dados ser concluído, O conteúdo de placeholder é automaticamente substituído pelo conteúdo real
Sem cintilação, Sem tela branca, Transição Suave
(2) error.tsx — Limite de Erro
▶ Exemplo: Página de Erro
Saída:
A página renderiza como descrito acima, com a UI atualizando baseada no comportamento descrito.
// ============================================
// Limite de Erro — Capturando Erros de Componentes Filhos
// Documento:src/app/products/error.tsx
// ============================================
'use client';
export default function ProductsError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="container mx-auto p-8 text-center">
<div className="max-w-md mx-auto">
<h2 className="text-2xl font-bold text-red-600 mb-4">
Algo deu errado!
</h2>
<p className="text-gray-600 mb-6">
{error.message || "Falha ao carregar produtos. Por favor, tente novamente."}
</p>
<button
onClick={reset}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Tentar Novamente
</button>
</div>
</div>
);
}
Saída:
Conteúdo: Algo deu errado! | Tentar Novamente
Saída:
Quando a recuperação de dados da página do produto falha, Mostra:
Algo deu errado!
Falha ao carregar produtos. Por favor, tente novamente.
[Try Again] botão → Clique para tentar novamente automaticamente
(3) not-found.tsx — Página 404
---
## 6. Route Handlers (Rotas de API)
### (1) Uso Básico de route.ts
```mermaid
graph LR
A[Requisição do Cliente] --> B[route.ts]
B --> C[GET /api/products]
B --> D[POST /api/products]
B --> E[PUT /api/products/:id]
B --> F[DELETE /api/products/:id]
style B fill:#cce5ff
| Método HTTP | Função Exportada | Finalidade |
|---|---|---|
GET |
export async function GET() |
Ler Dados |
POST |
export async function POST() |
Criar Dados |
PUT |
export async function PUT() |
Atualizar Dados |
DELETE |
export async function DELETE() |
Excluir Dados |
PATCH |
export async function PATCH() |
Atualização Parcial |
---
## 7. Exemplo Completo: Sistema de Navegação de Produtos E-commerce
```tsx
// ============================================
// Exemplo Abrangente: Sistema de Navegação de Produtos E-commerce
// Páginas Cobertas, Layout, Loading, Error, Roteamento de API
// ============================================
// src/app/layout.tsx — Layout Raiz
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="pt-BR">
<body className="bg-gray-50">
<nav className="bg-white shadow-sm p-4">
<a href="/" className="text-xl font-bold text-blue-600">
ShopHub
</a>
</nav>
<main>{children}</main>
</body>
</html>
);
}
// src/app/page.tsx — Início
export default function HomePage() {
return (
<div className="container mx-auto p-8 text-center">
<h1 className="text-4xl font-bold">Bem-vindo ao ShopHub</h1>
<p className="mt-4 text-gray-600">
Navegue pela nossa coleção de produtos incríveis.
</p>
<a
href="/products"
className="inline-block mt-6 px-8 py-3 bg-blue-600 text-white rounded-lg"
>
Navegar Produtos
</a>
</div>
);
}
// src/app/products/page.tsx — Lista de Produtos
export default async function ProductsPage() {
const products = await fetch(
"https://fakestoreapi.com/products"
);
return (
<div className="container mx-auto p-8">
<h1 className="text-3xl font-bold mb-6">Todos os Produtos</h1>
<div className="grid grid-cols-4 gap-6">
{products.map(p => (
<a
key={p.id}
href={`/products/${p.id}`}
className="border rounded-lg p-4 bg-white hover:shadow-lg"
>
<img
src={p.image}
alt={p.title}
className="h-40 mx-auto"
/>
<p className="mt-2 font-medium text-sm">{p.title}</p>
<p className="text-green-600 font-bold mt-1">${p.price}</p>
</a>
))}
</div>
</div>
);
}
// src/app/products/[id]/page.tsx — Detalhes do Produto
export default async function ProductDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await fetch(
`https://fakestoreapi.com/products/${id}`
);
if (!product) {
notFound();
}
return (
<div className="container mx-auto p-8">
<a
href="/products"
className="text-blue-600 hover:underline mb-4 inline-block"
>
← Voltar para produtos
</a>
<div className="flex gap-8 bg-white p-8 rounded-lg shadow">
<img
src={product.image}
alt={product.title}
className="w-96 h-96 object-contain"
/>
<div>
<h1 className="text-3xl font-bold">{product.title}</h1>
<p className="text-2xl text-green-600 font-bold mt-4">
${product.price}
</p>
<p className="mt-6 text-gray-700 leading-relaxed">
{product.description}
</p>
<button className="mt-6 px-8 py-3 bg-blue-600 text-white rounded-lg">
Adicionar ao Carrinho
</button>
</div>
</div>
</div>
);
}
// src/app/products/loading.tsx — Carregando a tela de esqueleto
export default function ProductsLoading() {
return (
<div className="container mx-auto p-8">
<div className="h-8 w-48 bg-gray-200 rounded animate-pulse mb-6" />
<div className="grid grid-cols-4 gap-6">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="border rounded-lg p-4 bg-white">
<div className="h-40 bg-gray-200 rounded animate-pulse" />
<div className="h-4 bg-gray-200 rounded mt-2 animate-pulse" />
<div className="h-4 w-16 bg-gray-200 rounded mt-2 animate-pulse" />
</div>
))}
</div>
</div>
);
}
// src/app/products/error.tsx — Limite de Erro
'use client';
export default function ProductsError({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div className="container mx-auto p-8 text-center">
<h2 className="text-2xl font-bold text-red-600">
Falha ao carregar produtos
</h2>
<button
onClick={reset}
className="mt-4 px-6 py-2 bg-blue-600 text-white rounded-lg"
>
Tentar Novamente
</button>
</div>
);
}
Saída Esperada:
Início(/):
Bem-vindo ao ShopHub → [Navegar Produtos]
Lista de Produtos(/products):
Carregando... → 8 Telas de esqueleto cinzas
Carregamento completo → Grade de Cards de Produto(Inclui imagens, Título, Preço)
Detalhes do Produto(/products/1):
Imagens do Produto (esquerda) + Título/Preço/Descrição/Adicionar ao Carrinho (direita)
← Voltar para produtos Link Voltar para Lista
Status de Erro:
Falha ao carregar produtos
[Tentar Novamente] botão
Status 404:
Automaticamente do raiz not-found.tsx Processamento
❓ Perguntas Frequentes
P: Posso usar .jsx em vez de .tsx para page.tsx? R: Sim. Se TypeScript não estiver habilitado no seu projeto, você pode usar a extensão .jsx. No entanto, este tutorial recomenda TypeScript, então todos os exemplos de código usam .tsx.
P: Por que
paramssão Promises no Next.js 16? R: A partir do Next.js 15,params,searchParamse variáveis semelhantes foram alteradas para Promises para suportar geração assíncrona de páginas. Se você usá-los diretamenteparams.id, encontrará um erro TypeScript; você deveawait paramspara recuperar seus valores.
P: Qual é a relação entre
loading.tsxe Suspense? R:loading.tsxé um fallback Suspense no nível da página. Next.js automaticamente envolveloading.tsxem<Suspense>. Se você precisar de um estado de carregamento no nível do componente, deve usar manualmente<Suspense fallback={...}>.
P: Por que
error.tsxprecisa deuse client? R:error.tsxdeve ser um Client Component porque precisa lidar com interações (como o botão de reset). Server Components não podem conter manipuladores de eventos ou hooks, então a diretiva'use client'é necessária.
P:
route.tsepage.tsxpodem coexistir? R: Não. Sob o mesmo segmento de rota,page.tsxeroute.tssão mutuamente exclusivos. Um diretório pode conter apenas um tipo de manipulador de rota—seja uma página (page.tsx) ou uma API (route.ts).
P: Qual é exatamente a diferença entre [...slug] e [[...catchAll]]? R: [...slug] requer pelo menos um segmento de caminho (/docs/a corresponde; /docs não corresponde e retorna 404). [[...catchAll]] permite zero segmentos de caminho (tanto /categories quanto /categories/a correspondem), tornando-o adequado para cenários de "navegação de categoria opcional".
📖 Resumo
- Roteamento por sistema de arquivos: O nome do arquivo serve como caminho da URL, eliminando a necessidade de configuração manual de tabelas de roteamento
page.tsxdefine o conteúdo da página;layout.tsxdefine o contêiner de layout- Roteamento dinâmico:
[id]corresponde a um único parâmetro,[...slug]corresponde a um caminho multi-nível [[...catchAll]]Permite zero argumentos (opcional);[...slug]requer pelo menos um argumentoloading.tsxEnvolve automaticamente comSuspensepara exibir uma tela de esqueleto de carregamentoerror.tsxé um componente cliente que fornece um limite de erro e um botão de tentar novamentenot-found.tsxTrata páginas 404; a funçãonotFound()é acionada manualmenteroute.tsDefine endpoints de API que suportam GET/POST/PUT/DELETE
📝 Exercícios
-
Exercício Básico (⭐): Crie uma página chamada
app/blog/[slug]/page.tsxno projeto para que quando/blog/hello-worldfor acessado, o console exibaslug: hello-world. -
Exercício Avançado (⭐⭐): Crie
app/api/todos/route.tspara implementar uma API TODO simples (GET retorna uma lista, POST cria um TODO), e verifique acessando-o em um navegador. -
Desafio (⭐⭐⭐): Crie duas implementações,
app/docs/[...slug]/page.tsxeapp/docs/[[...slug]]/page.tsx, para testar o comportamento de acesso de/docs, e use uma tabela para comparar as diferenças entre as duas.