tool / 67

Next.js Deep Dive

Routing, rendering, data, mutations, caching, components — every common pattern with a working example.

All local
41/41
Routing9
App Router

File-system routing where folders define URL segments and special files (page, layout, loading, error) define UI.

app/
  layout.tsx        // root layout, wraps everything
  page.tsx          // /
  blog/
    page.tsx        // /blog
    [slug]/
      page.tsx      // /blog/:slug
Dynamic Segments

Square brackets in folder names create dynamic segments. Params arrive as a Promise.

// app/blog/[slug]/page.tsx
export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  return <h1>{slug}</h1>;
}
Catch-all & Optional

[...slug] matches any number of segments. [[...slug]] also matches the parent route.

app/docs/[...slug]/page.tsx       // /docs/a, /docs/a/b, /docs/a/b/c
app/shop/[[...path]]/page.tsx     // matches /shop and /shop/anything
Route Groups

Folders wrapped in parentheses don't appear in the URL. Useful for organizing layouts.

app/
  (marketing)/
    layout.tsx     // marketing layout
    page.tsx       // /
    pricing/page.tsx  // /pricing
  (app)/
    layout.tsx     // app shell with auth
    dashboard/page.tsx  // /dashboard
Parallel Routes

@slot folders render multiple pages into the same layout simultaneously.

app/
  layout.tsx       // receives { children, analytics, team }
  @analytics/page.tsx
  @team/page.tsx
  page.tsx         // children
Intercepting Routes

(.) intercepts a route from the same level so you can show it as a modal while preserving the URL.

app/
  feed/page.tsx
  feed/(.)photo/[id]/page.tsx   // shown as modal over /feed
  photo/[id]/page.tsx            // standalone page
Link Component

<Link> from next/link does client-side navigation with automatic prefetching.

import Link from "next/link";

<Link href="/blog/hello" prefetch>Hello</Link>
useRouter (Client)

Programmatic navigation in client components.

"use client";
import { useRouter } from "next/navigation";

const router = useRouter();
router.push("/dashboard");
router.refresh();   // re-fetch server components on this route
redirect() / notFound()

Throw to redirect or 404 from server components, server actions or route handlers.

import { redirect, notFound } from "next/navigation";

if (!user) notFound();
if (!user.verified) redirect("/verify");
Rendering7
Server Components (default)

Components are server components by default. They run on the server, can fetch data directly, and have zero JS shipped.

// app/users/page.tsx — no "use client", runs on the server
async function UsersPage() {
  const users = await db.user.findMany();
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
Client Components ("use client")

Add 'use client' at the top of a file to mark it (and everything it imports) as client-side. Required for hooks, event handlers, browser APIs.

"use client";
import { useState } from "react";

export function Counter() {
  const [n, setN] = useState(0);
  return <button onClick={() => setN(n + 1)}>{n}</button>;
}
Static Rendering

Pages render at build time when they have no dynamic dependencies. Cached at the edge.

// no dynamic APIs used → static at build time
export default function About() {
  return <h1>About us</h1>;
}
Dynamic Rendering

Triggered automatically when you use cookies(), headers(), searchParams or fetch with no-store.

import { cookies } from "next/headers";

export default async function Page() {
  const c = await cookies();
  const theme = c.get("theme");
  return <div data-theme={theme?.value}>...</div>;
}
Streaming with Suspense

Wrap slow parts in <Suspense>. The shell streams immediately, slow data fills in as it resolves.

import { Suspense } from "react";

<Suspense fallback={<Skeleton />}>
  <SlowComponent />
</Suspense>
loading.tsx

Special file that wraps a route in <Suspense> automatically. Renders instantly while data loads.

// app/blog/loading.tsx
export default function Loading() {
  return <div>Loading…</div>;
}
error.tsx

Catches errors in the segment. Must be a client component.

"use client";
export default function Error({ error, reset }: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div>
      <p>{error.message}</p>
      <button onClick={reset}>Retry</button>
    </div>
  );
}
Data Fetching4
Fetching in Server Components

Use async/await directly. fetch() is automatically deduplicated and cached.

// app/posts/page.tsx
async function getPosts() {
  const res = await fetch("https://api.example.com/posts");
  return res.json();
}

export default async function Page() {
  const posts = await getPosts();
  return <List posts={posts} />;
}
Fetch Cache Options

Control caching per-request via fetch options.

// always fresh
fetch(url, { cache: "no-store" });

// revalidate every 60 seconds
fetch(url, { next: { revalidate: 60 } });

// tag for on-demand revalidation
fetch(url, { next: { tags: ["posts"] } });
Parallel Data Fetching

Use Promise.all to start requests in parallel instead of waterfalling.

const [user, posts] = await Promise.all([
  getUser(id),
  getPosts(id),
]);
Preload Pattern

Start a fetch as a side-effect so child components can await it later without a waterfall.

function preload(id: string) {
  void getUser(id);
}
export default async function Page({ id }) {
  preload(id);
  // ... other work
  const user = await getUser(id);
  return <Profile user={user} />;
}
Mutations3
Server Actions

Functions marked 'use server' run on the server but can be called from client components like normal functions.

// app/actions.ts
"use server";
export async function createPost(formData: FormData) {
  const title = formData.get("title");
  await db.post.create({ data: { title } });
}

// in a client form
<form action={createPost}>
  <input name="title" />
  <button>Save</button>
</form>
useActionState

React 19 hook for tracking form action state, errors, and pending status.

"use client";
import { useActionState } from "react";

const [state, action, pending] = useActionState(createPost, { error: null });

<form action={action}>
  <input name="title" />
  <button disabled={pending}>Save</button>
  {state.error && <p>{state.error}</p>}
</form>
revalidatePath / revalidateTag

Invalidate cached data after a mutation so the next request fetches fresh.

import { revalidatePath, revalidateTag } from "next/cache";

revalidatePath("/blog");                  // invalidate a route
revalidateTag("posts", "max");            // invalidate a tag
Caching3
Data Cache

fetch() responses are cached on the server, deduplicated per render. Persistent across requests until invalidated.

// cached forever, until revalidated
fetch("/api/posts");

// time-based revalidation
fetch("/api/posts", { next: { revalidate: 3600 } });
Router Cache

Client-side cache of RSC payloads for visited routes. Makes back/forward instant.

// invalidate the router cache
import { useRouter } from "next/navigation";
router.refresh();
Route Segment Config

Export config from a page or layout to control caching behavior at the route level.

export const dynamic = "force-dynamic";
export const revalidate = 60;
export const fetchCache = "default-no-store";
export const runtime = "edge";
Components4
Image Component

next/image automatically optimizes, lazy-loads and serves responsive images.

import Image from "next/image";

<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority
/>
Font Optimization

Self-host any Google or local font with zero layout shift.

import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });

export default function Layout({ children }) {
  return <html className={inter.className}>{children}</html>;
}
Script Component

Load third-party scripts with explicit loading strategy.

import Script from "next/script";

<Script src="https://analytics.example.com" strategy="afterInteractive" />
// strategies: beforeInteractive | afterInteractive | lazyOnload | worker
Metadata API

Export a metadata object or generateMetadata function from any page or layout.

export const metadata = {
  title: "DEVBOX",
  description: "Tools for developers",
  openGraph: {
    images: ["/og.png"],
  },
};

// or dynamic
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  return { title: post.title };
}
Conventions3
Special File Conventions

Specific filenames have special meaning in the App Router.

page.tsx       // route UI
layout.tsx     // wraps children, persists across navigations
loading.tsx    // suspense boundary
error.tsx      // error boundary
not-found.tsx  // 404 UI
template.tsx   // like layout but re-mounts on navigation
default.tsx    // fallback for parallel routes
route.ts       // API route handler
middleware.ts  // intercepts requests (deprecated → use proxy.ts)
PageProps & LayoutProps Helpers

Globally available types — generated during dev/build — that infer params and named slots.

export default async function Page(props: PageProps<"/blog/[slug]">) {
  const { slug } = await props.params;
  return <h1>{slug}</h1>;
}
Nesting Layouts

Layouts in folder hierarchy automatically nest. State and rendering persist on navigation.

app/layout.tsx              // wraps everything
app/blog/layout.tsx         // wraps blog routes only
app/blog/[slug]/page.tsx    // child of blog layout
API3
Route Handlers

Export named functions per HTTP method from a route.ts file.

// app/api/users/route.ts
export async function GET(request: Request) {
  const users = await db.user.findMany();
  return Response.json(users);
}

export async function POST(request: Request) {
  const body = await request.json();
  // ...
  return Response.json({ ok: true }, { status: 201 });
}
Dynamic Route Handlers

params arrive as a Promise (since Next 15+).

// app/api/users/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const user = await db.user.findUnique({ where: { id } });
  return Response.json(user);
}
Cookies & Headers (async)

Server APIs to read request cookies and headers.

import { cookies, headers } from "next/headers";

const c = await cookies();
const token = c.get("token");

const h = await headers();
const ip = h.get("x-forwarded-for");
Performance3
Partial Prerendering

Static shell + streamed dynamic islands in the same response. Best of both worlds.

// app/layout.tsx
export const experimental_ppr = true;

// the static shell renders instantly
// <Suspense> boundaries stream dynamic content
Edge Runtime

Run code at the edge for global low-latency. Limited to Web APIs (no Node.js APIs).

// route segment config
export const runtime = "edge";

// or for middleware/proxy
export const config = { runtime: "edge" };
Image priority + sizes

Use priority for above-the-fold images and sizes for responsive loading.

<Image
  src="/hero.jpg"
  alt="Hero"
  fill
  priority
  sizes="(max-width: 768px) 100vw, 50vw"
/>
Deployment2
Environment Variables

Vars in .env are server-only by default. Prefix with NEXT_PUBLIC_ to expose to the browser.

# .env
DATABASE_URL=postgres://...   # server only
NEXT_PUBLIC_APP_URL=https://example.com   # available in client

# in code
process.env.DATABASE_URL          // server
process.env.NEXT_PUBLIC_APP_URL   // both
Output Modes

Configure build output: standalone (Docker), export (static HTML), or default (serverful).

// next.config.ts
const config = {
  output: "standalone", // or "export"
};
export default config;