Next.js Cheat Sheet β Routing, Data Fetching, and File Conventions
Some links in this article are affiliate links. We earn a commission at no extra cost to you when you purchase through them. Full disclosure.
Click any item to expand the explanation and examples.
π File Conventions (App Router)
page.tsx β route page files
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
// 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
app/(marketing)/about/page.tsx β /about app/(marketing)/pricing/page.tsx β /pricing app/(app)/dashboard/page.tsx β /dashboardEach 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
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
'use client' at the top when you need interactivity.
'use client';You needimport { useState } from βreactβ;
export default function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; }
β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](/blog/what-is-environment-variables/) pattern
# .env.local DATABASE_URL=[postgresql](/blog/what-is-postgresql/)://... NEXT_PUBLIC_API_URL=https://api.example.comOnly variables prefixed withServer-only (no prefix)
process.env.DATABASE_URL
Client-accessible (NEXT_PUBLIC_ prefix)
process.env.NEXT_PUBLIC_API_URL
NEXT_PUBLIC_ are available in the browser. Everything else is server-only.
Quick access: Raycast lets you search commands, snippets, and cheat sheets instantly from your keyboard. Free for Mac.
Related: What is Next.js Β· React Interview Questions