skip to content

In a Next.js App Router app with app/blog/[slug]/page.tsx, what does exporting generateStaticParams from that file do, and what shape must it return?

level: middleimportance: must knowfreq 62%

answer

  1. dynamic routes have no URL list
  2. enumerate the segment values
  3. exported from the dynamic segment's page file
  4. one flat object per route
  5. keys are the bracket names, values strings

basics

~20 s

generateStaticParams tells Next which concrete values of a dynamic segment to prerender at build time. It returns an array with one object per route — for [slug], entries like { slug: 'hello' } — with keys named after the bracketed folders and string values.

solid answer

~40 s

Without it, Next cannot enumerate the URLs a dynamic route can serve, so pages are rendered when a request arrives. `generateStaticParams` supplies that list: it runs on the server **at build time**, and Next builds one static page per entry it returns. The shape is an array of plain objects whose keys are the bracketed folder names for that route: `[{ slug: 'hello' }, { slug: 'why-next' }]` for `app/blog/[slug]/page.tsx`. Values must be strings — coerce numeric ids with `String(post.id)` — and a catch-all segment takes a string array instead, `{ slug: ['api','auth'] }`. The function is async-capable, so it typically fetches the id list from a CMS or database. If the route has several dynamic segments, each returned object carries every one of them, for example `{ category: 'shoes', id: '42' }`.

code

typescript · 13 lines
typescript
// app/blog/[slug]/page.tsx
type Post = { id: number; slug: string }

async function getPosts(): Promise<Post[]> {
  const res = await fetch('https://example.com/api/posts')
  return res.json()
}

export async function generateStaticParams() {
  const posts = await getPosts()
  // one flat object per route; values must be strings
  return posts.map((post) => ({ slug: post.slug }))
}

go deeper

for a junior

Know that a dynamic route has no built-in list of URLs and that generateStaticParams supplies one so those pages can be built ahead of time. Recall that it returns an array of objects keyed by the bracket name.

for a middle

Explain the exact return shape — flat objects, string values, arrays for catch-alls, every segment of a multi-parameter route — and that the function is async and runs on the server at build time.

for a senior

Talk about the build itself: how big a list you are willing to enumerate, where the id list comes from in CI, and what breaks when that data source is unavailable or slow during a build.

for a principal

Own the tradeoff between build-time enumeration and on-demand rendering across a growing catalog, including build duration budgets, how content publishing triggers rebuilds, and who owns the freshness contract with the content team.

## The problem it solves A static segment such as `app/about/page.tsx` corresponds to exactly one URL, so Next can build it ahead of time without being told anything. A dynamic segment such as `app/blog/[slug]/page.tsx` corresponds to an unbounded set of URLs, and nothing in the filesystem says which ones actually exist. Left alone, Next has no choice but to render such a page when a request for it arrives. `generateStaticParams` is how you close that gap: it is the route's answer to "which values of this segment are real?" ## The contract Export it from the same file as the dynamic route's `page`: ```ts // app/blog/[slug]/page.tsx export async function generateStaticParams() { const posts = await getPosts() return posts.map((post) => ({ slug: post.slug })) } ``` Three rules govern the return value: 1. It is an **array**, and each element describes one route. 2. Each element is a plain object whose **keys are the bracketed folder names** on that path — `slug` for `[slug]`, `id` for `[id]`. 3. Each value is a **string**, or a string **array** for a catch-all segment. Numeric database ids need `String(post.id)`; a raw number is not a valid segment value. The function may be async and runs on the server during the build, so fetching from a CMS, a database, or the filesystem inside it is the normal case rather than an exception. ## What Next does with the list For each returned object, Next renders that route at build time and writes out the resulting HTML and RSC payload. At request time those URLs are served from the prerendered output — no rendering work per visitor. The page component still receives `params` exactly as it would for an on-demand render, so the page code does not change at all when you add the function: ```tsx export default async function Page({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params // same code, prerendered or not return <article>{slug}</article> } ``` This matters for the interview answer: `generateStaticParams` changes **when** the page renders, not **how**. ## Multiple dynamic segments For `app/shop/[category]/[id]/page.tsx` a single function in the deepest segment returns every parameter on the path: ```ts export function generateStaticParams() { return [ { category: 'shoes', id: '42' }, { category: 'hats', id: '7' }, ] } ``` Alternatively each segment can define its own, in which case the child's function receives the parent's already-resolved params as an argument and only needs to enumerate its own level. Both styles are supported; the single-function version is easier to reason about when the ids come from one query, while the split version avoids fetching the whole cross-product when the levels are independent. ## Catch-all segments The returned value has to mirror the runtime shape, so for `app/docs/[...slug]/page.tsx` the value is an array of segments: ```ts export function generateStaticParams() { return [{ slug: ['api', 'auth'] }, { slug: ['guides', 'setup'] }] } ``` `{ slug: 'api/auth' }` is a common wrong answer — a slash inside a single string is not two segments. For an optional catch-all, `{ slug: [] }` is how you prerender the bare parent URL. ## Common mistakes - **Wrapping the params.** Returning `[{ params: { slug: 'a' } }]` produces objects whose only key is `params`, which is not a segment name on this route. The App Router expects the flat form. - **Returning numbers.** `{ id: 42 }` rather than `{ id: '42' }`. - **Expecting it at request time.** It runs during the build, not per request; it is not a place for per-visitor logic, and it has no access to a request, cookies, or headers. - **Assuming it is exhaustive.** A path you did not return is not automatically a 404 — by default Next will still render it on demand. Controlling that is a separate segment export. - **Enumerating unbounded sets.** Returning fifty thousand entries multiplies your build time by fifty thousand renders; the list is a decision, not a dump of the table. ## How to phrase it in an interview "It is the build-time enumeration for a dynamic route. I export it from the dynamic segment's page file, it returns one flat object per URL keyed by the bracket names with string values, arrays for catch-alls, and Next prerenders one page per entry. The page component itself is unchanged — it still reads `params`."

  • Can generateStaticParams fetch data, and where does that fetch run?
    Yes — it can be async and typically calls a CMS or database to get the id list. It runs on the server during the build, so it has no request, no cookies, and no headers to read. Treat it as build tooling: whatever it fetches has to be reachable at build time, in CI, with build-time credentials.
  • How do you handle a route with two dynamic segments, like [category]/[id]?
    Either return every parameter from one function in the deepest segment — `{ category: 'shoes', id: '42' }` — or give each segment its own `generateStaticParams`, where the child receives the parent's resolved params and enumerates only its own level. The split form avoids materialising the whole cross-product when the levels come from independent queries.
  • Does adding generateStaticParams change how the page component reads its params?
    No. The page still receives the same `params` prop and still awaits it. The export changes only when the render happens — at build for the listed values instead of on request. That is why you can add or remove it without touching the page body.

saying these in an interview costs you the question

  • Returns [{ params: { slug: 'a' } }] instead of the flat form
  • Returns numeric ids without converting them to strings
  • Thinks it runs on every request
  • Returns 'a/b' as a string for a catch-all segment
  • Assumes unlisted paths automatically 404

context