Skip to content

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 })
}
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*']
}
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.