Next.js Routing Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
- Await params and searchParams: In Next.js 15+ both props are Promises in Server Components —
const { id } = await params— destructuring withoutawaityields the Promise object and your lookups silently returnundefined. - Don't Collide page.tsx With route.tsx: A folder cannot contain both a
page.tsxand aroute.tsx— the Route Handler will shadow the page, so keep API routes underapp/api/to avoid silent conflicts. - Use template.tsx for Per-Nav Resets:
layout.tsxpersists across navigation and its state survives child route changes; when you specifically need to remount (e.g., reset animations or state on each nav), usetemplate.tsxinstead. - 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. - Use [[...slug]] When the Base Path Must Match: A catch-all
[...path]does not match the parent route, so/docs404s; switch to[[...path]]or add a separatepage.tsxalongside it when the base segment should resolve. - Let Static Beat Dynamic Explicitly: When static and dynamic routes overlap, the static one always wins, so you can keep a
/blog/about/page.tsxnext to[slug]/page.tsxwithout conflict. - generateStaticParams Runs at Build Time: The function executes during
next build, so any unavailable data source fails the build; pair it withdynamicParams = truewhen new paths should still render on first request. - error.tsx Must Be "use client": React Error Boundaries require client lifecycle, so every
error.tsxneeds the"use client"directive — and it only catches errors below itself, never in the siblinglayout.tsxat the same segment. - global-error.tsx Owns html and body:
global-error.tsxreplaces 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). - notFound and redirect Throw: Both throw Next.js sentinel errors, so a surrounding
try/catchswallows them — call them outsidetry/catch, or useunstable_rethrowin the catch block to let them propagate. - Reserve Root not-found for Unmatched Routes: The root
not-found.tsxauto-catches any URL that does not match a route, while nestednot-found.tsxfiles only fire when you explicitly callnotFound()from within that subtree. - 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 usesuseSearchParams()inside a Suspense boundary. - 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.
- Import useRouter From next/navigation: In the App Router,
useRouter,usePathname, anduseSearchParamslive innext/navigation;next/routeris the Pages Router and silently fails or 404s when imported. - 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>. - Use useTransition for Pending UI:
router.push()returnsvoid, so wrap navigations inuseTransition—startTransition(() => router.push("/dashboard"))— to get anisPendingflag and disable buttons or show spinners during route changes. - router.refresh Scoped to Current Route:
refresh()re-fetches server data for the active route only; other cached routes stay stale, so pair it withrevalidateTag/revalidatePathwhen the mutation affects siblings. - Control Link Prefetch and Scroll:
<Link>prefetches on viewport entry and scrolls to top by default —<Link href="/settings" prefetch={false} scroll={false}>— setprefetch={false}on rarely used links andscroll={false}for tab/filter UIs that should stay in place. - Always Set a Middleware matcher: Without
config.matcheryour middleware runs on every request including/_next/staticand favicons —export const config = { matcher: ["/dashboard/:path*", "/api/:path*"] }— scope it explicitly and exclude redirect destinations to avoid infinite loops. - 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. - Return NextResponse.next to Continue:
NextResponse.next()does not short-circuit; you mustreturnit (or a redirect/rewrite) to actually stop the middleware chain, otherwise subsequent logic keeps executing. - Every Parallel Slot Needs default.tsx:
@slotfolders must include adefault.tsxfallback, otherwise hard navigations or mismatched subroutes 404; slots must also be direct children of the layout and cannot nest inside each other. - 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.
- Mind the Intercepting Dot Convention:
(.),(..),(..)(..), and(...)count route-group segments the same as regular folders, so route groups may require an extra(..)hop; missingdefault.tsxin the hosting slot also breaks back-navigation dismiss. - 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.