Next.js App Router Layout — Segments, Parallel Routes, Intercepts
TL;DR
Next.js 13+’s App Router rebuilt routing around the file system + React Server Components. Key concepts: segments (folders = URL segments), layouts (persistent wrappers per segment), loading/error/not-found UIs per segment (Suspense + ErrorBoundary scaffolded automatically), route groups (organize without affecting URLs), parallel routes (@slot for sidebar-like simultaneous routes), and intercepting routes ((.)/(..) to show a modal on top of the previous page). A senior should be able to design a layout for “modal-based detail view” or “dashboard with three independent panels” using these primitives.
Interview Q&A
Q: Basic App Router layout.
A:
app/
├── layout.tsx # root layout (HTML shell)
├── page.tsx # /
├── loading.tsx # / shown while page.tsx suspends
├── error.tsx # / catches errors
├── not-found.tsx
├── dashboard/
│ ├── layout.tsx # /dashboard layout (persistent)
│ ├── page.tsx # /dashboard
│ └── settings/
│ └── page.tsx # /dashboard/settings
└── posts/
└── [id]/
└── page.tsx # /posts/123 (dynamic segment)
page.tsx= the route’s leaf (the actual page content).layout.tsx= persistent wrapper; doesn’t re-render on child navigation.loading.tsx/error.tsx/not-found.tsx= auto-Suspense / ErrorBoundary / 404 boundary per segment.
Q: What’s a layout, and why “persistent”?
A: A layout.tsx wraps its segment + descendants. When the user navigates within the layout’s scope, the layout doesn’t unmount — only the page changes.
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="dashboard">
<Sidebar />
<main>{children}</main>
</div>
);
}
Navigating /dashboard → /dashboard/settings keeps Sidebar mounted; only main content swaps. Faster nav, preserved state (scroll, form input).
Compare to Pages Router (_app.tsx): one global wrapper, less granular.
Q: Route groups — (group)/.
A: A folder in parentheses doesn’t affect the URL — used to share a layout across routes without nesting them in a URL segment.
app/
├── (marketing)/
│ ├── layout.tsx # marketing layout
│ ├── page.tsx # /
│ ├── about/page.tsx # /about
│ └── pricing/page.tsx # /pricing
└── (app)/
├── layout.tsx # app layout (with sidebar, auth)
├── dashboard/page.tsx # /dashboard
└── settings/page.tsx # /settings
URLs: /, /about, /pricing, /dashboard, /settings. Two distinct layouts apply; no URL pollution.
Common pattern: (public) vs (auth) groups to separate logged-out and logged-in experiences.
Q: Parallel routes — @slot/.
A: Renders multiple routes simultaneously in one layout. Each slot is a named “@” folder; the layout receives them as props.
app/
└── dashboard/
├── layout.tsx
├── @analytics/
│ └── page.tsx
├── @team/
│ └── page.tsx
└── page.tsx # default content
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
team: React.ReactNode;
}) {
return (
<div className="grid grid-cols-3">
<main>{children}</main>
<aside>{analytics}</aside>
<aside>{team}</aside>
</div>
);
}
Use cases: dashboards with independently-loaded panels, conditional rendering (auth status changes which slot renders), simultaneous error/loading states per slot.
Each slot gets its own loading.tsx/error.tsx. Powerful but easy to over-use; reach for it when slots are genuinely independent.
Q: Intercepting routes — (.) (..) (...).
A: Render a route as modal on top of the previous one instead of a full page transition. The URL changes (deep-linkable), but visually it’s an overlay.
app/
├── feed/
│ ├── page.tsx # /feed
│ └── @modal/
│ └── (..)photo/
│ └── [id]/
│ └── page.tsx # intercepted /photo/:id
└── photo/
└── [id]/
└── page.tsx # /photo/:id (full page on direct visit)
The @modal slot in feed/ intercepts /photo/:id when navigated from /feed — shows as modal. Direct visit to /photo/:id renders the full page.
This is exactly Instagram’s “click photo from feed → modal; share link → full page” pattern. Hard to build by hand, trivial with intercepting routes.
Conventions:
(.)route— same level.(..)route— one level up.(...)route— root-level.
Q: Server Components default — what changes?
A: Every page.tsx/layout.tsx/loading.tsx is a Server Component by default. They run on the server, can be async, fetch data directly, ship zero JS for themselves.
// app/posts/[id]/page.tsx (server component)
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await db.posts.findUnique({ where: { id: params.id } });
return <article>{post.body}</article>;
}
To make something client-side (with hooks, event handlers), mark with "use client" at the top. See ../05_react/server_components.md.
Q: Data fetching — fetch with caching.
A: App Router extends fetch with caching directives:
// Cache forever (default in server components when called from a Server Component)
const data = await fetch("/api/x");
// Don't cache — always fresh
const data = await fetch("/api/x", { cache: "no-store" });
// Revalidate every 60s (ISR-style)
const data = await fetch("/api/x", { next: { revalidate: 60 } });
// Tag for on-demand revalidation
const data = await fetch("/api/x", { next: { tags: ["users"] } });
// Later: revalidateTag("users") to invalidate
Caching behavior changed across Next versions (15 made caching opt-in). Verify your version’s default; pin behavior explicitly with cache: / next.revalidate.
Q: Where do API routes live?
A: app/api/.../route.ts — file-based routing for HTTP handlers:
// app/api/users/route.ts
export async function GET(req: Request) {
const users = await db.users.findMany();
return Response.json(users);
}
export async function POST(req: Request) {
const body = await req.json();
const user = await db.users.create({ data: body });
return Response.json(user, { status: 201 });
}
// app/api/users/[id]/route.ts
export async function GET(req: Request, { params }: { params: { id: string } }) {
const user = await db.users.findUnique({ where: { id: params.id } });
if (!user) return new Response("Not found", { status: 404 });
return Response.json(user);
}
For monorepo-style backend separation, you might run a separate API service instead — but for simpler apps, app/api/ keeps the BFF colocated.
Q: Metadata API for SEO.
A: Export metadata from a page.tsx/layout.tsx:
// app/posts/[id]/page.tsx
import { Metadata } from "next";
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await db.posts.findUnique({ where: { id: params.id } });
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [{ url: post.coverImage }] },
};
}
export default async function PostPage(...) { ... }
Replaces the old <Head> hack. Type-safe, server-rendered, supports OG/Twitter cards/etc.
Q: A real-world layout pattern.
A: SaaS dashboard with auth, marketing, app:
app/
├── layout.tsx # global providers (theme, fonts)
├── (marketing)/
│ ├── layout.tsx # marketing nav + footer
│ ├── page.tsx # /
│ ├── pricing/page.tsx
│ └── blog/[slug]/page.tsx
├── (auth)/
│ ├── login/page.tsx # /login
│ ├── signup/page.tsx
│ └── layout.tsx # centered auth card layout
└── (app)/
├── layout.tsx # sidebar, top bar, auth check
├── dashboard/
│ ├── @overview/page.tsx
│ ├── @recent-activity/page.tsx
│ ├── @notifications/page.tsx
│ ├── layout.tsx # 3-column grid
│ └── page.tsx # default
└── settings/
├── account/page.tsx
├── billing/page.tsx
├── @modal/
│ └── (..)settings/cancel/page.tsx # cancel confirmation modal
└── layout.tsx # tabs for sub-pages
Each section has its own layout; intercepting routes for modals; parallel routes for the dashboard panels.
Gotchas / edge cases
- Layouts don’t re-render on navigation within scope — including their data fetches. Fetch data in pages, not layouts, if it needs to refresh per route.
use clientpropagates down — once a tree starts client, deeper components are also client (until you explicitly pass a Server Component aschildren).- Parallel routes need a
default.tsxfor unmatched slots (otherwise the slot shows 404). - Intercepting routes don’t work in dev mode for direct URL entry — both renderings exist; only soft nav triggers the intercept.
generateStaticParamsfor static generation of dynamic segments at build time.force-dynamic/force-staticroute segment config — explicit override of caching behavior.- Middleware (
middleware.tsat app root) runs on every request — auth, redirects, headers.
What a senior is expected to say
- “App Router rebuilt routing around RSC + file system. Folders = segments;
layout.tsxis persistent;loading.tsx/error.tsx/not-found.tsxauto-scaffold Suspense + ErrorBoundary.” - “Route groups (
(name)) organize without URL impact —(public)vs(auth)is a common split.” - “Parallel routes (
@slot) for dashboards with independent panels; intercepting routes ((.)/(..)) for modal-over-page patterns like Instagram.” - “Pages/layouts are server components by default —
async, direct DB access, zero client JS.'use client'opts into interactivity.” - “Caching for
fetchis opt-in / explicit in Next 15 — pin behavior withcache:ornext.revalidaterather than relying on defaults.”
Cross-references
- React Server Components: ../05_react/server_components.md
- Rendering modes (SSR/SSG/ISR): ../19_rendering_modes/
- React 19 + Server Actions: ../05_react/react_19.md
- Vue equivalent (Nuxt): ../06_vue/11_nuxt.md
Further reading
- Next.js App Router docs: https://nextjs.org/docs/app
- Parallel Routes: https://nextjs.org/docs/app/building-your-application/routing/parallel-routes
- Intercepting Routes: https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes
- Caching: https://nextjs.org/docs/app/building-your-application/caching