Next.js App Router Reference: Server Components, Caching, Server Actions and Middleware
Next.js App Router (v13+) changes React development fundamentally. Server Components, file-system routing, Server Actions, and the caching model are the four things to understand before everything else clicks.
1. App Router File Conventions
page.tsx, layout.tsx, loading.tsx, error.tsx, route.ts — what each file does
| File | Purpose |
|---|---|
| page.tsx | Route UI — Server Component by default |
| layout.tsx | Shared wrapper — doesn’t re-render on navigation |
| loading.tsx | Suspense fallback — auto-wraps page |
| error.tsx | Error boundary — must be Client Component |
| not-found.tsx | Rendered when notFound() is called |
| route.ts | API endpoint — export GET, POST, etc. |
| middleware.ts | Edge runtime — auth redirects before request |
app/
layout.tsx # root layout (must include html + body)
page.tsx # /
(marketing)/ # route group — no URL impact
about/page.tsx # /about
pricing/page.tsx # /pricing
dashboard/
layout.tsx # shared for /dashboard/*
page.tsx # /dashboard
[userId]/page.tsx # /dashboard/123 (dynamic segment)
api/
users/route.ts # GET /api/users, POST /api/users
users/[id]/route.ts # /api/users/123
2. Server Components vs Client Components
When to use each, the “use client” boundary, and passing Server as children
// Server Component (default):
// - async/await directly, direct DB access
// - no hooks, no event handlers, not sent to browser
import { db } from "@/lib/db";
export default async function UsersPage() {
const users = await db.query("SELECT * FROM users LIMIT 20");
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
// Client Component:
// - needs "use client" directive
// - useState, useEffect, event handlers, browser APIs
"use client";
import { useState } from "react";
export function SearchBar({ onSearch }) {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
onKeyDown={e => e.key === "Enter" && onSearch(query)}
/>
);
}
// Rules:
// Server can import Client: fine
// Client cannot import Server: not allowed
// But Client can receive Server as children prop: fine
"use client";
export function Modal({ children }) {
const [open, setOpen] = useState(false);
return open ? <dialog>{children}</dialog> : null;
}
// <Modal><ServerComponent /></Modal> -- works!
3. Data Fetching and Caching
fetch cache options, revalidate, unstable_cache, revalidatePath and revalidateTag
// Static (cached indefinitely — built at deploy):
const data = await fetch("https://api.example.com/products");
// ISR — revalidate every 60 seconds:
const data = await fetch(url, { next: { revalidate: 60 } });
// Dynamic — always fresh, no cache:
const data = await fetch(url, { cache: "no-store" });
// Force entire page to be dynamic:
export const dynamic = "force-dynamic"; // in page.tsx
// React cache() — deduplicate within one request:
import { cache } from "react";
export const getUser = cache(async (id) => db.getUser(id));
// getUser("123") called twice in same render = one DB query
// unstable_cache — persist across requests:
import { unstable_cache } from "next/cache";
const getProducts = unstable_cache(
async () => db.getProducts(),
["products"], // cache key array
{ revalidate: 300 } // 5 minutes
);
// On-demand revalidation after a mutation:
import { revalidatePath, revalidateTag } from "next/cache";
revalidatePath("/products"); // revalidate specific URL
revalidateTag("products"); // revalidate all tagged fetches
// Tag a fetch: fetch(url, { next: { tags: ["products"] } })
4. Server Actions
Mutations without an API route, progressive enhancement, useActionState
// Server Action — "use server" marks function to run on server:
async function createUser(formData) {
"use server";
const name = formData.get("name");
await db.query("INSERT INTO users (name) VALUES ($1)", [name]);
revalidatePath("/users");
redirect("/users");
}
// Works without JavaScript (progressive enhancement):
export default function CreateUserPage() {
return (
<form action={createUser}>
<input name="name" required />
<button type="submit">Create</button>
</form>
);
}
// With loading + error state (Client Component):
"use client";
import { useActionState } from "react";
export function CreateUserForm() {
const [state, action, isPending] = useActionState(createUser, null);
return (
<form action={action}>
<input name="name" required />
{state?.error && <p className="text-red-500">{state.error}</p>}
<button type="submit" disabled={isPending}>
{isPending ? "Creating..." : "Create"}
</button>
</form>
);
}
5. Route Handlers, Middleware and Metadata
API endpoints, auth middleware redirect, static and dynamic metadata
// Route handler (app/api/users/route.ts):
import { NextRequest, NextResponse } from "next/server";
export async function GET(request) {
const status = request.nextUrl.searchParams.get("status");
return NextResponse.json(await db.getUsers({ status }));
}
export async function POST(request) {
const user = await db.createUser(await request.json());
return NextResponse.json(user, { status: 201 });
}
// Dynamic segment (app/api/users/[id]/route.ts):
export async function GET(req, { params }) {
const user = await db.getUser(params.id);
if (!user) return NextResponse.json({ error: "Not found" }, { status: 404 });
return NextResponse.json(user);
}
// Middleware (middleware.ts at project root — runs on edge):
export function middleware(request) {
const token = request.cookies.get("session")?.value;
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
export const config = { matcher: ["/dashboard/:path*"] };
// Static metadata:
export const metadata = {
title: "Products | My App",
openGraph: { title: "Products", images: ["/og-products.png"] }
};
// Dynamic metadata:
export async function generateMetadata({ params }) {
const p = await getProduct(params.id);
return { title: p.name, description: p.description };
}
Track Next.js and React releases at ReleaseRun. Related: React Reference | TypeScript Reference | Express.js Reference | React EOL Tracker
🔍 Free tool: npm Package Health Checker — check Next.js and its dependencies for EOL status, known CVEs, and whether they’re actively maintained.
Founded
2023 in London, UK
Contact
hello@releaserun.com