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