Skip to content

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