App Router routing cheatsheet.

Dynamic segment

app/posts/[id]/page.tsx       → /posts/:id
async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return <p>{id}</p>;
}

Catch-all

app/docs/[...slug]/page.tsx   → /docs/a, /docs/a/b, /docs/a/b/c
async function Page({ params }: { params: Promise<{ slug: string[] }> }) {
  const { slug } = await params;     // ["a", "b", "c"]
}

Optional catch-all

app/docs/[[...slug]]/page.tsx → /docs, /docs/a, /docs/a/b

slug may be undefined.

Route groups (parens, not in URL)

app/(marketing)/about/page.tsx   → /about
app/(app)/dashboard/page.tsx     → /dashboard

Group layouts without affecting URL:

app/(marketing)/layout.tsx       # wraps marketing pages only
app/(app)/layout.tsx             # wraps app pages only

Parallel routes (@slots)

app/dashboard/
├── layout.tsx
├── page.tsx
├── @analytics/page.tsx
└── @team/page.tsx
// layout.tsx
export default function Layout({ children, analytics, team }: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <div>
      {children}
      {analytics}
      {team}
    </div>
  );
}

Each slot has its own loading/error states.

Intercepted routes

app/feed/page.tsx
app/feed/(.)photo/[id]/page.tsx    # intercepts /photo/:id when navigating from /feed
app/photo/[id]/page.tsx            # full route when directly visited

(.): same level. (..): one level up. (...): from root.

Common use: open photo as modal in feed; full page on direct visit.

Default page

app/dashboard/@team/default.tsx

Renders when slot doesn’t match (parallel routes).

generateStaticParams

// app/posts/[id]/page.tsx
export async function generateStaticParams() {
  const posts = await db.post.findMany();
  return posts.map((p) => ({ id: String(p.id) }));
}

async function Page({ params }) {
  const { id } = await params;
  ...
}

Pre-renders the pages at build time (SSG).

dynamic = “force-static” | “force-dynamic”

export const dynamic = "force-static";
// All requests rendered at build time. No dynamic features.

export const dynamic = "force-dynamic";
// All requests rendered on demand.

export const dynamic = "auto";       // default — Next decides

dynamicParams

export const dynamicParams = false;
// 404 for params not in generateStaticParams

revalidate

export const revalidate = 60;
// ISR: regenerate page every 60s

fetchCache

export const fetchCache = "force-cache" | "default-cache" | "default-no-store" | "force-no-store";

Default fetch caching for the route.

metadata

// Static
export const metadata = { title: "Page" };

// Dynamic
export async function generateMetadata({ params }) {
  const { id } = await params;
  const post = await fetchPost(id);
  return { title: post.title };
}

redirect / notFound at route level

async function Page({ params }) {
  const post = await db.post.find(params.id);
  if (!post) notFound();           // shows nearest not-found.tsx
  return <Article post={post} />;
}

Search params

async function Page({ searchParams }: { searchParams: Promise<{ q?: string }> }) {
  const { q } = await searchParams;
  return <p>q = {q}</p>;
}

In Next 15+ searchParams is a Promise.

Middleware

// middleware.ts (root)
import { NextResponse, type NextRequest } from "next/server";

export function middleware(req: NextRequest) {
  if (!req.cookies.has("session")) {
    return NextResponse.redirect(new URL("/login", req.url));
  }
  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*"],
};

Common mistakes

  • Forgetting await params in Next 15+ — TypeError.
  • Confusing route groups (()) with dynamic segments ([]).
  • Multiple [id] segments at the same level — conflict.
  • force-dynamic on a static page — kills caching.
  • Catch-all [...slug] blocking sibling routes.

Read this next

If you want my routing patterns + middleware setup, they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .