React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

best-practicessummarynextjs-routing

Next.js Routing Best Practices

A condensed summary of the 25 most important best practices drawn from every page in this section.

  1. Await params and searchParams: In Next.js 15+ both props are Promises in Server Components — const { id } = await params — destructuring without await yields the Promise object and your lookups silently return undefined.
  2. Don't Collide page.tsx With route.tsx: A folder cannot contain both a page.tsx and a route.tsx — the Route Handler will shadow the page, so keep API routes under app/api/ to avoid silent conflicts.
  3. Use template.tsx for Per-Nav Resets: layout.tsx persists across navigation and its state survives child route changes; when you specifically need to remount (e.g., reset animations or state on each nav), use template.tsx instead.
  4. Parse Dynamic Param Types: Every dynamic segment value arrives as a string — const postId = Number(params.id) — so coerce to the shape you actually need instead of trusting the type annotation alone.
  5. Use [[...slug]] When the Base Path Must Match: A catch-all [...path] does not match the parent route, so /docs 404s; switch to [[...path]] or add a separate page.tsx alongside it when the base segment should resolve.
  6. Let Static Beat Dynamic Explicitly: When static and dynamic routes overlap, the static one always wins, so you can keep a /blog/about/page.tsx next to [slug]/page.tsx without conflict.
  7. generateStaticParams Runs at Build Time: The function executes during next build, so any unavailable data source fails the build; pair it with dynamicParams = true when new paths should still render on first request.
  8. error.tsx Must Be "use client": React Error Boundaries require client lifecycle, so every error.tsx needs the "use client" directive — and it only catches errors below itself, never in the sibling layout.tsx at the same segment.
  9. global-error.tsx Owns html and body: global-error.tsx replaces the entire root layout when it triggers, so it must render its own <html><body>; also note it only activates in production (the dev overlay runs in development).
  10. notFound and redirect Throw: Both throw Next.js sentinel errors, so a surrounding try/catch swallows them — call them outside try/catch, or use unstable_rethrow in the catch block to let them propagate.
  11. Reserve Root not-found for Unmatched Routes: The root not-found.tsx auto-catches any URL that does not match a route, while nested not-found.tsx files only fire when you explicitly call notFound() from within that subtree.
  12. Layouts Can't Read searchParams: Only pages receive searchParams; if a layout needs URL state, lift the logic into a page or a Client Component that uses useSearchParams() inside a Suspense boundary.
  13. Pass Data Via Context, Not Layout Props: Layouts cannot forward props to their children, so share state through a Client Context provider, cookies, or a Server Component helper that re-fetches in each child.
  14. Import useRouter From next/navigation: In the App Router, useRouter, usePathname, and useSearchParams live in next/navigation; next/router is the Pages Router and silently fails or 404s when imported.
  15. Wrap useSearchParams in Suspense: useSearchParams() without a surrounding <Suspense> boundary opts the whole route into client-side rendering — always gate it: <Suspense fallback={null}><SearchFilters /></Suspense>.
  16. Use useTransition for Pending UI: router.push() returns void, so wrap navigations in useTransitionstartTransition(() => router.push("/dashboard")) — to get an isPending flag and disable buttons or show spinners during route changes.
  17. router.refresh Scoped to Current Route: refresh() re-fetches server data for the active route only; other cached routes stay stale, so pair it with revalidateTag/revalidatePath when the mutation affects siblings.
  18. Control Link Prefetch and Scroll: <Link> prefetches on viewport entry and scrolls to top by default — <Link href="/settings" prefetch={false} scroll={false}> — set prefetch={false} on rarely used links and scroll={false} for tab/filter UIs that should stay in place.
  19. Always Set a Middleware matcher: Without config.matcher your middleware runs on every request including /_next/static and favicons — export const config = { matcher: ["/dashboard/:path*", "/api/:path*"] } — scope it explicitly and exclude redirect destinations to avoid infinite loops.
  20. Edge Middleware Forbids Node APIs: Middleware runs in the Edge Runtime, so bcrypt, fs, and most database drivers break at build or runtime — use Web APIs and edge-compatible libraries only.
  21. Return NextResponse.next to Continue: NextResponse.next() does not short-circuit; you must return it (or a redirect/rewrite) to actually stop the middleware chain, otherwise subsequent logic keeps executing.
  22. Every Parallel Slot Needs default.tsx: @slot folders must include a default.tsx fallback, otherwise hard navigations or mismatched subroutes 404; slots must also be direct children of the layout and cannot nest inside each other.
  23. Reserve Space for Independent Slots: Parallel route slots resolve independently, so give each one a fixed height or skeleton matching the final content — otherwise streaming causes visible layout shift as each slot fills in.
  24. Mind the Intercepting Dot Convention: (.), (..), (..)(..), and (...) count route-group segments the same as regular folders, so route groups may require an extra (..) hop; missing default.tsx in the hosting slot also breaks back-navigation dismiss.
  25. Route Groups Cannot Collide: Two (group) folders cannot resolve to the same URL path (the build fails), and multiple root layouts force a full page reload between groups instead of soft navigation — keep the home route / in exactly one group.