Suggest an editImprove this articleRefine the answer for “Routing in Next.js (file-based routing)”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**File-based routing in Next.js** maps the `app` folder structure to URL paths. Each `page.tsx` makes that folder accessible as a route. ``` app/page.tsx → / app/docs/page.tsx → /docs app/blog/[slug]/page.tsx → /blog/:slug ``` **Key point:** a folder without `page.tsx` creates no route.Shown above the full answer for quick recall.Answer (EN)Image**File-based routing in Next.js** maps the `app` directory's folder structure directly to URL paths. Each `page.tsx` file defines a route. No router config, no `<Route>` declarations, no imports from react-router. ## Theory ### TL;DR - Folder name = URL segment; `app/docs/page.tsx` becomes `/docs` - `[slug]` in a folder name captures that segment into `params` - `[...slug]` captures 1+ segments as an array; `[[...slug]]` captures 0+ (optional) - Route groups `(name)` organize files without touching the URL - A folder without `page.tsx` creates no route, it only organizes code ### Quick example ```tsx // app/page.tsx → URL: / export default function Home() { return <h1>Home</h1> } // app/blog/page.tsx → URL: /blog export default function Blog() { return <h1>Blog</h1> } // app/blog/[slug]/page.tsx → URL: /blog/my-first-post export default function Post({ params }: { params: { slug: string } }) { return <h1>Post: {params.slug}</h1> // "my-first-post" } ``` Next.js scans `app/` at build time and generates a route manifest from the folder structure. That manifest is what the server uses to match incoming requests. ### How it works internally Next.js (via Turbopack or webpack) walks the `app` directory recursively at build time. Each `page.tsx` becomes a route segment based on its folder path. Dynamic segments like `[slug]` become regex patterns in the route manifest (`build-manifest.json`). At request time, the server matches the URL against those patterns and extracts `params`. The `params` object is injected as a prop through React Server Components, so it is available server-side by default. On client components, you use `useParams()` from `next/navigation` instead. Mixing these up is the most common bug I see in code reviews when a server component gets refactored into a client one and nobody updates the params access. ### Dynamic routes Square brackets mark a folder as a dynamic segment: ```tsx // app/shop/[category]/page.tsx → /shop/laptops, /shop/phones export default async function Category({ params }: { params: { category: string } }) { const products = await getProducts(params.category) return <h1>{params.category}</h1> } ``` Nesting works the same way. `app/shop/[category]/[id]/page.tsx` gives you both `params.category` and `params.id` at `/shop/laptops/123`. ### Catch-all routes `[...slug]` captures one or more segments as an array: ```tsx // app/docs/[...slug]/page.tsx // /docs/react → slug = ['react'] // /docs/react/hooks → slug = ['react', 'hooks'] export default function Docs({ params }: { params: { slug: string[] } }) { return <div>{params.slug.join(' / ')}</div> } ``` `[[...slug]]` (double brackets) also matches the root path. So `app/docs/[[...slug]]/page.tsx` handles `/docs` (empty array) and every deeper path. Single `[...slug]` requires at least one segment. That is the only difference, but it causes a lot of 404s when building docs indexes. ### Route groups Parentheses create a grouping folder that does not appear in the URL: ``` app/ (marketing)/ layout.tsx <- marketing layout about/page.tsx -> /about pricing/page.tsx -> /pricing (platform)/ layout.tsx <- platform layout dashboard/page.tsx -> /dashboard ``` `(marketing)` and `(platform)` never show up in the URL. Each group can have its own `layout.tsx`, so `/about` and `/dashboard` use completely different layouts while living in the same `app/` directory. ### Parallel routes Parallel routes render multiple independent sections inside one layout. Slots are defined with `@`: ``` app/dashboard/ layout.tsx page.tsx @stats/page.tsx @activity/page.tsx ``` ```tsx // app/dashboard/layout.tsx export default function DashboardLayout({ children, stats, activity }: { children: React.ReactNode stats: React.ReactNode activity: React.ReactNode }) { return ( <div> {children} <div className="grid grid-cols-2 gap-4"> {stats} {activity} </div> </div> ) } ``` Each slot loads and streams independently. `@stats` can show a loading state while `@activity` is already rendered. ### Intercepting routes Intercepting routes let you show a route in a different context without changing the URL. The standard use case: clicking a photo opens a modal, but navigating directly to `/photos/123` shows the full photo page. ``` (.) — intercept at the same level (..) — one level up (..)(..) — two levels up (...) — from the app root ``` On IT Lead, clicking a problem card opens a modal preview. Direct URL access shows the full problem page. That is intercepting routes in practice. ### Special files | File | Purpose | |------|----------| | `page.tsx` | Page UI (makes the segment accessible as a route) | | `layout.tsx` | Shared layout, persists across navigations | | `loading.tsx` | Suspense fallback during data fetching | | `error.tsx` | Error boundary for the segment | | `not-found.tsx` | Custom 404 for the segment | | `template.tsx` | Like layout but re-mounts on every navigation | | `route.ts` | API endpoint (Route Handler, App Router only) | A folder without `page.tsx` creates no route. It only organizes code (colocation). Next.js registers a segment only where it finds a `page.tsx`. ### Common mistakes **Mistake 1: Accessing `params` directly in a client component** ```tsx // WRONG 'use client' export default function Page({ params }: { params: { slug: string } }) { return <div>{params.slug}</div> // undefined on client } // CORRECT 'use client' import { useParams } from 'next/navigation' export default function Page() { const params = useParams() // works client-side return <div>{params.slug as string}</div> } ``` `params` as a prop only works in Server Components. Client components need `useParams()`. **Mistake 2: Using `[...slug]` when a single segment is enough** ```tsx // app/[...userId]/page.tsx // /profile/123 → params.userId = ['profile', '123'] — captures too much // Fix: app/profile/[userId]/page.tsx ``` Catch-all grabs everything in the path, including segments you didn't intend to capture. **Mistake 3: Forgetting `generateStaticParams` for dynamic routes** ```tsx // Without this, /blog/[slug] is server-rendered on every request → slow TTFB export async function generateStaticParams() { const posts = await getPosts() return posts.map((post) => ({ slug: post.slug })) } ``` If you know your slugs at build time, export `generateStaticParams`. Next.js pre-renders those pages as static HTML. **Mistake 4: Confusing `[...slug]` and `[[...slug]]`** `[...slug]` at `/docs` (no segments) returns a 404. `[[...slug]]` at `/docs` matches and gives `params.slug = []`. When your docs index page also needs to match, use double brackets. ### Real-world usage - Next.js blog: `/blog/[slug]` fetches MDX posts from the filesystem - Vercel docs: `/docs/[product]/[version]/api` for multi-product documentation - IT Lead: intercepting routes for problem modal previews - CMS-backed sites: `generateStaticParams` pre-builds pages from API at deploy time - Linear-style apps: `[...id]` catch-all for deep-linked issue URLs ### Follow-up questions **Q:** How does `[...slug]` differ from `[[...slug]]`? **A:** `[...slug]` requires at least one segment. A request to `/docs` with no further path will 404. `[[...slug]]` makes all segments optional, so `/docs` matches and returns an empty array. **Q:** What happens at `/docs/a/b` when the file is `app/docs/[slug]/page.tsx`? **A:** 404. A single `[slug]` captures exactly one segment. Multi-segment paths need `[...slug]`. **Q:** How do `params` and `searchParams` differ? **A:** `params` comes from the URL path (`/post/123` gives `{ id: '123' }`). `searchParams` comes from the query string (`?draft=true` gives `{ draft: 'true' }`). Both are available as props in Server Components. **Q:** How do you pre-build dynamic routes as static pages? **A:** Export `generateStaticParams()` returning an array of param objects. Next.js runs this at build time and generates one static HTML file per entry. **Q:** (Senior) Middleware intercepts `/[slug]` and rewrites the URL. Do `params` still work in the page component? **A:** Yes. Use `NextResponse.rewrite(new URL('/real-path', req.url))` in middleware. The rewrite changes which file handles the request, but `params` are extracted from the URL pattern of the matched file, so they stay intact. ## Examples ### Basic route structure ```tsx // app/page.tsx → / export default function Home() { return <h1>Home</h1> } // app/problems/page.tsx → /problems export default function Problems() { return <h1>Problems</h1> } // app/problems/[id]/page.tsx → /problems/fizzbuzz export default function Problem({ params }: { params: { id: string } }) { return <h1>Problem: {params.id}</h1> } ``` Folder name becomes URL segment. `page.tsx` makes it accessible. That is the whole system. ### Blog post loader with notFound ```tsx // app/blog/[slug]/page.tsx import { notFound } from 'next/navigation' async function getPost(slug: string) { const res = await fetch(`https://api.example.com/posts/${slug}`, { cache: 'force-cache' // cached at build time or via ISR }) if (!res.ok) notFound() // triggers the nearest not-found.tsx return res.json() } export default async function Post({ params }: { params: { slug: string } }) { const post = await getPost(params.slug) return ( <article> <h1>{post.title}</h1> <div dangerouslySetInnerHTML={{ __html: post.content }} /> </article> ) } // Pre-build known slugs at deploy time export async function generateStaticParams() { const posts = await getPosts() return posts.map((p) => ({ slug: p.slug })) } ``` `notFound()` from `next/navigation` triggers the nearest `not-found.tsx`. `generateStaticParams` tells Next.js which slugs to pre-render as static HTML, which removes per-request server rendering and improves TTFB. ### Catch-all docs route with optional root match ```tsx // app/docs/[[...slug]]/page.tsx // Matches: /docs, /docs/react, /docs/react/hooks/useState export default function Docs({ params }: { params: { slug?: string[] } }) { if (!params.slug || params.slug.length === 0) { return <h1>Docs Home</h1> } const path = params.slug.join('/') // /docs/react/hooks → path = "react/hooks" return <div>Doc: /{path}</div> } ``` Double brackets `[[...slug]]` make all segments optional. One file handles the docs index page and every nested path below it. With single brackets `[...slug]`, the index would 404.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.