Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
These skill recipes are designed for Claude Code but also work with other AI coding agents that support skill/instruction files.
The complete SKILL.md content you can copy into .claude/skills/nextjs-routing/SKILL.md:
---
name: nextjs-routing-deep-dive
description: "Advanced App Router routing patterns, parallel routes, intercepting routes, and layouts. Use when asked to: routing help, parallel routes, intercepting routes, modal pattern, route groups, dynamic routes, middleware, catch-all routes."
allowed-tools: "Read, Write, Edit, Glob, Grep, Bash(npm:*), Bash(npx:*), Agent"
---
# Next.js Routing Deep Dive
You are a Next.js App Router routing expert. Provide authoritative guidance on every routing pattern available in the App Router.
## File Conventions
| File | Purpose | Renders |
|------|---------|---------|
| layout.tsx | Shared UI wrapper, preserves state on navigation | Wraps children |
| template.tsx | Like layout but remounts on every navigation | Wraps children |
| page.tsx | Unique route UI, makes route publicly accessible | Route content |
| loading.tsx | Instant loading UI via Suspense | While page loads |
| error.tsx | Error boundary for the segment | On error |
| not-found.tsx | 404 UI for the segment | On notFound() |
| default.tsx | Fallback for parallel route slots when no match | Slot fallback |
| route.ts | API endpoint (GET, POST, etc.) | JSON or Response |
| global-error.tsx | Root-level error boundary | On root error |
## Route Types
### Static Routesapp/about/page.tsx -> /about app/blog/page.tsx -> /blog app/dashboard/settings/page.tsx -> /dashboard/settings
### Dynamic Routes
app/blog/[slug]/page.tsx -> /blog/my-post (single param) app/shop/[...slug]/page.tsx -> /shop/a/b/c (catch-all, required) app/docs/[[...slug]]/page.tsx -> /docs or /docs/a/b (optional catch-all)
### Route Groups
app/(marketing)/about/page.tsx -> /about (no /marketing in URL) app/(shop)/cart/page.tsx -> /cart app/(auth)/login/layout.tsx -> separate layout for auth pages
Route groups with parentheses do NOT affect the URL. Use them to:
- Organize routes by concern without changing URL structure
- Apply different layouts to different route groups
- Split the root layout into multiple root layouts
### Parallel Routes
app/@modal/default.tsx app/@modal/(.)photo/[id]/page.tsx app/layout.tsx -> receives { children, modal } props app/photo/[id]/page.tsx -> hard-navigation target
Rules for parallel routes:
- Defined with @folder convention
- Each slot is passed as a prop to the parent layout
- Slots render independently, can have their own loading/error states
- ALWAYS create a default.tsx for each slot (prevents 404 on hard refresh)
- Slots do NOT affect URL structure
### Intercepting Routes
(.)folder -> intercepts same level (..)folder -> intercepts one level up (..)(..)folder -> intercepts two levels up (...)folder -> intercepts from root
Common pattern - modal interception:
app/@modal/(.)photo/[id]/page.tsx -> soft nav shows modal app/photo/[id]/page.tsx -> hard nav shows full page
## Decision Tree: Choosing a Routing Pattern
1. **Do you need a shared wrapper that persists across navigations?**
- Yes -> Use layout.tsx
- Yes, but it must remount -> Use template.tsx
2. **Do you need multiple independent sections on the same page?**
- Yes -> Use parallel routes (@slot)
- Each section loads independently -> Add loading.tsx per slot
3. **Do you need a modal that opens on soft navigation but shows a full page on hard navigation?**
- Yes -> Use intercepting routes + parallel routes
- Create @modal slot with (.) intercepting route
4. **Do you need different layouts for different sections?**
- Yes -> Use route groups: (marketing), (dashboard), (auth)
5. **Do you need dynamic segments in the URL?**
- Single param -> [param]
- Multiple segments -> [...params] or [[...params]]
6. **Do you need to protect routes?**
- Simple redirects -> middleware.ts
- Complex auth logic -> layout.tsx with auth check
- API protection -> route.ts with auth validation
## Middleware
```typescript
// middleware.ts (project root or src/)
import \{ NextResponse \} from "next/server";
import type \{ NextRequest \} from "next/server";
export function middleware(request: NextRequest) \{
// Check auth
const token = request.cookies.get("session");
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) \{
return NextResponse.redirect(new URL("/login", request.url));
\}
return NextResponse.next();
\}
export const config = \{
// Match all routes except static files and API
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
\};
Middleware rules:
app/
(shop)/
layout.tsx # Shop layout with nav
page.tsx # Home
products/
page.tsx # Product listing
[slug]/page.tsx # Product detail
cart/page.tsx
checkout/page.tsx
(account)/
layout.tsx # Account layout
profile/page.tsx
orders/page.tsx
@modal/
default.tsx
(.)products/[slug]/page.tsx # Product quick view modal
app/
(auth)/
login/page.tsx
register/page.tsx
(dashboard)/
layout.tsx
page.tsx
@sidebar/
default.tsx
page.tsx
@main/
default.tsx
[section]/page.tsx
settings/
layout.tsx
page.tsx
profile/page.tsx
billing/page.tsx
## Working Example
### Example 1: User asks "How do I create a photo modal pattern?"
**User prompt:** "I want clicking a photo thumbnail to open a modal, but sharing the URL should show the full photo page."
**Skill-guided response would produce this file structure:**
app/ layout.tsx @modal/ default.tsx (.)photo/[id]/page.tsx # Modal version photo/[id]/page.tsx # Full page version gallery/page.tsx # Grid of thumbnails
```tsx
// app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
{modal}
</body>
</html>
);
}
// app/@modal/default.tsx
export default function Default() {
return null; // No modal by default
}// app/@modal/(.)photo/[id]/page.tsx
import { Modal } from "@/components/Modal";
export default async function PhotoModal({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<Modal>
<img src={photo.url} alt={photo.alt} />
<p>{photo.description}</p>
</Modal>
);
}// app/photo/[id]/page.tsx
export default async function PhotoPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const photo = await getPhoto(id);
return (
<main className="flex min-h-screen items-center justify-center">
<img src={photo.url} alt={photo.alt} className="max-w-4xl" />
<h1>{photo.title}</h1>
<p>{photo.description}</p>
</main>
);
}Skill-guided response would recommend:
(marketing) for public pages, (app) for authenticated dashboard, (auth) for login/signupThis skill provides Claude with:
Extend this skill by adding:
mkdir -p .claude/skills/nextjs-routing
# Paste the Recipe content into .claude/skills/nextjs-routing/SKILL.md(.) means same level in the filesystem, not URL level. This trips up many developers.await params in page and layout components.| Approach | When to Use |
|---|---|
| Pages Router | Legacy Next.js projects not yet migrated |
| React Router v7 | Non-Next.js React apps, Remix migration |
| TanStack Router | Type-safe routing with search param validation |
| Expo Router | React Native + web file-based routing |
layout.tsx wraps children and preserves state across navigations (does not remount)template.tsx wraps children but remounts on every navigation (fresh state each time)template.tsx when you need animations or state resets on route change@folder (e.g., @modal, @sidebar)layout.tsxloading.tsx and error.tsxdefault.tsx for every parallel route slotdefault.tsx typically returns null when no content should be shown@modal) with an intercepting route like (.)photo/[id]/page.tsxphoto/[id]/page.tsx(.) prefix means "intercept at the same filesystem level"(.)folder -- intercepts at the same level(..)folder -- intercepts one level up(..)(..)folder -- intercepts two levels up(...)folder -- intercepts from the app root(marketing) or (auth) do NOT appear in the URLapp/(marketing)/about/page.tsx maps to /about, not /marketing/about[...slug] is a required catch-all -- it matches /shop/a/b/c but NOT /shop[[...slug]] is an optional catch-all -- it matches both /docs and /docs/a/bexport default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// ...
}params is a Promise and must be awaitedpage.tsx instead of route.tsdefault.tsx in parallel routesredirect() in Client Components instead of middleware(marketing) for public pages, (app) for the dashboard, (auth) for login/signuplayout.tsxexport default function RootLayout({
children,
modal,
}: {
children: React.ReactNode;
modal: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}{modal}</body>
</html>
);
}@slot folder name becomes a prop typed as React.ReactNodeReviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥