Catch-All Routing with Next.js

Website Rebuild with Next.js

Chapter 3 · Catch-All Routing with Next.js

Every sibling course's own routing chapter cites this one by the same shape: one catch-all route, one lookup by path, resolved fresh per request, with no pre-declared list of valid URLs anywhere. This chapter builds that shape directly, using the exact mandatory async params pattern Chapter 1 already flagged as firmer than ever in Next.js 16.

Generating Type Helpers First

npx next typegen

This generates the globally-available PageProps/LayoutProps/RouteContext type helpers, used below with zero import needed — they're ambient types, resolved automatically from the project's own route structure.

The Catch-All Route

// app/[...path]/page.tsx import { prisma } from '@/lib/prisma'; import { notFound } from 'next/navigation'; export default async function CatchAllPage({ params }: PageProps<'/[...path]'>) { const { path } = await params; const fullPath = path.join('/'); const page = await prisma.page.findUnique({ where: { fullPath } }); if (!page) { notFound(); } return <h1>{page.title}</h1>; }
A real difference from Astro's own catch-all, not just syntax
Astro's own [...path].astro gives Astro.params.path as a single, already-slash-joined string. Next.js's [...path] gives params.path as a genuine string[] — one array element per URL segment. path.join('/') reconstructs the full path string this course's own fullPath column expects. Forgetting this and querying against the raw array directly is a real, easy mistake — Prisma won't error, it'll just never match any row.

Not Found: No Manual Status Code Needed

// app/not-found.tsx export default function NotFound() { return <h1>Page not found</h1>; }
Genuinely cleaner than Astro's own two-piece combination
Astro's own Chapter 3 needed Astro.response.status = 404 and Astro.rewrite('/404') together — one for the status code, one for the content — because its catch-all otherwise shadows 404.astro entirely. Next.js's notFound() does both jobs by itself: calling it renders the nearest not-found.tsx boundary and sets a real 404 HTTP status automatically. One function call, not two separate pieces of manual bookkeeping.

Routing, Compared Across the Whole Series

Next.js Django Laravel Rails Astro Express
Mechanism[...path] folder<path:full_path>{path?} + regex*path glob[...path].astro/*splat (Express 5)
Path arrives asA string[] array — needs .join('/')A single stringA single stringA single stringA single, already-joined stringA single string
Resolved byA database lookupA database lookupA database lookupA database lookupA database lookupA database lookup
Not-found handlingOne call: notFound()Manual Http404 raiseManual abort(404)Manual render status: :not_foundTwo pieces: status + rewriteManual res.status(404)

The mechanism differs in shape everywhere, but the underlying architecture — a database lookup standing in for a pre-declared path list — is genuinely identical across all six frameworks. Only Next.js's own array-vs-string parameter shape and its single-call notFound() stand out as real, structural differences rather than syntax variation.

Hands-On Exercises

Exercise 1

Build app/[...path]/page.tsx with a real Prisma query resolving the joined params.path against the fullPath column, and confirm a real stored page renders correctly at its own full path.

📄 View solution
Exercise 2

Deliberately query against the raw params.path array instead of the joined string, and observe exactly what happens — no error, just a page that never matches. Explain why Prisma doesn't raise a type error here even though the query is wrong.

📄 View solution
Exercise 3

Visit a genuinely nonexistent path with app/not-found.tsx already in place, and confirm both the correct page content renders and the real HTTP response carries a 404 status code — with only the single notFound() call handling both.

📄 View solution

Chapter 3 Quick Reference

  • npx next typegen — generates the ambient PageProps/LayoutProps helpers used with zero import
  • app/[...path]/page.tsx — the catch-all route; params.path arrives as a string[], joined with .join('/')
  • No pre-declared path list — every request resolves fresh, via a real database lookup
  • notFound() + app/not-found.tsx — one call handles both the correct content and a real 404 status, unlike Astro's own two-piece combination
  • Next chapter: React Components & Layout