Advanced App Router Routing Patterns (Next.js)

The App Router is more than nested folders. Below are lesser-known patterns I use in real projects to compose UX, keep code maintainable, and ship fast.
Use these as bite-sized recipes; each has a minimal example you can paste into a new Next.js app (App Router enabled).
1) Parallel routes for composable shells
Render multiple route trees in one layout using parallel segments. Great for dashboards with a persistent sidebar or a live preview alongside an editor.
Folder shape:
app/
layout.tsx
@nav/ ← parallel slot
page.tsx
@content/ ← parallel slot
page.tsx
Layout mounts the slots by name:
// app/layout.tsx
export default function RootLayout({
children,
nav,
content,
}: {
children: React.ReactNode;
nav: React.ReactNode;
content: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<aside>{nav}</aside>
<main>{content ?? children}</main>
</body>
</html>
);
}
Overview:

Notes:
- Each slot has its own loading.tsx and error.tsx.
- You can navigate within a slot without re-rendering siblings.
2) Intercepted routes for modal/detail overlays
Open a detail page as a modal over the current list without losing the list state, and open the same route as a full page when navigated directly.
Folder shape:
app/
feed/
page.tsx
@modal/(..)post/[id]/page.tsx ← intercept from feed
post/
[id]/page.tsx ← canonical page
In feed/page.tsx
, render the @modal
slot where the modal should appear:
// app/feed/page.tsx
export default function Feed({ modal }: { modal: React.ReactNode }) {
return (
<>
<ul>{/* feed items */}</ul>
{modal}
</>
);
}
Notes:
(..)post/[id]
means “use the route from a sibling/parent when intercepted”.- Visiting
/post/123
directly renders the full page; clicking from/feed
shows the modal overlay.
3) Route groups to control layout boundaries
Group folders without affecting the URL. Useful for changing layouts per section or isolating error/loading boundaries.
app/
(marketing)/
layout.tsx
page.tsx
(app)/
layout.tsx
dashboard/page.tsx
URL stays /dashboard
; the (app)
layout wraps only the app area.
4) Optional catch-all with smart 404s
Use optional segments to serve nested content while returning proper 404s for unknown paths.
// app/docs/[[...slug]]/page.tsx
import { notFound } from 'next/navigation';
export default async function Docs({ params }: { params: { slug?: string[] }}) {
const path = (params.slug ?? []).join('/');
const doc = await fetchDoc(path); // your lookup
if (!doc) return notFound();
return <article dangerouslySetInnerHTML={{ __html: doc.html }} />;
}
Tips:
- Pair with
generateStaticParams
for hot paths, then fallback dynamic fetch for the rest.
5) Segment-level loading and error boundaries
Each segment can define loading.tsx
and error.tsx
. Errors bubble up until a boundary catches them.
// app/projects/[id]/error.tsx
'use client';
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Failed to load project</h2>
<pre>{error.message}</pre>
<button onClick={reset}>Retry</button>
</div>
);
}
6) Route Handlers with fine-grained caching
Control HTTP caching and ISR on a per-request basis.
// app/api/search/route.ts
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const q = searchParams.get('q') ?? '';
// Cache popular empty query for 60s, but user queries are dynamic.
const isPopular = q === '';
const data = await search(q);
return new Response(JSON.stringify(data), {
headers: {
'content-type': 'application/json',
'cache-control': isPopular ? 's-maxage=60, stale-while-revalidate=300' : 'no-store',
},
});
}
Also handy: next: { revalidate: 60 }
in fetch, or export const dynamic = 'force-static' | 'force-dynamic'
in the segment.
7) generateStaticParams with partial prebuild
Pre-render hot routes and keep the rest dynamic to avoid massive builds.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const hot = await getTopBlogSlugs(); // top 100
return hot.map(slug => ({ slug }));
}
export const dynamicParams = true; // non-listed slugs are rendered on-demand
export const revalidate = 60; // and revalidated periodically
8) Locale-aware routing without changing URLs
Use middleware to detect locale and set a header/cookie; use a route group to wrap a locale provider, but keep clean URLs.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(req: NextRequest) {
const url = req.nextUrl
const locale = req.cookies.get('locale')?.value || req.headers.get('accept-language')?.split(',')[0] || 'en'
const res = NextResponse.next()
res.headers.set('x-locale', locale)
return res
}
// app/(locale)/layout.tsx
export default function LocaleLayout({ children }: { children: React.ReactNode }) {
// Read header via server only APIs if needed (e.g., headers())
return <html lang="en"><body>{children}</body></html>;
}
9) Slot-only pages: replace children entirely
You can omit children
and render only parallel slots for shell UIs.
// app/layout.tsx
export default function RootLayout({ nav, content }: { nav: React.ReactNode; content: React.ReactNode }) {
return (
<html lang="en">
<body>
<aside>{nav}</aside>
<main>{content}</main>
</body>
</html>
);
}
10) Robust notFound + redirect control flow
Use notFound()
and redirect()
for clear server-driven control, instead of leaking 404/302 into client code.
// app/orgs/[id]/page.tsx
import { redirect, notFound } from 'next/navigation';
export default async function OrgPage({ params }: { params: { id: string }}) {
const org = await getOrg(params.id)
if (!org) return notFound()
if (!org.active) redirect(`/orgs/${params.id}/billing`)
return <h1>{org.name}</h1>
}
Testing your tree quickly
- Use
npx next build && npx next analyze
or the built-in route tree in dev to spot unexpected boundaries. - Add tiny
loading.tsx
anderror.tsx
files to visualize segment behavior. - Prefer server-driven control (
notFound/redirect
) to avoid double renders.
If you want, I can turn any pattern above into a tiny starter repo with both the canonical route and the intercepted/modal variant working side by side.