Next.js
Next.js is a React framework for building full-stack web applications with built-in optimization, routing, and deployment features. This cheat sheet covers Next.js 13+ with App Router and modern patterns.
Quick Start
Installation and Setup
# Create new Next.js app
npx create-next-app@latest my-app
cd my-app
npm run dev
# With specific options
# Next.js
Next.js is a React framework for building full-stack web applications with built-in optimization, routing, and deployment features. This cheat sheet covers Next.js 15+ with App Router and modern patterns.
## Quick Start
### Installation and Setup
```bash
# Create new Next.js app (Next.js 15 requires React 19)
npx create-next-app@latest my-app
cd my-app
npm run dev
# Manual installation
npm install next@latest react@latest react-dom@latest
Basic Project Structure
my-app/
├── app/ # App Router
│ ├── globals.css # Global styles
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ └── loading.tsx # Loading UI
├── public/ # Static assets
├── next.config.js # Next.js configuration
└── package.json
Basic Configuration
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'assets.example.com',
},
],
},
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY,
}
}
module.exports = nextConfig
App Router (Next.js 15+)
File-based Routing
app/
├── page.tsx # / (home)
├── about/page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/page.tsx # /blog/[slug] (dynamic)
└── (dashboard)/ # Route groups (no URL segment)
├── settings/page.tsx # /settings
└── profile/page.tsx # /profile
Asynchronous Pages and Layouts
In Next.js 15, params and searchParams are now Promises.
// app/blog/[slug]/page.tsx - Dynamic Page
type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>
export default async function BlogPost({ params, searchParams }: { params: Params, searchParams: SearchParams }) {
const { slug } = await params;
const search = await searchParams;
return (
<div>
<h1>Blog Post: {slug}</h1>
<p>Search params: {JSON.stringify(search)}</p>
</div>
)
}
Special Files
// app/loading.tsx - Loading UI
export default function Loading() {
return <div className="spinner">Loading...</div>
}
// app/error.tsx - Error UI
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
)
}
Data Fetching
Server Components (Default)
In Next.js 15, fetch is not cached by default. You must opt-in.
// Server Component - runs on server
async function getData() {
// Opt-in to caching
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache',
});
// No caching (SSR)
const dynamicRes = await fetch('https://api.example.com/data', {
cache: 'no-store',
});
// Incremental Static Regeneration (ISR)
const isrRes = await fetch('https://api.example.com/data', {
next: { revalidate: 60 } // Revalidate every 60 seconds
});
if (!res.ok) {
throw new Error('Failed to fetch data')
}
return res.json()
}
export default async function PostsPage() {
const posts = await getData()
return (
<div>
<h1>Posts</h1>
{posts.map((post: any) => (
<article key={post.id}>
<h2>{post.title}</h2>
</article>
))}
</div>
)
}
Client Components
'use client'
import { useState, useEffect } from 'react'
export default function ClientDataFetching() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData)
}, [])
if (!data) return <div>Loading...</div>
return <div>{/* Render data */}</div>
}
Static Site Generation (SSG)
// app/posts/[slug]/page.tsx
// Generate static params at build time
export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json())
return posts.map((post) => ({ slug: post.slug }))
}
// Page component
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await fetch(`https://.../posts/${slug}`).then((res) => res.json())
return <div>{post.title}</div>
}
API Routes (Route Handlers)
Basic API Routes
// app/api/hello/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ message: 'Hello World' })
}
Dynamic API Routes
In Next.js 15, params in Route Handlers is a Promise.
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
type Params = Promise<{ id: string }>
export async function GET(request: NextRequest, { params }: { params: Params }) {
const { id } = await params;
const user = await getUserById(id);
if (!user) {
return new NextResponse('User not found', { status: 404 })
}
return NextResponse.json(user)
}
Asynchronous Headers and Cookies
// app/api/some-route/route.ts
import { cookies, headers } from 'next/headers'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const headersList = await headers()
const userAgent = headersList.get('user-agent')
const cookieStore = await cookies()
const token = cookieStore.get('token')
return NextResponse.json({ userAgent, token: token?.value })
}
Navigation and Routing
Link Component
import Link from 'next/link'
export default function Navigation() {
return (
<nav>
<Link href="/about">About</Link>
<Link href="/products" className="nav-link">Products</Link>
<Link href={`/posts/${post.slug}`}>{post.title}</Link>
</nav>
)
}
useRouter Hook
'use client'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
export default function ClientNavigation() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const handleNavigation = () => {
router.push('/dashboard')
}
return (
<div>
<p>Current path: {pathname}</p>
<button onClick={handleNavigation}>Go to Dashboard</button>
</div>
)
}
Performance Optimization
Partial Prerendering (PPR)
Enable PPR in next.config.js for incremental static/dynamic rendering.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
ppr: 'incremental',
},
}
module.exports = nextConfig
Caching Strategies
fetch is no longer cached by default.
// Default: No caching (SSR)
const dynamicData = await fetch('https://api.example.com/dynamic-data')
// Static caching (opt-in)
const staticData = await fetch('https://api.example.com/static-data', {
cache: 'force-cache'
})
// Time-based revalidation (ISR)
const revalidatedData = await fetch('https://api.example.com/data', {
next: { revalidate: 60 } // Revalidate every 60 seconds
})
// Set default caching for a layout or page
export const fetchCache = 'default-cache'
SEO and Metadata
Dynamic Metadata
params is now a Promise.
import type { Metadata } from 'next'
type Props = {
params: Promise<{ slug: string }>
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = await fetch(`https://.../posts/${slug}`).then((res) => res.json())
return {
title: post.title,
description: post.excerpt,
}
}
export default async function PostPage({ params }: { params: Promise<{ slug:string }> }) {
const { slug } = await params;
const post = await fetch(`https://.../posts/${slug}`).then((res) => res.json())
return <div>{post.title}</div>
}
This comprehensive Next.js cheat sheet covers modern patterns, App Router features, and best practices for building production-ready applications with Next.js 15+. Focus on understanding the App Router paradigm, server/client component patterns, and data fetching strategies for effective Next.js development.
Manual installation
npm install next@latest react@latest react-dom@latest
### Basic Project Structure
my-app/ ├── app/ # App Router (Next.js 13+) │ ├── globals.css # Global styles │ ├── layout.tsx # Root layout │ ├── page.tsx # Home page │ └── loading.tsx # Loading UI ├── public/ # Static assets ├── next.config.js # Next.js configuration └── package.json
### Basic Configuration
```javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['example.com'],
formats: ['image/webp', 'image/avif'],
},
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY,
}
}
module.exports = nextConfig
App Router (Next.js 13+)
File-based Routing
app/
├── page.tsx # / (home)
├── about/page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/page.tsx # /blog/[slug] (dynamic)
├── products/
│ ├── page.tsx # /products
│ ├── [id]/page.tsx # /products/[id]
│ └── [...slug]/page.tsx # /products/[...slug] (catch-all)
└── (dashboard)/ # Route groups (no URL segment)
├── settings/page.tsx # /settings
└── profile/page.tsx # /profile
Layout Components
// app/layout.tsx - Root Layout
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'My App',
description: 'Generated by Next.js',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<header>
<nav>{/* Navigation */}</nav>
</header>
<main>{children}</main>
<footer>{/* Footer */}</footer>
</body>
</html>
)
}
// app/dashboard/layout.tsx - Nested Layout
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="dashboard">
<aside>
{/* Sidebar */}
</aside>
<div className="content">
{children}
</div>
</div>
)
}
Pages
// app/page.tsx - Home Page
export default function HomePage() {
return (
<div>
<h1>Welcome to Next.js</h1>
<p>This is the home page</p>
</div>
)
}
// app/about/page.tsx - About Page
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>Learn more about our company</p>
</div>
)
}
// app/blog/[slug]/page.tsx - Dynamic Page
interface Props {
params: { slug: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export default function BlogPost({ params, searchParams }: Props) {
return (
<div>
<h1>Blog Post: {params.slug}</h1>
<p>Search params: {JSON.stringify(searchParams)}</p>
</div>
)
}
Special Files
// app/loading.tsx - Loading UI
export default function Loading() {
return <div className="spinner">Loading...</div>
}
// app/error.tsx - Error UI
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
)
}
// app/not-found.tsx - 404 Page
import Link from 'next/link'
export default function NotFound() {
return (
<div>
<h2>Page Not Found</h2>
<p>Could not find the requested page.</p>
<Link href="/">Return Home</Link>
</div>
)
}
Data Fetching
Server Components (Default)
// Server Component - runs on server
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache', // Default caching
// cache: 'no-store', // No caching (SSR)
// next: { revalidate: 60 } // Revalidate every 60 seconds
})
if (!res.ok) {
throw new Error('Failed to fetch data')
}
return res.json()
}
export default async function PostsPage() {
const posts = await getData()
return (
<div>
<h1>Posts</h1>
{posts.map((post: any) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
)
}
Client Components
'use client'
import { useState, useEffect } from 'react'
export default function ClientDataFetching() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(data => {
setData(data)
setLoading(false)
})
}, [])
if (loading) return <div>Loading...</div>
return (
<div>
<h1>Client-side Data</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
)
}
Static Site Generation (SSG)
// app/posts/[slug]/page.tsx
interface Post {
id: string
title: string
content: string
slug: string
}
// Generate static params at build time
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json())
return posts.map((post: Post) => ({
slug: post.slug,
}))
}
// Generate metadata for each page
export async function generateMetadata(
{ params }: { params: { slug: string } }
) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(res => res.json())
return {
title: post.title,
description: post.excerpt,
}
}
// Page component
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(res => res.json())
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
Incremental Static Regeneration (ISR)
async function getData() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Revalidate every hour
})
return res.json()
}
export default async function PostsPage() {
const posts = await getData()
return (
<div>
{posts.map((post: any) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}
API Routes
Basic API Routes
// app/api/hello/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ message: 'Hello World' })
}
export async function POST(request: NextRequest) {
const body = await request.json()
return NextResponse.json({
message: 'Data received',
data: body
})
}
Dynamic API Routes
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const id = params.id
// Fetch user data
const user = await getUserById(id)
if (!user) {
return new NextResponse('User not found', { status: 404 })
}
return NextResponse.json(user)
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const id = params.id
const body = await request.json()
const updatedUser = await updateUser(id, body)
return NextResponse.json(updatedUser)
}
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const id = params.id
await deleteUser(id)
return new NextResponse(null, { status: 204 })
}
Error Handling in API Routes
// app/api/protected/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
try {
// Check authentication
const token = request.headers.get('authorization')
if (!token) {
return new NextResponse('Unauthorized', { status: 401 })
}
// Validate token
const user = await validateToken(token)
if (!user) {
return new NextResponse('Invalid token', { status: 401 })
}
// Return protected data
const data = await getProtectedData(user.id)
return NextResponse.json(data)
} catch (error) {
console.error('API Error:', error)
return new NextResponse('Internal Server Error', { status: 500 })
}
}
Middleware
// middleware.ts (in root directory)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Check if user is authenticated
const token = request.cookies.get('auth-token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Add custom headers
const response = NextResponse.next()
response.headers.set('x-custom-header', 'my-value')
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*']
}
Navigation and Routing
Link Component
import Link from 'next/link'
export default function Navigation() {
return (
<nav>
{/* Basic link */}
<Link href="/about">About</Link>
{/* Link with custom styling */}
<Link href="/products" className="nav-link">
Products
</Link>
{/* Dynamic link */}
<Link href={`/posts/${post.slug}`}>
{post.title}
</Link>
{/* External link */}
<Link href="https://example.com" target="_blank">
External Link
</Link>
{/* Link with query parameters */}
<Link
href={{
pathname: '/search',
query: { q: 'nextjs' }
}}
>
Search
</Link>
</nav>
)
}
useRouter Hook
'use client'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
export default function ClientNavigation() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const handleNavigation = () => {
// Programmatic navigation
router.push('/dashboard')
// router.replace('/dashboard') // Replace current entry
// router.back() // Go back
// router.forward() // Go forward
}
const handleSearch = (term: string) => {
// Update URL with search params
const params = new URLSearchParams(searchParams)
params.set('q', term)
router.push(`${pathname}?${params.toString()}`)
}
return (
<div>
<p>Current path: {pathname}</p>
<p>Search: {searchParams.get('q')}</p>
<button onClick={handleNavigation}>Go to Dashboard</button>
<input
type="text"
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search..."
/>
</div>
)
}
Styling
CSS Modules
// components/Button.module.css
.button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
background-color: #0070f3;
color: white;
cursor: pointer;
}
.button:hover {
background-color: #0051a2;
}
.primary {
background-color: #0070f3;
}
.secondary {
background-color: #666;
}
// components/Button.tsx
import styles from './Button.module.css'
interface ButtonProps {
children: React.ReactNode
variant?: 'primary' | 'secondary'
onClick?: () => void
}
export default function Button({ children, variant = 'primary', onClick }: ButtonProps) {
return (
<button
className={`${styles.button} ${styles[variant]}`}
onClick={onClick}
>
{children}
</button>
)
}
Global Styles
/* app/globals.css */
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
html,
body {
max-width: 100vw;
overflow-x: hidden;
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
Tailwind CSS Integration
// Install: npm install -D tailwindcss postcss autoprefixer
// Initialize: npx tailwindcss init -p
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {},
},
plugins: [],
}
// Component with Tailwind classes
export default function Card({ children }: { children: React.ReactNode }) {
return (
<div className="max-w-sm mx-auto bg-white rounded-xl shadow-md overflow-hidden">
<div className="p-6">
{children}
</div>
</div>
)
}
Image Optimization
Next.js Image Component
import Image from 'next/image'
export default function Gallery() {
return (
<div>
{/* Local image */}
<Image
src="/hero-image.jpg"
alt="Hero"
width={800}
height={600}
priority // Load immediately
/>
{/* Remote image */}
<Image
src="https://example.com/image.jpg"
alt="Remote image"
width={400}
height={300}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..." // Base64 blur placeholder
/>
{/* Responsive image */}
<Image
src="/responsive-image.jpg"
alt="Responsive"
fill
style={{ objectFit: 'cover' }}
/>
{/* Image with custom loader */}
<Image
src="image-key"
alt="Custom loader"
width={300}
height={200}
loader={({ src, width, quality }) => {
return `https://cdn.example.com/${src}?w=${width}&q=${quality || 75}`
}}
/>
</div>
)
}
Image Configuration
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: ['example.com', 'cdn.example.com'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
formats: ['image/webp', 'image/avif'],
minimumCacheTTL: 60,
dangerouslyAllowSVG: true,
contentDispositionType: 'attachment',
},
}
module.exports = nextConfig
Authentication Patterns
Session-based Authentication
// app/api/auth/login/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { cookies } from 'next/headers'
import jwt from 'jsonwebtoken'
export async function POST(request: NextRequest) {
const { email, password } = await request.json()
// Validate credentials
const user = await validateUser(email, password)
if (!user) {
return new NextResponse('Invalid credentials', { status: 401 })
}
// Create JWT token
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET!,
{ expiresIn: '7d' }
)
// Set cookie
cookies().set('auth-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7, // 7 days
})
return NextResponse.json({ user: { id: user.id, email: user.email } })
}
Protected Routes
// app/dashboard/page.tsx
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
import jwt from 'jsonwebtoken'
async function getCurrentUser() {
const token = cookies().get('auth-token')?.value
if (!token) {
return null
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any
return await getUserById(decoded.userId)
} catch {
return null
}
}
export default async function DashboardPage() {
const user = await getCurrentUser()
if (!user) {
redirect('/login')
}
return (
<div>
<h1>Welcome, {user.name}!</h1>
<p>This is your dashboard</p>
</div>
)
}
Environment Variables
Environment Configuration
# .env.local (not committed to Git)
DATABASE_URL=postgresql://...
JWT_SECRET=your-secret-key
API_KEY=your-api-key
# .env (committed to Git - public variables only)
NEXT_PUBLIC_APP_NAME=My App
NEXT_PUBLIC_API_URL=https://api.example.com
// lib/env.ts - Type-safe environment variables
export const env = {
DATABASE_URL: process.env.DATABASE_URL!,
JWT_SECRET: process.env.JWT_SECRET!,
// Public variables (available in browser)
NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME!,
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL!,
}
// Usage in components
export default function Header() {
return (
<header>
<h1>{env.NEXT_PUBLIC_APP_NAME}</h1>
</header>
)
}
Database Integration
Prisma Setup
# Install Prisma
npm install prisma @prisma/client
npx prisma init
# Generate client after schema changes
npx prisma generate
# Run migrations
npx prisma migrate dev --name init
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
// app/api/posts/route.ts
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET() {
const posts = await prisma.post.findMany({
include: {
author: {
select: {
name: true,
email: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
})
return NextResponse.json(posts)
}
export async function POST(request: Request) {
const { title, content, authorId } = await request.json()
const post = await prisma.post.create({
data: {
title,
content,
authorId,
},
})
return NextResponse.json(post, { status: 201 })
}
Performance Optimization
Code Splitting and Lazy Loading
import { lazy, Suspense } from 'react'
import dynamic from 'next/dynamic'
// React.lazy (client-side only)
const LazyComponent = lazy(() => import('../components/ExpensiveComponent'))
// Next.js dynamic imports
const DynamicComponent = dynamic(() => import('../components/DynamicComponent'), {
loading: () => <p>Loading...</p>,
ssr: false, // Disable SSR for this component
})
// Dynamic import with named export
const DynamicChart = dynamic(
() => import('../components/Chart').then(mod => mod.Chart),
{ ssr: false }
)
export default function Page() {
return (
<div>
<h1>My Page</h1>
<Suspense fallback={<div>Loading lazy component...</div>}>
<LazyComponent />
</Suspense>
<DynamicComponent />
<DynamicChart data={chartData} />
</div>
)
}
Caching Strategies
// Static caching (default)
const staticData = await fetch('https://api.example.com/static-data')
// No caching (SSR)
const dynamicData = await fetch('https://api.example.com/dynamic-data', {
cache: 'no-store'
})
// Time-based revalidation (ISR)
const revalidatedData = await fetch('https://api.example.com/data', {
next: { revalidate: 60 } // Revalidate every 60 seconds
})
// Tag-based revalidation
const taggedData = await fetch('https://api.example.com/data', {
next: { tags: ['posts'] }
})
// Manual revalidation
import { revalidateTag, revalidatePath } from 'next/cache'
// In API route or server action
revalidateTag('posts') // Revalidate all requests with 'posts' tag
revalidatePath('/posts') // Revalidate specific path
SEO and Metadata
Dynamic Metadata
import type { Metadata } from 'next'
interface Props {
params: { slug: string }
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(res => res.json())
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [
{
url: post.featuredImage,
width: 1200,
height: 630,
alt: post.title,
},
],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.featuredImage],
},
}
}
export default async function PostPage({ params }: Props) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(res => res.json())
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
Sitemap Generation
// app/sitemap.ts
import type { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
{
url: 'https://example.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: 'https://example.com/blog',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.5,
},
]
}
Robots.txt
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/private/',
},
sitemap: 'https://example.com/sitemap.xml',
}
}
Testing
Jest Setup
// jest.config.js
/** @type {import('jest').Config} */
const config = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/$1',
},
transform: {
'^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],
},
}
module.exports = config
// jest.setup.js
import '@testing-library/jest-dom'
Component Testing
// __tests__/components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import Button from '@/components/Button'
describe('Button', () => {
test('renders with correct text', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
test('calls onClick when clicked', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)
fireEvent.click(screen.getByText('Click me'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
})
API Route Testing
// __tests__/api/users.test.ts
import { GET } from '@/app/api/users/route' // Adjust path as needed
import { NextRequest } from 'next/server'
describe('/api/users route', () => {
it('returns a successful response with a list of users', async () => {
// Mock the request object
const request = new NextRequest('http://localhost/api/users')
// Call the route handler
const response = await GET(request)
const body = await response.json()
// Assertions
expect(response.status).toBe(200)
expect(body).toHaveProperty('users')
expect(Array.isArray(body.users)).toBe(true)
})
})
Deployment
Vercel Deployment
# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Deploy to production
vercel --prod
Docker Deployment
# Dockerfile
FROM node:18-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
RUN mkdir .next
RUN chown nextjs:nodejs .next
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
Environment Variables for Production
# .env.production
NEXT_PUBLIC_API_URL=https://api.production.com
DATABASE_URL=postgresql://prod-db...
JWT_SECRET=production-secret-key
Common Patterns and Best Practices
Error Handling
// Global error boundary
'use client'
import { useEffect } from 'react'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
useEffect(() => {
console.error(error)
}, [error])
return (
<html>
<body>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</body>
</html>
)
}
Loading States
// app/dashboard/loading.tsx
export default function DashboardLoading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded mb-4"></div>
<div className="h-4 bg-gray-200 rounded mb-2"></div>
<div className="h-4 bg-gray-200 rounded mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
</div>
)
}
Form Handling with Server Actions
// app/contact/page.tsx
import { redirect } from 'next/navigation'
async function createContact(formData: FormData) {
'use server'
const name = formData.get('name') as string
const email = formData.get('email') as string
const message = formData.get('message') as string
// Save to database
await saveContact({ name, email, message })
redirect('/contact/success')
}
export default function ContactPage() {
return (
<form action={createContact}>
<input type="text" name="name" placeholder="Name" required />
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<button type="submit">Send Message</button>
</form>
)
}
TypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@/components/*": ["./components/*"],
"@/lib/*": ["./lib/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
This comprehensive Next.js cheat sheet covers modern patterns, App Router features, and best practices for building production-ready applications with Next.js 13+. Focus on understanding the App Router paradigm, server/client component patterns, and data fetching strategies for effective Next.js development.