Skip to content
Vibe Code Basics
Next.js

Next.js Routing Explained: Dynamic Routes, Route Groups & Layouts (Next.js 16)

Master Next.js App Router routing: dynamic segments, catch-all routes, route groups, layouts, loading and error states, parallel routes and navigation, with Next.js 16 code examples.

By Vibe Code Basics Editorial TeamPublished Updated 3 min read
On this page (11)

Next.js uses file-based routing: folders inside the app directory become URL segments, and a page file makes a route publicly accessible. Dynamic routes use square brackets like [slug], route groups use parentheses like (marketing), and special files such as layout, loading and error add shared UI and states.

This guide covers routing in Next.js 16. If you're brand new, read Getting Started with Next.js first. Knowing these conventions also makes you far better at reviewing what your AI agent builds, which is a core vibe coding skill.

File-based routing basics

FileURL
app/page.tsx/
app/about/page.tsx/about
app/blog/page.tsx/blog
app/blog/[slug]/page.tsx/blog/any-post

A folder without a page file isn't a route, so you can keep components and utilities next to the pages that use them.

Special files

FilePurpose
layout.tsxShared UI that wraps child routes and keeps state between navigations
page.tsxThe unique UI for a route
loading.tsxInstant loading state while the page streams in
error.tsxError boundary for the segment (must be a Client Component)
not-found.tsxUI shown when you call notFound()
route.tsAn API endpoint instead of a page (see Route Handlers)

Dynamic routes

Wrap a folder name in square brackets to capture part of the URL as a parameter.

// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: PageProps<'/blog/[slug]'>) {
  const { slug } = await params;
  return <h1>Post: {slug}</h1>;
}

Two things to notice in Next.js 16:

  1. params is a Promise. You must await it. Synchronous access was fully removed in Next.js 16, and AI models trained on older code often get this wrong.
  2. PageProps is a global type helper generated by Next.js (run npx next typegen if your editor doesn't see it), giving you type-safe params.

Prerender dynamic routes with generateStaticParams

For content like blog posts, prerender every page at build time:

export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

// Return a 404 for any slug not returned above
export const dynamicParams = false;

This is exactly how this site's blog works, and it's great for SEO because every page is static HTML.

Multiple dynamic segments

app/shop/[category]/[product]/page.tsx  →  /shop/shoes/runner-2
export default async function Product({ params }: PageProps<'/shop/[category]/[product]'>) {
  const { category, product } = await params;
  return <h1>{category} / {product}</h1>;
}

Catch-all and optional catch-all routes

  • app/docs/[...slug]/page.tsx matches /docs/a, /docs/a/b/c (but not /docs). slug is an array.
  • app/docs/[[...slug]]/page.tsx also matches /docs, where slug is undefined.
export default async function Docs({ params }: PageProps<'/docs/[[...slug]]'>) {
  const { slug } = await params;
  const path = slug ? slug.join('/') : 'index';
  return <h1>Docs: {path}</h1>;
}

Route groups

Wrap a folder in parentheses to organize routes without changing the URL:

app/
  (marketing)/
    layout.tsx        ← marketing header/footer
    about/page.tsx    → /about
    pricing/page.tsx  → /pricing
  (app)/
    layout.tsx        ← sidebar + auth check
    dashboard/page.tsx → /dashboard

Route groups are perfect for giving marketing pages and logged-in app pages completely different layouts.

Layouts

A layout wraps all routes below it and doesn't re-render on navigation:

// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: LayoutProps<'/dashboard'>) {
  return (
    <div className="flex">
      <nav className="w-60">{/* sidebar links */}</nav>
      <main className="flex-1">{children}</main>
    </div>
  );
}

Loading and error states

// app/dashboard/loading.tsx
export default function Loading() {
  return <p>Loading dashboard…</p>;
}
// app/dashboard/error.tsx
'use client';

export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <div>
      <p>Something went wrong: {error.message}</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

Next.js 16.3 also added catchError from next/error for custom error boundaries that can retry Server Components.

Parallel routes

Parallel routes render multiple pages in the same layout using named "slots" (folders starting with @), which is useful for dashboards and modals:

app/dashboard/
  @analytics/page.tsx
  @analytics/default.tsx
  @team/page.tsx
  @team/default.tsx
  layout.tsx

Since Next.js 16, every slot needs a default.tsx file or the build fails. A default.tsx that returns null is enough.

Use the Link component for client-side navigation with automatic prefetching:

import Link from 'next/link';

export default function Nav() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/blog">Blog</Link>
    </nav>
  );
}

For navigation in code (after a form submit, for example), use useRouter from next/navigation in a Client Component:

'use client';
import { useRouter } from 'next/navigation';

export default function GoToDashboard() {
  const router = useRouter();
  return <button onClick={() => router.push('/dashboard')}>Dashboard</button>;
}

In Server Components and Server Actions, use redirect() from next/navigation instead.

Routing best practices

  1. Keep URLs short, lowercase and descriptive (good for users and SEO).
  2. Use route groups to separate layouts rather than nesting URLs.
  3. Add loading.tsx to slow routes so navigation feels instant.
  4. Use generateStaticParams for content pages.
  5. Call notFound() for missing records so search engines get a real 404.

Prompting your AI agent about routing

Add a /dashboard area using a (app) route group with its own layout and sidebar.
Pages: /dashboard, /dashboard/settings, /dashboard/projects/[id].
Await params (Next.js 16), add loading.tsx and error.tsx for the group,
and call notFound() when a project id doesn't exist.

More prompt patterns are in our vibe coding prompting guide.

Next steps

  • #Next.js
  • #Routing
  • #Tutorial
  • #React

Keep reading