Next.js Routing Cheatsheet
Basic Route Structure
app/
├── page.js # / (home)
├── about/
│ └── page.js # /about
├── blog/
│ ├── page.js # /blog
│ └── [slug]/
│ └── page.js # /blog/[slug]
├── products/
│ ├── page.js # /products
│ └── [category]/
│ └── [id]/
│ └── page.js # /products/[category]/[id]
└── api/
├── users/
│ └── route.js # /api/users
└── posts/
└── [id]/
└── route.js # /api/posts/[id]
Basic Pages
export default function HomePage() {
return (
<div>
<h1>Welcome to our site</h1>
<p>This is the home page</p>
</div>
);
}
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>Learn more about our company</p>
</div>
);
}
Single Dynamic Segment
export default function BlogPost({ params }) {
return (
<div>
<h1>Blog Post: {params.slug}</h1>
<p>This is the content for {params.slug}</p>
</div>
);
}
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
Multiple Dynamic Segments
export default function ProductPage({ params }) {
const { category, id } = params;
return (
<div>
<h1>Product Details</h1>
<p>Category: {category}</p>
<p>Product ID: {id}</p>
</div>
);
}
export async function generateStaticParams() {
const products = await getProducts();
return products.map((product) => ({
category: product.category,
id: product.id,
}));
}
Catch-All Routes
export default function DocsPage({ params }) {
const { slug } = params;
const path = slug.join('/');
return (
<div>
<h1>Documentation</h1>
<p>Current path: {path}</p>
</div>
);
}
Route Groups
Basic API Route
export async function GET() {
return Response.json({ message: 'Hello World' });
}
export async function POST(request) {
const data = await request.json();
return Response.json({ received: data });
}
Dynamic API Routes
export async function GET(request, { params }) {
const { id } = params;
const user = await getUser(id);
if (!user) {
return Response.json({ error: 'User not found' }, { status: 404 });
}
return Response.json(user);
}
export async function PUT(request, { params }) {
const { id } = params;
const data = await request.json();
const updatedUser = await updateUser(id, data);
return Response.json(updatedUser);
}
export async function DELETE(request, { params }) {
const { id } = params;
await deleteUser(id);
return Response.json({ message: 'User deleted' });
}
API Route with Query Parameters
export async function GET(request) {
const { searchParams } = new URL(request.url);
const query = searchParams.get('q');
const page = searchParams.get('page') || '1';
const results = await search(query, parseInt(page));
return Response.json({
results,
page: parseInt(page),
query
});
}
API Route with Middleware
import { NextResponse } from 'next/server';
export async function GET(request) {
const token = request.headers.get('authorization');
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const user = await verifyToken(token);
return NextResponse.json({ user });
}
Link Component
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
<Link href="/" className="nav-link">
Home
</Link>
<Link href="/about" className="nav-link">
About
</Link>
<Link href="/blog" className="nav-link">
Blog
</Link>
<Link href="/products" className="nav-link">
Products
</Link>
</nav>
);
}
Dynamic Links
import Link from 'next/link';
export default function BlogList({ posts }) {
return (
<div>
{posts.map(post => (
<Link
key={post.id}
href={`/blog/${post.slug}`}
className="blog-link"
>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</Link>
))}
</div>
);
}
Programmatic Navigation
'use client';
import { useRouter } from 'next/navigation';
export default function LoginForm() {
const router = useRouter();
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const response = await fetch('/api/login', {
method: 'POST',
body: formData,
});
if (response.ok) {
router.push('/dashboard');
} else {
router.push('/login?error=invalid');
}
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Login</button>
</form>
);
}
Navigation with State
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
export default function SearchComponent() {
const router = useRouter();
const searchParams = useSearchParams();
const handleSearch = (query) => {
const params = new URLSearchParams(searchParams);
params.set('q', query);
router.push(`/search?${params.toString()}`);
};
return (
<div>
<input
type="text"
placeholder="Search..."
onChange={(e) => handleSearch(e.target.value)}
/>
</div>
);
}
Basic Middleware
import { NextResponse } from 'next/server';
export function middleware(request) {
const path = request.nextUrl.pathname;
const isAuthenticated = request.cookies.get('auth-token');
if (path.startsWith('/dashboard') && !isAuthenticated) {
return NextResponse.redirect(new URL('/login', request.url));
}
const response = NextResponse.next();
response.headers.set('x-custom-header', 'value');
return response;
}
export const config = {
matcher: [
'/dashboard/:path*',
'/api/protected/:path*',
],
};
Authentication Middleware
import { NextResponse } from 'next/server';
import { verifyToken } from './lib/auth';
export async function middleware(request) {
const token = request.cookies.get('auth-token')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
try {
const user = await verifyToken(token);
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', user.id);
requestHeaders.set('x-user-role', user.role);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
} catch (error) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};
Internationalization Middleware
import { NextResponse } from 'next/server';
const locales = ['en', 'es', 'fr'];
const defaultLocale = 'en';
export function middleware(request) {
const pathname = request.nextUrl.pathname;
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) return;
const locale = defaultLocale;
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
Root Layout
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header>
<nav>{/* Navigation */}</nav>
</header>
<main>{children}</main>
<footer>
<p>© 2024 My App</p>
</footer>
</body>
</html>
);
}
Nested Layouts
export default function DashboardLayout({ children }) {
return (
<div className="dashboard-layout">
<aside className="sidebar">
<nav>
<a href="/dashboard">Overview</a>
<a href="/dashboard/profile">Profile</a>
<a href="/dashboard/settings">Settings</a>
</nav>
</aside>
<main className="content">
{children}
</main>
</div>
);
}
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<p>Welcome to your dashboard</p>
</div>
);
}
Parallel Routes
export default function LoginModal() {
return (
<div className="modal">
<h2>Login</h2>
{/* Login form */}
</div>
);
}
export default function RegisterModal() {
return (
<div className="modal">
<h2>Register</h2>
{/* Register form */}
</div>
);
}
export default function Layout({ children, modal }) {
return (
<html>
<body>
{children}
{modal}
</body>
</html>
);
}
Dynamic Metadata
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.featuredImage],
},
};
}
Sitemap Generation
export default async function sitemap() {
const baseUrl = 'https://example.com';
const posts = await getAllPosts();
const blogUrls = posts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly',
priority: 0.7,
}));
const staticPages = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
{
url: `${baseUrl}/about`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
];
return [...staticPages, ...blogUrls];
}
Robots.txt
export default function robots() {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/private/', '/admin/', '/api/'],
},
],
sitemap: 'https://example.com/sitemap.xml',
};
}
Error Pages
'use client';
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
export default function NotFound() {
return (
<div>
<h2>Page Not Found</h2>
<p>Could not find the requested resource.</p>
</div>
);
}
export default function GlobalError({ error, reset }) {
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</body>
</html>
);
}
Loading States
export default function Loading() {
return (
<div className="loading">
<div className="spinner"></div>
<p>Loading...</p>
</div>
);
}
export default function DashboardLoading() {
return (
<div className="dashboard-loading">
<div className="skeleton-header"></div>
<div className="skeleton-content"></div>
</div>
);
}
Route Organization
app/
├── (auth)/
│ ├── login/
│ └── register/
├── (dashboard)/
│ ├── dashboard/
│ └── profile/
├── (marketing)/
│ ├── about/
│ └── contact/
└── api/
├── auth/
└── users/
app/
├── very/
│ └── deep/
│ └── nested/
│ └── routes/
│ └── that/
│ └── are/
│ └── hard/
│ └── to/
│ └── maintain/
Performance Optimization
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
<Link href="/dashboard" prefetch={true}>
Dashboard
</Link>
<Link href="/profile" prefetch={false}>
Profile
</Link>
</nav>
);
}
import dynamic from 'next/dynamic';
const HeavyPage = dynamic(() => import('./HeavyPage'), {
loading: () => <div>Loading...</div>,
ssr: false,
});
Security Considerations
export default function UserPage({ params }) {
const { id } = params;
if (!/^\d+$/.test(id)) {
notFound();
}
return <div>User {id}</div>;
}
import { rateLimit } from '@/lib/rate-limit';
export async function POST(request) {
const limiter = await rateLimit(request);
if (!limiter.success) {
return Response.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
return Response.json({ success: true });
}