1. App Router
The App Router (stable since Next.js 13.4) is the default and recommended router. The Pages Router is in maintenance mode.
1. File-System Routing
A folder becomes a route. A page.tsx inside it makes that route publicly accessible.
app/
├── layout.tsx → root layout (required)
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/:slug
└── shop/
└── [...categories]/
└── page.tsx → /shop/a/b/cDynamic Segment Types
| Pattern | Matches | params |
|---|---|---|
[id] | /posts/1 | { id: "1" } |
[...slug] | /shop/a/b | { slug: ["a","b"] } |
[[...slug]] | /shop and /shop/a/b | { slug: undefined | ["a","b"] } |
2. Special Files
| File | Purpose |
|---|---|
page.tsx | The route UI — makes the route accessible |
layout.tsx | Shared shell that wraps children; preserves state on navigation |
template.tsx | Like layout, but remounts on every navigation |
loading.tsx | Suspense fallback for the segment |
error.tsx | Error boundary for the segment (must be a Client Component) |
global-error.tsx | Catches errors in the root layout |
not-found.tsx | UI for notFound() and unmatched routes |
route.ts | API endpoint (cannot coexist with page.tsx in the same folder) |
default.tsx | Fallback for parallel routes — required in Next 16 |
Interview Point
layout vs template: a layout does not re-render when you navigate between its children, so scroll position and state survive. A template creates a new instance every time — use it when you need an enter animation or per-route state reset.
3. Root Layout
Required. It must render <html> and <body>.
// app/layout.tsx
export const metadata = {
title: "DDocs",
description: "Developer learning notes",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Navbar />
{children}
<Footer />
</body>
</html>
);
}4. Server Components vs Client Components
The single most important App Router concept. Everything in app/ is a Server Component by default.
Server Component
// No directive needed — this is the default
async function Users() {
const users = await db.user.findMany(); // direct DB access
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}- Runs on the server only
- Ships zero JavaScript to the browser
- Can
awaitdirectly — nouseEffect, no loading state - Can read secrets, env vars, the filesystem, the database
- Cannot use
useState,useEffect, event handlers or browser APIs
Client Component
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}- Prerendered on the server to HTML, then hydrated in the browser
- Its JavaScript ships to the client
- Can use hooks, event handlers,
window,localStorage
Comparison
| Server Component | Client Component | |
|---|---|---|
| Directive | none (default) | "use client" |
| JS shipped | None | Yes |
| Hooks | No | Yes |
| Event handlers | No | Yes |
| Direct DB / secrets | Yes | Never |
async component | Yes | No |
| Browser APIs | No | Yes |
When You Need "use client"
useState,useEffect,useReducer,useContextonClick,onChange, any event handlerwindow,document,localStorage- Browser-only libraries (most chart, map and animation libraries)
Interview Point
"use client" marks a boundary, not a file. Every component imported by a Client Component also becomes client-side. Push the directive as far down the tree as possible — one "use client" in your root layout ships your whole app to the browser.
5. Composition Pattern
A Client Component cannot import a Server Component. But it can render one passed as children.
// ❌ Wrong — ServerThing becomes a Client Component
"use client";
import ServerThing from "./ServerThing";
// ✅ Right — pass it in as a slot
// app/page.tsx (Server Component)
import ClientWrapper from "./ClientWrapper";
import ServerThing from "./ServerThing";
export default function Page() {
return (
<ClientWrapper>
<ServerThing /> {/* stays a Server Component */}
</ClientWrapper>
);
}This is the most common "how do I mix them" interview answer.
6. Async params and searchParams (Next.js 16 Breaking Change)
In Next.js 15+ params and searchParams are promises. You must await them.
// Next.js 16
export default async function Page({
params,
searchParams,
}: {
params: Promise<{ slug: string }>;
searchParams: Promise<{ page?: string }>;
}) {
const { slug } = await params;
const { page } = await searchParams;
return <h1>{slug} — page {page ?? 1}</h1>;
}// Next.js 14 and earlier — sync, now removed
export default function Page({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>;
}Same change applies to cookies(), headers() and draftMode() — all async now:
const cookieStore = await cookies();
const theme = cookieStore.get("theme")?.value;Interview Point
The reason is Partial Prerendering. Making them promises lets Next.js prerender the static shell without knowing the request, and resolve the request-specific parts later.
7. Navigation
Link
import Link from "next/link";
<Link href="/blog">Blog</Link>
<Link href={`/blog/${slug}`} prefetch={false}>Post</Link><Link> automatically prefetches routes when they enter the viewport. Next 16 added layout deduplication (a shared layout is downloaded once for 50 links, not 50 times) and incremental prefetching.
Programmatic
"use client";
import { useRouter } from "next/navigation"; // NOT next/router
const router = useRouter();
router.push("/dashboard");
router.replace("/login");
router.back();
router.refresh(); // re-fetch server data, keep client stateServer-Side Redirect
import { redirect, notFound } from "next/navigation";
export default async function Page({ params }) {
const { id } = await params;
const post = await getPost(id);
if (!post) notFound();
if (post.archived) redirect("/archive");
return <Article post={post} />;
}Client Navigation Hooks
"use client";
import { usePathname, useSearchParams, useParams } from "next/navigation";
const pathname = usePathname(); // "/blog/hello"
const searchParams = useSearchParams(); // read-only URLSearchParams
const params = useParams(); // { slug: "hello" }Interview Point
next/router is the Pages Router. next/navigation is the App Router. Importing the wrong one is one of the most common migration bugs.
8. Loading UI and Streaming
// app/blog/loading.tsx
export default function Loading() {
return <PostSkeleton />;
}Next.js automatically wraps page.tsx in a <Suspense> boundary with this fallback. The shared layout renders instantly while the page streams in.
Manual Suspense for Finer Control
export default function Page() {
return (
<>
<Header /> {/* instant */}
<Suspense fallback={<Skeleton />}>
<SlowFeed /> {/* streams in */}
</Suspense>
</>
);
}Streaming means the server sends HTML in chunks as it becomes ready, instead of blocking the whole page on the slowest query.
9. Error Handling
// app/blog/error.tsx
"use client"; // error boundaries must be Client Components
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}error.tsxcatches errors in that segment and below — but not in its own layoutglobal-error.tsxcatches errors in the root layout; it must render its own<html>and<body>not-found.tsxrenders fornotFound()
10. Route Groups and Private Folders
Route Group — (folder)
Organises files without adding a URL segment.
app/
├── (marketing)/
│ ├── layout.tsx → marketing layout
│ ├── page.tsx → /
│ └── about/page.tsx → /about
└── (shop)/
├── layout.tsx → different layout
└── cart/page.tsx → /cartTwo completely different layouts, no (marketing) in the URL.
Private Folder — _folder
Opts a folder out of routing. app/_components/Button.tsx is never a route.
11. Parallel and Intercepting Routes
Parallel Routes — @folder
Render several pages in the same layout simultaneously.
app/dashboard/
├── layout.tsx
├── @analytics/page.tsx
├── @team/page.tsx
└── @analytics/default.tsx ← required in Next 16export default function Layout({ children, analytics, team }) {
return (
<>
{children}
{analytics}
{team}
</>
);
}Next 16 breaking change: every parallel slot now needs an explicit default.tsx, or the build fails. Return null or call notFound() for the old behaviour.
Intercepting Routes — (.)folder
Show a route in a modal when navigated to from within the app, but as a full page on direct visit or refresh. This is how Instagram-style photo modals work.
| Pattern | Matches |
|---|---|
(.)photo | same level |
(..)photo | one level up |
(...)photo | from the app root |
12. Metadata and SEO
Static
export const metadata = {
title: "Blog | DDocs",
description: "Developer notes",
openGraph: { title: "Blog", images: ["/og.png"] },
};Dynamic
export async function generateMetadata({ params }) {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [post.coverImage] },
};
}Title Template
// app/layout.tsx
export const metadata = {
title: { default: "DDocs", template: "%s | DDocs" },
};Child pages setting title: "Blog" render as Blog | DDocs.
File Conventions
Drop these in app/ and Next.js wires them up: favicon.ico, icon.png, apple-icon.png, opengraph-image.png, robots.ts, sitemap.ts.
// app/sitemap.ts
export default async function sitemap() {
const posts = await getPosts();
return [
{ url: "https://ddocs.dev", lastModified: new Date() },
...posts.map((p) => ({ url: `https://ddocs.dev/blog/${p.slug}` })),
];
}13. Built-in Optimisations
next/image
import Image from "next/image";
<Image src="/hero.png" alt="Hero" width={800} height={400} priority />
<Image src={url} alt="" fill sizes="(max-width: 768px) 100vw, 50vw" />Gives you automatic WebP/AVIF conversion, responsive srcset, lazy loading, and a reserved layout box that prevents cumulative layout shift.
Next 16 defaults changed: minimumCacheTTL is now 4 hours (was 60s), qualities defaults to [75], redirects are capped at 3, and local IP optimisation is blocked unless you opt in. Use images.remotePatterns — images.domains is deprecated.
next/font
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"], display: "swap" });
<body className={inter.className}>Fonts are downloaded at build time and self-hosted — no request to Google, and zero layout shift.
next/script
<Script src="https://analytics.example.com/s.js" strategy="lazyOnload" />Strategies: beforeInteractive, afterInteractive (default), lazyOnload, worker.
14. Proxy (formerly Middleware)
Next.js 16 renamed middleware.ts to proxy.ts and moved it to the Node.js runtime.
// proxy.ts (project root)
import { NextResponse, type NextRequest } from "next/server";
export default function proxy(request: NextRequest) {
const token = request.cookies.get("token");
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/admin/:path*"],
};Runs before the request is completed. Use it for auth redirects, A/B tests, geo-routing, bot blocking and header rewriting.
middleware.ts still works for Edge runtime cases but is deprecated and will be removed.
Interview Point
Keep proxy logic light — it runs on every matched request. Do a cheap cookie/JWT presence check there and the real database session lookup in the page or Route Handler.
15. Common Migration Bugs
| Symptom | Cause |
|---|---|
useRouter is not a function | Imported from next/router instead of next/navigation |
params.slug is undefined | Forgot await params in Next 15/16 |
useState is not defined | Missing "use client" |
| Whole app is client-side | "use client" sits too high in the tree |
| Build fails on parallel routes | Missing default.tsx (Next 16) |
window is not defined | Browser API used in a Server Component |
| Hydration mismatch | Date.now(), Math.random(), or localStorage during render |