Alternar tema

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

Easton editorial illustration: server-client bridge

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:

  1. ❌ Usar [slug].tsx como nome do arquivo (o App Router exige uma pasta)
  2. ❌ Acessar props.slug diretamente (o valor vem pelo objeto params)
  3. ❌ 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:

RecursoPages RouterApp Router
Local do arquivopages/blog/[slug].tsxapp/blog/[slug]/page.tsx
Obtenção do parâmetrorouter.query.slug ou getStaticPropsparams.slug
Definição de tipoManualInferida a partir do tipo das props
Geração estáticagetStaticPathsgenerateStaticParams

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:

  1. O componente usa async, pois Server Components aceitam operações assíncronas
  2. Primeiro os dados são obtidos; depois, o resultado determina o que será renderizado
  3. 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:

  1. slugArray.join('/') transforma o array do caminho em uma string
  2. A navegação por breadcrumbs usa slice para obter cada prefixo do caminho
  3. 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 rotaNome da pastaO que correspondeTipo do parâmetro
Rota dinâmica[slug]/blog/123string
Catch-all[...slug]/docs/a/b/c (sem incluir /docs)string[]
Catch-all opcional[[...slug]]/docs e /docs/a/b/cstring[] | 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:

  1. Consultar o banco de dados para buscar o conteúdo
  2. Renderizar o HTML no servidor
  3. 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?

  1. generateStaticParams é executado durante o build e retorna os slugs de todos os artigos
  2. O Next.js pré-renderiza um arquivo HTML estático para cada slug
  3. 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 = true para gerar páginas sob demanda
  • Usar revalidate para 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:

  1. O tipo Locale fica limitado a "zh" | "en" | "ja"; um valor incorreto gera erro
  2. O tipo de retorno de generateStaticParams é PageParams[], garantindo a estrutura correta
  3. O Zod valida os dados em tempo de execução e bloqueia requisições inválidas
  4. 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:

  1. Se o modo estrito está ativado em tsconfig.json
  2. Se os tipos do Next.js foram importados corretamente
  3. 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 generateStaticParams corresponde ao de params
  • 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:

Consulta rápida de problemas comuns:

ProblemaO que verificarSolução
A rota dinâmica retorna 404Nome da pasta e generateStaticParamsConfira os colchetes e a configuração da geração estática
params é anyConfiguração do TypeScriptAtive o modo estrito e defina o tipo dos parâmetros
O build demora demaisQuantidade de itens retornados por generateStaticParamsReduza as páginas pré-renderizadas e use geração sob demanda
Os dados não são atualizadosEstratégia de cacheConfigure 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:

  1. Consulte primeiro a seção “Troubleshooting” da documentação oficial
  2. Pesquise uma Issue relacionada no repositório do Next.js no GitHub
  3. 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. 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. 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. 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. 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. 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. 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?
O App Router obtém os parâmetros por meio do objeto params.

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?
Possíveis causas:
• 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?
Rota catch-all [...slug]:
• 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?
Etapas:
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?
Use-o para gerar estaticamente todas as páginas possíveis.

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?
Principais mudanças:
• 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?
Crie pastas em vários níveis:
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

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog