Next.js App Router Cheat Sheet

New

Next.js 14 App Router: layouts, pages, server components, routing, and data fetching

File Conventions

page.tsx

Defines a route segment UI. Only page.tsx makes a route publicly accessible.

// app/about/page.tsx export default function AboutPage() { return <h1>About</h1>; }

layout.tsx

Shared UI wrapping page and children. Persists across navigations.

export default function Layout({ children }: { children: React.ReactNode }) { return <div className="container">{children}</div>; }

loading.tsx

Automatic loading UI shown while a page segment is loading

export default function Loading() { return <div>Loading...</div>; }

error.tsx

Error boundary UI for a route segment. Must be a Client Component.

"use client" export default function Error({ error, reset }: { error: Error; reset: () => void }) { return <button onClick={reset}>Try again</button>; }

not-found.tsx

UI rendered when notFound() is called or a URL has no match

export default function NotFound() { return <h2>404 - Page Not Found</h2>; }

Server vs Client Components

Server Component (default)

Renders on the server. Can fetch data directly. No hooks or browser APIs.

// No "use client" directive needed export default async function Page() { const data = await fetch("https://api.example.com/data"); const json = await data.json(); return <div>{json.title}</div>; }

Client Component

Runs in the browser. Can use hooks, event listeners, and browser APIs.

"use client" import { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; }

Data Fetching

fetch with cache

Default: cached indefinitely (static). force-dynamic disables cache.

const res = await fetch("https://api.example.com/posts", { cache: "force-cache", // static (default) // cache: "no-store", // dynamic, no cache // next: { revalidate: 60 } // ISR: revalidate every 60s });

Parallel Data Fetching

Fetch multiple resources in parallel to avoid waterfalls

const [users, posts] = await Promise.all([ fetch("/api/users").then(r => r.json()), fetch("/api/posts").then(r => r.json()), ]);

Server Actions

Async functions that run on the server, callable from Client Components

"use server" export async function createPost(formData: FormData) { const title = formData.get("title") as string; await db.post.create({ data: { title } }); revalidatePath("/posts"); }

Dynamic Routes

Dynamic Segment

Folder in brackets creates a dynamic route parameter

// app/posts/[id]/page.tsx export default function PostPage({ params }: { params: { id: string } }) { return <h1>Post {params.id}</h1>; }

Catch-all Segment

Match multiple path segments with [...slug]

// app/docs/[...slug]/page.tsx export default function Doc({ params }: { params: { slug: string[] } }) { return <div>{params.slug.join("/")}</div>; }

generateStaticParams

Pre-generate static paths for dynamic routes at build time

export async function generateStaticParams() { const posts = await getPosts(); return posts.map(p => ({ id: String(p.id) })); }

Metadata API

Static Metadata

Export a metadata object from any layout or page

export const metadata: Metadata = { title: "My App", description: "Welcome to My App", openGraph: { title: "My App", images: ["/og.png"] }, };

Dynamic Metadata

Generate metadata based on route params

export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> { const post = await getPost(params.id); return { title: post.title }; }

Route Handlers

API Route Handler

Create API endpoints with app/api/route.ts

// app/api/hello/route.ts import { NextResponse } from "next/server"; export async function GET() { return NextResponse.json({ message: "Hello" }); } export async function POST(request: Request) { const body = await request.json(); return NextResponse.json({ received: body }, { status: 201 }); }

Common Patterns

Server Component with Fetch

Data fetching in a Server Component — no useState or useEffect needed

// app/users/page.tsx
interface User { id: number; name: string; email: string }

export default async function UsersPage() {
  const res = await fetch("https://jsonplaceholder.typicode.com/users", {
    next: { revalidate: 3600 }
  });
  const users: User[] = await res.json();

  return (
    <ul>
      {users.map(u => (
        <li key={u.id}>{u.name} — {u.email}</li>
      ))}
    </ul>
  );
}

generateMetadata

Dynamic page title and OG tags based on route params

import type { Metadata } from "next";

export async function generateMetadata(
  { params }: { params: { slug: string } }
): Promise<Metadata> {
  const post = await getPost(params.slug);
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      images: [post.coverImage],
    },
  };
}

Route Handler (CRUD)

Typed route handler with request body parsing

// app/api/posts/[id]/route.ts
import { NextResponse } from "next/server";

export async function GET(_req: Request, { params }: { params: { id: string } }) {
  const post = await db.post.findUnique({ where: { id: Number(params.id) } });
  if (!post) return NextResponse.json({ error: "Not found" }, { status: 404 });
  return NextResponse.json(post);
}

export async function DELETE(_req: Request, { params }: { params: { id: string } }) {
  await db.post.delete({ where: { id: Number(params.id) } });
  return new NextResponse(null, { status: 204 });
}

Tips & Best Practices

Default to Server Components — add 'use client' only when you need interactivity or browser APIs

Colocate data fetching as close to the component that needs it as possible

Use next: { revalidate: N } for ISR instead of disabling cache entirely

Use loading.tsx to provide instant loading UI without extra boilerplate

Wrap Client Components at the leaf of the tree to keep as much code on the server as possible