Skip to main content
StackDevLife
Next.jsintermediateMay 2, 2026

Next.js App Router Cheatsheet

App Router conventions, data fetching, routing, metadata, and server vs client component patterns.

File Conventions

page.tsx

app/blog/page.tsx  → /blog
app/blog/[slug]/page.tsx  → /blog/:slug
app/(group)/about/page.tsx → /about (group ignored in URL)

layout.tsx

export default function Layout({ children }) {
  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

loading.tsx

export default function Loading() {
  return <Spinner />; // automatic Suspense boundary
}

error.tsx

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

not-found.tsx

export default function NotFound() {
  return <h1>404 — Page not found</h1>;
}

route.ts (API route)

export async function GET(request: Request) {
  return Response.json({ ok: true });
}

Server vs Client Components

Server Component (default)

// No directive needed — this runs on the server
export default async function Page() {
  const data = await fetchFromDB();
  return <div>{data.title}</div>;
}

Client Component

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

Rule of thumb

Server: data fetching, DB access, sensitive secrets.
Client: interactivity, browser APIs, useState/useEffect.

Push "use client" as far down the tree as possible.

Data Fetching

Fetch in Server Component

async function Page() {
  const data = await fetch("https://api.example.com/posts", {
    next: { revalidate: 60 }, // ISR — revalidate every 60s
  }).then(r => r.json());
  return <PostList posts={data} />;
}

Static (cache forever)

fetch(url, { cache: "force-cache" });

Dynamic (no cache)

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

generateStaticParams

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

Parallel data fetching

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

Routing & Navigation

Link component

import Link from "next/link";
<Link href="/about">About</Link>
<Link href={`/post/${slug}`} prefetch={false}>Post</Link>

useRouter (client)

"use client"
import { useRouter } from "next/navigation";
const router = useRouter();
router.push("/dashboard");
router.replace("/login");
router.back();

usePathname

"use client"
import { usePathname } from "next/navigation";
const pathname = usePathname(); // "/blog/hello-world"

useSearchParams

"use client"
import { useSearchParams } from "next/navigation";
const params = useSearchParams();
const q = params.get("q");

redirect (server)

import { redirect } from "next/navigation";
if (!session) redirect("/login");

notFound (server)

import { notFound } from "next/navigation";
if (!post) notFound();

Metadata

Static metadata

export const metadata: Metadata = {
  title: "My Page",
  description: "Page description",
};

Dynamic metadata

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

Open Graph

export const metadata = {
  openGraph: {
    title: "My Page",
    description: "...",
    images: [{ url: "/og.png", width: 1200, height: 630 }],
  },
};

Image & Font Optimization

next/image

import Image from "next/image";
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={630}
  priority  // for above-the-fold images
/>

next/font (Google)

import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
<html className={inter.className}>
#app-router#server-components#data-fetching#routing