Rotas dinâmicas e parâmetros no Next.js: do básico à segurança de tipos

Na semana passada, ao refatorar um projeto em Next.js, esbarrei em um problema de enlouquecer: a rota dinâmica estava escrita conforme a documentação, mas clicar nela levava a uma página 404. O console não mostrava erro algum, então eu não fazia ideia do que estava acontecendo. Depois descobri que o App Router do Next.js 14 havia mudado a forma de obter os parâmetros da rota, enquanto eu ainda usava o padrão antigo do Pages Router.
Não foi a primeira vez que tropecei nas rotas do Next.js. Do getStaticPaths do Pages Router ao generateStaticParams do App Router, cada atualização parece exigir que você aprenda tudo outra vez. Quando usar uma rota dinâmica? Quando recorrer a uma rota catch-all? E como funcionam os parâmetros opcionais? Com esses conceitos misturados, é fácil ficar confuso.
Se você também tem dúvidas sobre rotas dinâmicas no Next.js ou está migrando do Pages Router para o App Router, este artigo é para você. Vamos começar pelos fundamentos e chegar às práticas de segurança de tipos, com vários exemplos reais de código para organizar as ideias.
Ao final, você terá uma visão completa das rotas dinâmicas: saberá qual tipo usar em cada cenário, como obter os parâmetros corretamente e como adicionar sugestões de tipo aos parâmetros com TypeScript. Nada de teoria vazia: vamos trabalhar com código e soluções concretas.
Capítulo 1: fundamentos das rotas dinâmicas
O que é uma rota dinâmica?
Comecemos com um cenário comum: você tem um blog e a URL de cada artigo é /blog/ID-do-artigo. Com rotas estáticas, seria necessário criar um arquivo de página para cada artigo, o que evidentemente não é viável. É aí que entram as rotas dinâmicas: um único arquivo de página trata os detalhes de todos os artigos.
No App Router do Next.js, uma rota dinâmica é implementada por uma pasta cujo nome fica entre colchetes. O conceito pode parecer abstrato, então veja o exemplo:
app/
├── blog/
│ └── [slug]/
│ └── page.tsx ← esta é a rota dinâmica
Essa estrutura corresponde a qualquer caminho /blog/*, por exemplo:
/blog/hello-world→slug = "hello-world"/blog/nextjs-guide→slug = "nextjs-guide"/blog/123→slug = "123"
A implementação mais simples de uma rota dinâmica
Crie app/blog/[slug]/page.tsx e adicione este código:
// app/blog/[slug]/page.tsx
export default function BlogPost({
params
}: {
params: { slug: string }
}) {
return (
<div>
<h1>Detalhes do artigo</h1>
<p>Slug atual: {params.slug}</p>
</div>
)
}
É só isso. Quando alguém acessa /blog/hello-world, o valor de params.slug é "hello-world".
Erros comuns de quem está começando:
- ❌ Usar
[slug].tsxcomo nome do arquivo (o App Router exige uma pasta) - ❌ Acessar
props.slugdiretamente (o valor vem pelo objetoparams) - ❌ Esquecer os colchetes no nome da pasta (sem eles, a rota é estática)
Comparação entre Pages Router e App Router
Se você já usou o Pages Router, talvez estranhe: “Antes não era em pages/blog/[slug].tsx?” Sim. O App Router mudou vários pontos:
| Recurso | Pages Router | App Router |
|---|---|---|
| Local do arquivo | pages/blog/[slug].tsx | app/blog/[slug]/page.tsx |
| Obtenção do parâmetro | router.query.slug ou getStaticProps | params.slug |
| Definição de tipo | Manual | Inferida a partir do tipo das props |
| Geração estática | getStaticPaths | generateStaticParams |
No início da migração, o que mais me incomodou foi a nova forma de obter os parâmetros. No Pages Router, era possível usar o hook useRouter; já os Server Components do App Router não podem usar hooks e recebem os valores pela prop params. Isso acontece porque Server Components são renderizados no servidor por padrão e não têm o objeto router do cliente.
Exemplo prático: página de detalhes de um produto
Suponha que você esteja criando uma loja virtual cuja página de produto usa a URL /products/ID-do-produto. A implementação completa fica assim:
// app/products/[id]/page.tsx
interface Product {
id: string
name: string
price: number
description: string
}
// Simula a busca de um produto no banco de dados
async function getProduct(id: string): Promise<Product | null> {
// Em um projeto real, aqui entraria uma consulta ao banco ou uma chamada de API
const products: Product[] = [
{ id: '1', name: 'Livro introdutório de TypeScript', price: 99, description: 'Adequado para iniciantes' },
{ id: '2', name: 'Guia prático de React', price: 129, description: 'Do zero à implantação do projeto' }
]
return products.find(p => p.id === id) || null
}
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id)
if (!product) {
return <div>Produto não encontrado</div>
}
return (
<div>
<h1>{product.name}</h1>
<p className="price">¥{product.price}</p>
<p>{product.description}</p>
</div>
)
}
Observe estes detalhes:
- O componente usa
async, pois Server Components aceitam operações assíncronas - Primeiro os dados são obtidos; depois, o resultado determina o que será renderizado
- O caso em que o produto não existe foi tratado, cobrindo o cenário de 404
Para retornar uma página 404 de verdade, use a função notFound do Next.js:
import { notFound } from 'next/navigation'
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id)
if (!product) {
notFound() // Retorna a página 404
}
return (
<div>
<h1>{product.name}</h1>
{/* ... */}
</div>
)
}
Assim, quando alguém acessa um produto inexistente, vê a página personalizada not-found.tsx, o que melhora a experiência.
Até aqui, você já domina as rotas dinâmicas básicas. Mas isso é apenas o começo. A seguir, veremos um cenário mais complexo: como lidar com caminhos de vários níveis.
Capítulo 2: rotas catch-all e parâmetros opcionais
Quando usar uma rota catch-all?
Imagine um site de documentação com esta estrutura de URLs:
/docs/getting-started/docs/api/authentication/docs/api/database/queries/docs/guides/deployment/vercel
O número de níveis não é fixo: às vezes há dois, às vezes três ou mais. Uma rota dinâmica comum não resolve esse caso. Você precisa de uma rota catch-all.
Rota catch-all: [...slug]
Uma pasta chamada [...slug] (com três pontos) corresponde a qualquer quantidade de segmentos:
app/
├── docs/
│ └── [...slug]/
│ └── page.tsx ← corresponde a todos os caminhos sob /docs/*
Ela corresponde a:
/docs/getting-started→slug = ["getting-started"]/docs/api/authentication→slug = ["api", "authentication"]/docs/guides/deployment/vercel→slug = ["guides", "deployment", "vercel"]
Atenção: o parâmetro slug é um array, não uma string.
Implementação: sistema de documentação
// app/docs/[...slug]/page.tsx
interface Doc {
title: string
content: string
}
// Obtém o documento a partir do array do caminho
async function getDoc(slugArray: string[]): Promise<Doc | null> {
// Junta o array em um caminho, por exemplo: ["api", "auth"] → "api/auth"
const path = slugArray.join('/')
// Em um projeto real, os dados viriam do sistema de arquivos ou do banco de dados
const docs: Record<string, Doc> = {
'getting-started': {
title: 'Primeiros passos',
content: 'Boas-vindas ao nosso produto...'
},
'api/authentication': {
title: 'Autenticação da API',
content: 'Usamos JWT para autenticação...'
},
'api/database/queries': {
title: 'Consultas ao banco de dados',
content: 'Use o Prisma para consultar o banco...'
}
}
return docs[path] || null
}
export default async function DocsPage({
params
}: {
params: { slug: string[] }
}) {
const doc = await getDoc(params.slug)
if (!doc) {
return <div>Documento não encontrado</div>
}
return (
<article>
<h1>{doc.title}</h1>
<div dangerouslySetInnerHTML={{ __html: doc.content }} />
{/* Navegação por breadcrumbs */}
<nav>
<a href="/docs">Documentação</a>
{params.slug.map((segment, i) => {
const href = `/docs/${params.slug.slice(0, i + 1).join('/')}`
return (
<span key={i}>
{' / '}
<a href={href}>{segment}</a>
</span>
)
})}
</nav>
</article>
)
}
Pontos fortes deste código:
slugArray.join('/')transforma o array do caminho em uma string- A navegação por breadcrumbs usa
slicepara obter cada prefixo do caminho - A anotação
params: { slug: string[] }permite que o TypeScript detecte erros
Rota catch-all opcional: [[...slug]]
Às vezes, você quer que a rota corresponda tanto a /docs quanto a /docs/*. Uma rota catch-all comum não corresponde a /docs, pois não há parâmetro. Nesse caso, use uma rota catch-all opcional:
app/
├── docs/
│ └── [[...slug]]/
│ └── page.tsx ← observe os colchetes duplos
Ela corresponde a:
/docs→slug = undefined/docs/getting-started→slug = ["getting-started"]/docs/api/auth→slug = ["api", "auth"]
No código, é necessário tratar a possibilidade de slug ser undefined:
// app/docs/[[...slug]]/page.tsx
export default async function DocsPage({
params
}: {
params: { slug?: string[] } // Observe que slug é opcional
}) {
// Página inicial /docs
if (!params.slug) {
return <div>Boas-vindas à central de documentação</div>
}
// Trata os subcaminhos
const doc = await getDoc(params.slug)
// ...
}
Armadilhas comuns para iniciantes
Armadilha 1: esquecer que slug é um array
// ❌ Incorreto
<h1>Caminho atual: {params.slug}</h1> // Exibe "api,authentication"
// ✅ Correto
<h1>Caminho atual: {params.slug.join('/')}</h1> // Exibe "api/authentication"
Armadilha 2: usar a estrutura de dados errada na geração estática
// ❌ Incorreto
export function generateStaticParams() {
return [
{ slug: 'api/auth' } // Isto é uma string, não um array
]
}
// ✅ Correto
export function generateStaticParams() {
return [
{ slug: ['api', 'auth'] } // Formato de array
]
}
Armadilha 3: confundir uma rota dinâmica comum com uma rota catch-all
| Tipo de rota | Nome da pasta | O que corresponde | Tipo do parâmetro |
|---|---|---|---|
| Rota dinâmica | [slug] | /blog/123 | string |
| Catch-all | [...slug] | /docs/a/b/c (sem incluir /docs) | string[] |
| Catch-all opcional | [[...slug]] | /docs e /docs/a/b/c | string[] | undefined |
Eu mesmo misturei esses três tipos e acabei com rotas que às vezes abriam e às vezes não. Depois de muito procurar, descobri que o nome da pasta estava errado.
Dica prática: tratar caracteres especiais
Se a URL tiver caracteres chineses ou especiais, lembre-se de codificá-los e decodificá-los:
export default async function Page({
params
}: {
params: { slug: string[] }
}) {
// A URL é codificada automaticamente; decodifique-a para exibi-la corretamente
const decodedSlug = params.slug.map(s => decodeURIComponent(s))
console.log(params.slug) // ["api", "autentica%C3%A7%C3%A3o"]
console.log(decodedSlug) // ["api", "autenticação"]
// ...
}
Agora você já consegue tratar estruturas de caminho complexas. Mas ainda falta resolver uma questão importante: quando essas páginas dinâmicas são geradas? Em toda requisição ou antecipadamente durante o build? Esse é o papel de generateStaticParams, assunto do próximo capítulo.
Capítulo 3: generateStaticParams em detalhes
Por que usar generateStaticParams?
Suponha que seu blog tenha 100 artigos, todos em uma rota dinâmica /blog/[slug]. Sem otimização, a cada acesso seria necessário:
- Consultar o banco de dados para buscar o conteúdo
- Renderizar o HTML no servidor
- Retornar o resultado para o usuário
Isso aumenta o tempo de resposta e a carga do servidor. O Next.js oferece uma alternativa melhor: pré-renderizar durante o build todas as páginas dos artigos e gerar HTML estático. Essa é a função de generateStaticParams.
Uso básico: geração estática de artigos do blog
// app/blog/[slug]/page.tsx
interface Post {
slug: string
title: string
content: string
}
// Obtém os slugs de todos os artigos
export async function generateStaticParams() {
// Busca todos os artigos no banco de dados ou CMS
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
// Retorna todas as combinações possíveis de parâmetros
return posts.map((post: Post) => ({
slug: post.slug
}))
}
// Renderiza os detalhes do artigo
export default async function BlogPost({
params
}: {
params: { slug: string }
}) {
// Obtém o conteúdo de acordo com o slug
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
O que este código faz?
generateStaticParamsé executado durante o build e retorna os slugs de todos os artigos- O Next.js pré-renderiza um arquivo HTML estático para cada slug
- Quando alguém acessa a página, o arquivo estático é servido diretamente, o que é muito rápido
Resultado do build:
.next/server/app/blog/
├── hello-world.html
├── nextjs-guide.html
└── typescript-tips.html
Quando usar generateStaticParams?
Essa é uma das dúvidas que mais recebo. Uma regra simples ajuda:
✅ Cenários adequados para generateStaticParams:
- Artigos de blog e notícias cujo conteúdo muda pouco
- Páginas de produto com uma quantidade limitada de itens, por exemplo, menos de 10.000
- Páginas de documentação e centrais de ajuda
- Perfis de usuário, se a base não for muito grande
❌ Cenários inadequados:
- Páginas de resultados de busca, cujas combinações de parâmetros são ilimitadas
- Dados em tempo real, como cotações de ações e placares esportivos
- Plataformas de conteúdo gerado pelo usuário com uma base enorme, em que é inviável pré-renderizar todos os perfis
- Páginas que exibem conteúdo diferente conforme o estado de login
Uso avançado 1: geração estática de uma rota catch-all
Em uma rota [...slug], o parâmetro retornado deve ser um array:
// app/docs/[...slug]/page.tsx
export async function generateStaticParams() {
// Todos os caminhos da documentação
const docPaths = [
['getting-started'],
['api', 'authentication'],
['api', 'database', 'queries'],
['guides', 'deployment', 'vercel']
]
return docPaths.map(slug => ({ slug }))
}
export default async function DocsPage({
params
}: {
params: { slug: string[] }
}) {
// ...
}
Atenção: o formato retornado é { slug: ['api', 'auth'] }, e não { slug: 'api/auth' }.
Uso avançado 2: rotas com vários parâmetros
Se a rota tiver vários parâmetros dinâmicos, como /shop/[category]/[productId]:
app/
├── shop/
│ └── [category]/
│ └── [productId]/
│ └── page.tsx
Escreva generateStaticParams assim:
// app/shop/[category]/[productId]/page.tsx
export async function generateStaticParams() {
const products = [
{ category: 'electronics', productId: 'iphone-15' },
{ category: 'electronics', productId: 'macbook-pro' },
{ category: 'books', productId: 'clean-code' },
{ category: 'books', productId: 'refactoring' }
]
return products.map(p => ({
category: p.category,
productId: p.productId
}))
}
export default async function ProductPage({
params
}: {
params: { category: string; productId: string }
}) {
return (
<div>
<h1>Categoria: {params.category}</h1>
<p>ID do produto: {params.productId}</p>
</div>
)
}
Uso avançado 3: geração sob demanda, no modo fallback
Se houver conteúdo demais — por exemplo, 100 mil artigos —, pré-renderizar tudo não é realista. Você pode gerar apenas os itens mais populares e deixar o restante para ser criado sob demanda:
// app/blog/[slug]/page.tsx
export const dynamicParams = true // Permite gerar dinamicamente páginas não pré-renderizadas
export async function generateStaticParams() {
// Pré-renderiza apenas os 100 artigos mais populares
const topPosts = await fetchTopPosts(100)
return topPosts.map(post => ({
slug: post.slug
}))
}
export default async function BlogPost({
params
}: {
params: { slug: string }
}) {
// Mesmo sem pré-renderização, o primeiro acesso gera a página e a armazena em cache
const post = await fetchPost(params.slug)
if (!post) {
notFound()
}
return <article>{/* ... */}</article>
}
Com dynamicParams = true:
- Páginas pré-renderizadas são retornadas imediatamente, com a maior velocidade
- Páginas não pré-renderizadas são geradas na primeira requisição e reutilizam o cache nos acessos seguintes
- Páginas inexistentes retornam 404
Dúvidas que costumam travar quem está começando
Pergunta 1: quando generateStaticParams é executado?
Ele é executado apenas durante o build (npm run build), não a cada requisição. Por isso, o efeito não aparece no ambiente de desenvolvimento (npm run dev); é preciso concluir um build para ver os arquivos gerados estaticamente.
Pergunta 2: o que acontece quando os dados são atualizados?
Depois da geração estática, o conteúdo fica fixo. Se os dados mudarem, será necessário reconstruir e implantar o projeto. Algumas opções são:
- Usar ISR (Incremental Static Regeneration) para atualizações periódicas
- Combinar com
dynamicParams = truepara gerar páginas sob demanda - Usar
revalidatepara definir o tempo de expiração do cache
// Gera novamente a página a cada 60 segundos
export const revalidate = 60
export default async function Page() {
// ...
}
Pergunta 3: por que o build ficou mais demorado?
Quanto mais caminhos generateStaticParams retorna, maior é o tempo de build. Se o build atingir o limite de tempo:
- Reduza a quantidade de páginas pré-renderizadas e gere apenas o conteúdo popular
- Use builds incrementais, disponíveis na Vercel e na Netlify
- Considere a geração sob demanda com
dynamicParams = true
Agora você já domina os principais usos das rotas dinâmicas no Next.js. No último capítulo, vamos resolver uma dúvida que afeta muita gente: como obter sugestões de tipo do TypeScript também para os parâmetros da rota?
Capítulo 4: segurança de tipos nos parâmetros da rota
Por que a segurança de tipos é necessária?
Você consegue identificar o problema neste código?
export default async function Page({
params
}: {
params: { slug: string }
}) {
// Suponha que seja um ID numérico, mas o tipo definido é string
const id = parseInt(params.slug)
if (isNaN(id)) {
// O tipo incorreto só é descoberto em tempo de execução
return <div>ID inválido</div>
}
// ...
}
O problema é que params.slug tem o tipo string, mas o valor necessário é um número. Essa incompatibilidade não é descoberta durante a compilação e só aparece em tempo de execução.
Restrições básicas de tipo
No objeto params do Next.js, todos os parâmetros são string ou string[] por padrão. Você pode criar tipos próprios para reforçar as restrições:
// app/blog/[slug]/page.tsx
interface BlogParams {
slug: string
}
export default async function BlogPost({
params
}: {
params: BlogParams
}) {
// O TypeScript sabe que params.slug é uma string
const post = await fetchPost(params.slug)
// ...
}
Isso pode parecer pouco útil em um exemplo tão simples, mas faz bastante diferença quando há vários parâmetros:
// app/shop/[category]/[productId]/page.tsx
interface ShopParams {
category: 'electronics' | 'books' | 'clothing' // Limita o valor a estas opções
productId: string
}
export default async function ProductPage({
params
}: {
params: ShopParams
}) {
// O TypeScript verifica se category contém um valor permitido
if (params.category === 'toys') { // ❌ Erro de compilação
// ...
}
}
Validação em tempo de execução com Zod
As definições de tipo só verificam o código durante a compilação; em tempo de execução, ainda é possível receber valores inválidos. Combine-as com o Zod para validar os parâmetros:
npm install zod
// app/products/[id]/page.tsx
import { z } from 'zod'
import { notFound } from 'next/navigation'
// Define o schema dos parâmetros
const paramsSchema = z.object({
id: z.string().regex(/^\d+$/, 'Deve ser um ID numérico')
})
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
// Valida em tempo de execução
const result = paramsSchema.safeParse(params)
if (!result.success) {
notFound() // Parâmetro inválido retorna 404 diretamente
}
const { id } = result.data
const product = await fetchProduct(parseInt(id))
// ...
}
Essa abordagem oferece três vantagens:
- Verificação de tipos durante a compilação
- Validação do formato dos parâmetros em tempo de execução
- Requisições inválidas retornam 404 sem consultar o banco de dados
Técnica avançada: generateStaticParams com segurança de tipos
Também é possível restringir o tipo do retorno de generateStaticParams:
// app/blog/[slug]/page.tsx
interface BlogParams {
slug: string
}
export async function generateStaticParams(): Promise<BlogParams[]> {
const posts = await fetchAllPosts()
return posts.map(post => ({
slug: post.slug
// Se você usar slug: post.id com um tipo incompatível, o TypeScript indicará um erro
}))
}
export default async function BlogPost({
params
}: {
params: BlogParams
}) {
// ...
}
Exemplo prático: rota de blog multilíngue
Imagine um blog multilíngue cuja URL seja /[locale]/blog/[slug], por exemplo:
/zh/blog/hello-world/en/blog/hello-world
Uma implementação completa com segurança de tipos fica assim:
// app/[locale]/blog/[slug]/page.tsx
import { z } from 'zod'
import { notFound } from 'next/navigation'
// Lista de idiomas disponíveis
const locales = ['zh', 'en', 'ja'] as const
type Locale = typeof locales[number] // "zh" | "en" | "ja"
interface PageParams {
locale: Locale
slug: string
}
// Schema de validação em tempo de execução
const paramsSchema = z.object({
locale: z.enum(locales),
slug: z.string().min(1)
})
export async function generateStaticParams(): Promise<PageParams[]> {
const posts = await fetchAllPosts()
// Gera o caminho correspondente para cada idioma
return locales.flatMap(locale =>
posts.map(post => ({
locale,
slug: post.slug
}))
)
}
export default async function BlogPost({
params
}: {
params: PageParams
}) {
// Validação em tempo de execução
const result = paramsSchema.safeParse(params)
if (!result.success) {
notFound()
}
const { locale, slug } = result.data
// Obtém o artigo no idioma correspondente
const post = await fetchPost(slug, locale)
if (!post) {
notFound()
}
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}
Vantagens deste código:
- O tipo
Localefica limitado a"zh" | "en" | "ja"; um valor incorreto gera erro - O tipo de retorno de
generateStaticParamséPageParams[], garantindo a estrutura correta - O Zod valida os dados em tempo de execução e bloqueia requisições inválidas
- Todo o fluxo é rigorosamente tipado, desde a definição até a validação em tempo de execução
Diagnóstico de problemas comuns de tipo
Pergunta 1: o tipo de params é Promise<...>. O que fazer?
A partir do Next.js 15, params pode ser assíncrono. Escreva assim:
export default async function Page({
params
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params // Primeiro use await
// ...
}
Ou use a versão síncrona, se tiver certeza de que o projeto está no Next.js 14:
export default async function Page({
params
}: {
params: { slug: string }
}) {
// Use diretamente
}
Pergunta 2: as sugestões de tipo estão incorretas
Se o TypeScript indicar que params é any, confira:
- Se o modo estrito está ativado em
tsconfig.json - Se os tipos do Next.js foram importados corretamente
- Se o arquivo foi nomeado corretamente: ele deve ser
page.tsx
Pergunta 3: a validação do Zod falhou, mas quero ver o erro detalhado
const result = paramsSchema.safeParse(params)
if (!result.success) {
console.error('Falha ao validar os parâmetros:', result.error.format())
notFound()
}
Checklist de segurança de tipos
Confira estes pontos no projeto para garantir a segurança de tipos:
- Todas as páginas com rotas dinâmicas definem o tipo de
params - O tipo retornado por
generateStaticParamscorresponde ao deparams - Rotas críticas usam validação em tempo de execução com Zod
- O modo estrito do TypeScript está ativado
- Parâmetros complexos usam union types ou literal types
Com essas medidas, seu sistema de rotas dificilmente terá bugs relacionados a tipos.
Conclusão
Se você acompanhou o artigo até aqui, agora tem uma visão completa das rotas dinâmicas no Next.js. Vamos recapitular:
✅ Rotas dinâmicas básicas: use [slug] para corresponder a um segmento do caminho e entenda como obter valores por params
✅ Rotas catch-all: use [...slug] para lidar com caminhos de vários níveis e saiba quando recorrer ao parâmetro opcional
✅ generateStaticParams: entenda quando e como usá-lo, além das estratégias de geração sob demanda
✅ Segurança de tipos: aplique uma solução completa, das restrições em tempo de compilação à validação em tempo de execução
O mais importante é compreender a diferença entre App Router e Pages Router para não misturar os dois padrões. Você também já sabe quando pré-renderizar e quando gerar uma página sob demanda, podendo escolher a estratégia de acordo com o cenário.
O que fazer a seguir?
Pratique agora:
- Crie uma rota dinâmica no seu projeto e teste a obtenção de valores por
params - Se houver caminhos com vários níveis, experimente uma rota catch-all
- Adicione tipos do TypeScript e validação com Zod às suas rotas
Continue estudando:
- Rotas paralelas: carregue várias rotas na mesma página com a sintaxe
@folder - Rotas interceptadas: mostre outra rota sem sair da página atual com a sintaxe
(.)folder - Grupos de rotas: organize as rotas com
(folder)sem alterar a estrutura da URL - Middleware: implemente controle de acesso e redirecionamentos no nível da rota
Recursos de estudo:
- Documentação oficial do Next.js — fundamentos de roteamento
- Documentação oficial do Next.js — rotas dinâmicas
- Documentação oficial do Next.js — generateStaticParams
- TypeScript Deep Dive — aprofunde seus conhecimentos de TypeScript
Consulta rápida de problemas comuns:
| Problema | O que verificar | Solução |
|---|---|---|
| A rota dinâmica retorna 404 | Nome da pasta e generateStaticParams | Confira os colchetes e a configuração da geração estática |
params é any | Configuração do TypeScript | Ative o modo estrito e defina o tipo dos parâmetros |
| O build demora demais | Quantidade de itens retornados por generateStaticParams | Reduza as páginas pré-renderizadas e use geração sob demanda |
| Os dados não são atualizados | Estratégia de cache | Configure revalidate ou dynamicParams |
Para encerrar
O sistema de rotas do Next.js mudou bastante na transição do Pages Router para o App Router, e muita gente — inclusive eu — sentiu as dificuldades da migração. Depois que você entende a lógica do App Router, porém, ele fica mais direto e oferece mais recursos.
Rotas dinâmicas são apenas uma parte do Next.js, mas formam a base de toda a aplicação. Quando o roteamento está claro, conceitos como busca de dados, estratégias de cache e middleware ficam muito mais fáceis de aprender.
Se aparecer algum problema durante a prática:
- Consulte primeiro a seção “Troubleshooting” da documentação oficial
- Pesquise uma Issue relacionada no repositório do Next.js no GitHub
- Pergunte na comunidade do Next.js no Discord; ela funciona em inglês, mas costuma responder rapidamente
Não tenha medo de errar. Eu precisei experimentar em vários projetos antes de entender por completo o mecanismo de rotas do App Router. Com este artigo como referência, você deve evitar boa parte desses desvios.
Agora abra seu editor e comece a criar sua rota dinâmica. 🚀
Processo completo para configurar rotas dinâmicas no Next.js
Etapas completas, da criação de uma rota dinâmica às práticas de segurança de tipos
⏱️ Estimated time: 2 hr
- 1
Step 1: Criar a pasta da rota dinâmica
Escolha o tipo de rota conforme a necessidade:
• Um parâmetro: app/posts/[id]/page.tsx
• Vários parâmetros: app/posts/[category]/[id]/page.tsx
• Catch-all: app/posts/[...slug]/page.tsx
• Catch-all opcional: app/posts/[[...slug]]/page.tsx
Regras para nomear pastas:
• [id]: parâmetro obrigatório
• [...slug]: captura todos os segmentos do caminho
• [[...slug]]: captura opcionalmente todos os segmentos do caminho - 2
Step 2: Obter os parâmetros da rota
Obtenha os parâmetros em page.tsx:
• O App Router usa o objeto params
• params é uma Promise e precisa de await
• Use desestruturação para obter cada parâmetro
Exemplo:
export default async function Page({ params }) {
const { id } = await params
return <div>Post {id}</div>
}
Atenção: é obrigatório usar await em params; caso contrário, ocorrerá um erro - 3
Step 3: Configurar a segurança de tipos
Defina os tipos com TypeScript:
• Defina uma interface para o tipo de params
• Use o tipo Promise<{ params }>
• Defina o tipo de retorno de generateStaticParams
Exemplo:
interface PageProps {
params: Promise<{ id: string }>
}
export default async function Page({ params }: PageProps) {
const { id } = await params
// ...
} - 4
Step 4: Implementar geração estática (opcional)
Use generateStaticParams:
• Retorne todas as combinações possíveis de parâmetros
• Use uma função async para buscar os dados
• Gere estaticamente todas as páginas
Exemplo:
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map(post => ({ id: post.id }))
}
Atenção: isso serve apenas para geração estática; rotas dinâmicas não são obrigadas a usá-lo - 5
Step 5: Tratar parâmetros opcionais
Rota catch-all opcional:
• Use a sintaxe [[...slug]]
• params.slug pode ser undefined
• Verifique se o parâmetro existe
Exemplo:
export default async function Page({ params }) {
const { slug } = await params
if (!slug) {
return <div>Todos os posts</div>
}
return <div>Categoria: {slug.join('/')}</div>
} - 6
Step 6: Testar e validar
Pontos de teste:
• Teste se todas as rotas funcionam
• Valide se os parâmetros foram obtidos corretamente
• Confira se as sugestões de tipo funcionam
• Teste se a geração estática foi concluída
Checklist:
• Todas as rotas dinâmicas podem ser acessadas
• Os tipos dos parâmetros estão corretos
• generateStaticParams retorna os dados corretos
• Os erros 404 foram tratados
FAQ
Como obter os parâmetros de uma rota dinâmica?
Pontos principais:
• params é uma Promise e exige await
• Use desestruturação para obter cada parâmetro
• É necessário definir os tipos
Exemplo:
export default async function Page({ params }) {
const { id } = await params
return <div>{id}</div>
}
Por que uma rota dinâmica retorna 404?
• Nome incorreto da pasta (deve ser [id], não {id})
• Caminho incompatível (confira a URL e a estrutura de pastas)
• Dados incompletos retornados por generateStaticParams
• Arquivo page.tsx ausente
Como resolver:
• Confira se o nome da pasta está correto
• Confirme se o caminho da URL corresponde à estrutura de pastas
• Verifique o valor retornado por generateStaticParams
Qual é a diferença entre uma rota catch-all e uma catch-all opcional?
• Precisa corresponder a pelo menos um segmento do caminho
• /posts/[...slug] corresponde a /posts/a, mas não a /posts
Rota catch-all opcional [[...slug]]:
• Pode corresponder a zero ou mais segmentos do caminho
• /posts/[[...slug]] corresponde a /posts e /posts/a/b
Casos de uso:
• Catch-all: é necessário ter pelo menos um parâmetro
• Catch-all opcional: o parâmetro pode ser omitido
Como implementar rotas dinâmicas com segurança de tipos?
1) Defina uma interface para o tipo de params
2) Use o tipo Promise<{ params }>
3) Defina o tipo de retorno de generateStaticParams
Exemplo:
interface PageProps {
params: Promise<{ id: string }>
}
export default async function Page({ params }: PageProps) {
const { id } = await params
// ...
}
Quando usar generateStaticParams?
Casos adequados:
• Todos os valores possíveis dos parâmetros são conhecidos
• É necessário gerar todas as páginas estaticamente
• O objetivo é melhorar o desempenho e o SEO
Casos inadequados:
• Os valores dos parâmetros mudam dinamicamente
• Há parâmetros demais para enumerar
• Os dados precisam ser atualizados em tempo real
Atenção: ele serve apenas para geração estática; rotas dinâmicas não são obrigadas a usá-lo
Como migrar uma rota dinâmica do Pages Router?
• getStaticPaths → generateStaticParams
• context.params → params (com await)
• O formato de retorno muda de { paths, fallback } para um array
Etapas da migração:
1) Troque getStaticPaths por generateStaticParams
2) Altere a forma de obter os parâmetros (use await params)
3) Atualize as definições de tipo
4) Teste todas as rotas
Como tratar uma rota dinâmica com vários parâmetros?
app/posts/[category]/[id]/page.tsx
Obtenha os parâmetros:
export default async function Page({ params }) {
const { category, id } = await params
return <div>{category} - {id}</div>
}
generateStaticParams retorna todas as combinações:
export async function generateStaticParams() {
return [
{ category: 'tech', id: '1' },
{ category: 'tech', id: '2' },
// ...
]
}
19 min de leitura · Publicado em: 25 dez 2025 · Atualizado em: 4 set 2026
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Next.js App Router na prática: use route groups e layouts aninhados para organizar diretórios em projetos grandes
Use route groups, layouts aninhados, parallel routes e intercepting routes para resolver diretórios confusos, conflitos de rota e colaboração em equipes em projetos Next.js grandes, com uma estrutura completa pronta para adaptar.
Parte 5 de 51
Próximo
Armadilhas comuns do Next.js App Router e soluções: 8 experiências práticas para evitar retrabalho
Da busca de dados ao tratamento de erros, veja 14 armadilhas comuns no desenvolvimento com Next.js App Router e como resolvê-las. Inclui confusão entre Server Components e Client Components, cache, migração e outras lições práticas para evitar a maioria dos erros recorrentes.
Parte 7 de 51



Comentários
Entre com GitHub para comentar