📋 Cheat Sheets

Next.js Cheat Sheet — Routing, Data Fetching, and File Conventions


Click any item to expand the explanation and examples.

📁 File Conventions (App Router)

page.tsx — route page files
Every route needs a page.tsx. The file path = the URL.
app/page.tsx              → /
app/about/page.tsx        → /about
app/blog/page.tsx         → /blog
app/blog/[slug]/page.tsx  → /blog/my-post
// app/page.tsx
export default function Home() {
  return <h1>Home</h1>;
}
layout.tsx — shared layout files
Wraps all pages in the same directory and below. Does NOT re-render on navigation.
// app/layout.tsx (root layout — required)
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

// app/dashboard/layout.tsx (nested layout) export default function DashboardLayout({ children }) { return ( <div> <nav>Dashboard Nav</nav> {children} </div> ); }

loading.tsx, error.tsx, not-found.tsx files
// app/dashboard/loading.tsx — shows while page loads
export default function Loading() {
  return <div>Loading...</div>;
}

// app/dashboard/error.tsx — catches errors ‘use client’; export default function Error({ error, reset }) { return ( <div> <p>Something went wrong: {error.message}</p> <button onClick={reset}>Try again</button> </div> ); }

// app/not-found.tsx — custom 404 export default function NotFound() { return <h1>Page not found</h1>; }

route.ts — API routes files
// app/api/users/route.ts
import { NextResponse } from 'next/server';

export async function GET() { const users = await db.user.findMany(); return NextResponse.json(users); }

export async function POST(request: Request) { const body = await request.json(); const user = await db.user.create({ data: body }); return NextResponse.json(user, { status: 201 }); }

Supported methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS.

🔀 Routing

Dynamic routes — [slug] routing
// app/blog/[slug]/page.tsx
export default async function Post({ params }) {
  const { slug } = await params;
  return <h1>Post: {slug}</h1>;
}

// Catch-all: app/docs/[…slug]/page.tsx // Matches /docs/a, /docs/a/b, /docs/a/b/c export default async function Docs({ params }) { const { slug } = await params; // [‘a’, ‘b’, ‘c’] }

// Optional catch-all: app/docs/[[…slug]]/page.tsx // Also matches /docs (without any slug)

Route groups — (folder) routing
Parentheses in folder names are ignored in the URL. Use for organizing layouts.
app/(marketing)/about/page.tsx   → /about
app/(marketing)/pricing/page.tsx → /pricing
app/(app)/dashboard/page.tsx     → /dashboard

Each group can have its own layout

app/(marketing)/layout.tsx app/(app)/layout.tsx

Parallel & intercepting routes routing
# Parallel routes — render multiple pages in same layout
app/@modal/login/page.tsx
app/layout.tsx  → receives { children, modal } props

Intercepting routes — show route in modal

app/feed/@modal/(.)photo/[id]/page.tsx

(.) = same level, (..) = one level up, (…) = root

📡 Data Fetching

Server Components — fetch in components data
Components are Server Components by default. Just use async/await.
// app/users/page.tsx (Server Component)
export default async function UsersPage() {
  const res = await fetch('https://api.example.com/users');
  const users = await res.json();

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

Caching & revalidation data
// Cache forever (default)
fetch('https://api.example.com/data');

// Revalidate every 60 seconds fetch(‘https://api.example.com/data’, { next: { revalidate: 60 } });

// No cache (always fresh) fetch(‘https://api.example.com/data’, { cache: ‘no-store’ });

// Page-level revalidation export const revalidate = 60;

// On-demand revalidation (in a Server Action or Route Handler) import { revalidatePath, revalidateTag } from ‘next/cache’; revalidatePath(‘/blog’); revalidateTag(‘posts’);

Server Actions data
// app/actions.ts
'use server';

export async function createPost(formData: FormData) { const title = formData.get(‘title’); await db.post.create({ data: { title } }); revalidatePath(‘/blog’); }

// In a component import { createPost } from ’./actions’;

export default function NewPost() { return ( <form action={createPost}> <input name=“title” /> <button type=“submit”>Create</button> </form> ); }

🧩 Common Patterns

'use client' — Client Components pattern
Add 'use client' at the top when you need interactivity.
'use client';

import { useState } from ‘react’;

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

You need ‘use client’ for: useState, useEffect, event handlers, browser APIs.

Metadata & SEO pattern
// Static metadata
export const metadata = {
  title: 'My Page',
  description: 'Page description',
  openGraph: { title: 'My Page', images: ['/og.png'] },
};

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

Middleware pattern
// middleware.ts (in project root)
import { NextResponse } from 'next/server';

export function middleware(request) { // Redirect if (request.nextUrl.pathname === ‘/old’) { return NextResponse.redirect(new URL(‘/new’, request.url)); }

// Rewrite if (request.nextUrl.pathname.startsWith(‘/api’)) { return NextResponse.rewrite(new URL(‘/api/v2’ + request.nextUrl.pathname, request.url)); }

return NextResponse.next(); }

export const config = { matcher: [‘/old’, ‘/api/:path*’], };

Environment variables pattern
# .env.local
DATABASE_URL=postgresql://...
NEXT_PUBLIC_API_URL=https://api.example.com

Server-only (no prefix)

process.env.DATABASE_URL

Client-accessible (NEXT_PUBLIC_ prefix)

process.env.NEXT_PUBLIC_API_URL

Only variables prefixed with NEXT_PUBLIC_ are available in the browser. Everything else is server-only.