Tags: nextjs, react, app-router, pages-router, server-components, middleware, caching Last updated: 2026-06-26

Next.js Cheatsheet

Quick Reference

ConceptApp RouterPages Router
Pages app/page.tsx pages/index.tsx
Layouts app/layout.tsx _app.tsx + per-page
Data fetching async components, fetch() getServerSideProps, getStaticProps
Dynamic routes app/[id]/page.tsx pages/[id].tsx
API routes app/api/route.ts pages/api/*.ts
Middleware middleware.ts (root) middleware.ts (root)
Metadata export const metadata <Head> component
Loading UI loading.tsx (Suspense) Manual <Suspense>

App Router vs Pages Router

Pages Router (Classic)

// pages/posts/[id].tsx
export async function getServerSideProps(ctx) {
  const post = await fetch(...).then(r => r.json());
  return { props: { post } };
}
export default function Post({ post }) {
  return <h1>{post.title}</h1>;
}

App Router (Modern)

// app/posts/[id]/page.tsx
export default async function Post({ params }) {
  const post = await fetch(...).then(r => r.json());
  return <h1>{post.title}</h1>;
}

Key Differences

Server Components & Client Components

Server Component (Default)

// No "use client" = server component
// Can be async, access DB directly, keep secrets
export default async function ServerComp() {
  const data = await db.query("SELECT ...");
  return <div>{data}</div>;
}

Client Component

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

export default function ClientComp() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      {count}
    </button>
  );
}

Server-Only Imports

import "server-only";  // Build error if imported client-side
import "client-only";  // Build error if imported server-side

When to Use Client Components

Routing

App Router File Conventions

app/
├── layout.tsx          # Shared layout (wraps children)
├── page.tsx            # Route UI
├── loading.tsx         # Suspense fallback
├── error.tsx           # Error boundary
├── not-found.tsx       # 404 UI
├── route.ts            # API endpoint
├── template.tsx        # Re-mounts on navigate
└── default.tsx         # Parallel route fallback

Dynamic Routes

// app/blog/[slug]/page.tsx
export default async function Post({ params }) {
  const { slug } = await params;
  const post = await getPost(slug);
  return <article>{post.content}</article>;
}

// Catch-all: [...slug]  → /shop/a, /shop/a/b
// Optional:   [[...slug]] → also matches /shop

Navigation

import Link from "next/link";
// App Router
import { useRouter } from "next/navigation";
// Pages Router
import { useRouter } from "next/router";

<Link href="/about">About</Link>

const router = useRouter();
router.push("/dashboard");
router.replace("/login");
router.refresh();        // Re-fetch server components
router.back();

Data Fetching & Caching

fetch() in App Router

// Static (cached forever — default)
const res = await fetch("https://api.example.com/data");

// Revalidate every 60 seconds
const res = await fetch("https://api.example.com/data", {
  next: { revalidate: 60 }
});

// Dynamic (no cache)
const res = await fetch("https://api.example.com/data", {
  cache: "no-store"
});

// On-demand revalidation
import { revalidateTag, revalidatePath } from "next/cache";
revalidateTag("posts");
revalidatePath("/blog/[slug]");

Static Generation (Pages Router)

export async function getStaticProps() {
  return { props: { data }, revalidate: 3600 };  // ISR
}
export async function getStaticPaths() {
  return { paths: [...], fallback: "blocking" };
}

Server Actions (App Router)

// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";

export async function createPost(formData: FormData) {
  const title = formData.get("title");
  await db.insert({ title });
  revalidatePath("/posts");
}

// Client usage
<form action={createPost}>
  <input name="title" />
  <button type="submit">Create</button>
</form>

Middleware

// middleware.ts (at project root, NOT in app/)
import { NextResponse } from "next/server";

export function middleware(request: NextRequest) {
  const token = request.cookies.get("token")?.value;
  if (!token && request.nextUrl.pathname
      .startsWith("/dashboard")) {
    return NextResponse.redirect(
      new URL("/login", request.url));
  }
  return NextResponse.next();
}

export const config = {
  matcher: ["/dashboard/:path*"],
};

Caching Strategies

CacheDefaultControl
Full RouteStatic (build time) dynamic = "force-dynamic"
Data (fetch)Cached forever cache: "no-store" or revalidate: N
Client Router30s in-memory router.refresh()
ISRRevalidate after TTL revalidate in fetch/gsP

Route Segment Config

// app/page.tsx
export const dynamic = "force-dynamic";   // Always SSR
export const dynamic = "force-static";    // Static (default)
export const revalidate = 60;            // ISR every 60s
export const runtime = "edge";           // Edge Runtime

Layouts & Templates

Root Layout (Required)

// app/layout.tsx
export const metadata = {
  title: "My App",
  description: "Built with Next.js",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <main>{children}</main>
      </body>
    </html>
  );
}

Nested Layouts

// app/dashboard/layout.tsx
// Wraps all /dashboard/* pages — state persists
export default function DashboardLayout({ children }) {
  return (
    <div><Sidebar />{children}</div>
  );
}

Template (vs Layout)

Like layout but re-mounts on every navigation. Use for fresh state on each visit (page view tracking, resetting forms).

Metadata & SEO

// Static
export const metadata = {
  title: "About",
  openGraph: { images: ["/og.png"] },
};

// Dynamic
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  return { title: post.title };
}

Tips