Next.js logoNext.jsv16INTERMEDIATE

Next.js

Next.js cheat sheet for App Router, Server Components, Server Actions, data fetching, routing, and caching with code examples.

20 min read
nextjsreactssrapp-routerserver-componentsrsctypescript

Sign in to mark items as known and track your progress.

Sign in

Project Setup & Structure

Create new Next.js app

📄 Codebash
# Create new Next.js app
npx create-next-app@latest my-app

# With all recommended options
npx create-next-app@latest my-app --typescript --tailwind --app --src-dir

# With package manager choice
npx create-next-app@latest my-app --use-npm
npx create-next-app@latest my-app --use-yarn
npx create-next-app@latest my-app --use-pnpm
npx create-next-app@latest my-app --use-bun
💡 Use --typescript flag for TypeScript support
⚡ --tailwind adds Tailwind CSS automatically
📌 App Router + Turbopack are the defaults in 16
🟢 --src-dir creates a src/ directory

App Router file structure

📄 Codetext
app/
├── layout.tsx          # Root layout
├── page.tsx           # Home page (/)
├── loading.tsx        # Loading UI
├── error.tsx          # Error UI
├── not-found.tsx      # 404 page
├── global.css         # Global styles
├── about/
│   └── page.tsx       # /about route
├── blog/
│   ├── layout.tsx     # Nested layout
│   ├── page.tsx       # /blog route
│   └── [slug]/
│       └── page.tsx   # Dynamic route /blog/[slug]
└── api/
    └── route.ts       # API route
💡 page.tsx defines a route
⚡ layout.tsx wraps pages and preserves state
📌 loading.tsx shows while page loads
🔥 error.tsx handles errors with error boundary

Configuration files

📄 Codejavascript
// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  // Enable React Strict Mode
  reactStrictMode: true,
  
  // Configure remote image hosts (domains is deprecated)
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**.example.com',
      },
    ],
  },
  
  // Environment variables
  env: {
    API_URL: process.env.API_URL,
  },
  
  // Redirects
  async redirects() {
    return [
      {
        source: '/old-path',
        destination: '/new-path',
        permanent: true,
      },
    ]
  },
}

export default nextConfig
💡 next.config.ts (or .js) - typed via NextConfig
⚡ TypeScript config is auto-generated
📌 .env.local for environment variables
🟢 proxy.ts (was middleware.ts) runs before requests

Routing & Navigation

Basic routing

📄 Codetypescript
// app/page.tsx - Home route (/)
export default function HomePage() {
  return <h1>Home Page</h1>
}

// app/about/page.tsx - About route (/about)
export default function AboutPage() {
  return <h1>About Page</h1>
}

// app/(marketing)/contact/page.tsx - Route group
// URL is /contact (marketing is ignored)
export default function ContactPage() {
  return <h1>Contact Page</h1>
}
💡 Folders define routes, page.tsx makes them accessible
⚡ Nested folders create nested routes
📌 Use Link component for client-side navigation
🔥 Route groups with () don't affect URL

Dynamic routes

typescript
// app/blog/[slug]/page.tsx
💡 [param] for dynamic segments
⚡ [...slug] for catch-all routes
📌 [[...slug]] for optional catch-all
🟢 Access params in page components

Link component & navigation

📄 Codetypescript
'use client'

import Link from 'next/link'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'

export default function NavigationExample() {
  const router = useRouter()
  const pathname = usePathname()
  const searchParams = useSearchParams()

  return (
    <nav>
      {/* Basic Link */}
      <Link href="/about">About</Link>
      
      {/* Dynamic Link */}
      <Link href="/blog/my-post">Blog Post</Link>
      <Link href={`/products/${productId}`}>Product</Link>
      
      {/* With query params */}
      <Link href="/shop?category=electronics">Electronics</Link>
      <Link href={{ pathname: '/shop', query: { category: 'books' } }}>
        Books
      </Link>
      
      {/* Active link styling */}
      <Link 
        href="/dashboard"
        className={pathname === '/dashboard' ? 'active' : ''}
      >
        Dashboard
      </Link>
      
      {/* Programmatic navigation */}
      <button onClick={() => router.push('/login')}>
        Login
      </button>
      
      <button onClick={() => router.back()}>
        Go Back
      </button>
      
      <button onClick={() => router.refresh()}>
        Refresh
      </button>
      
      {/* Prefetch control */}
      <Link href="/heavy-page" prefetch={false}>
        Heavy Page (no prefetch)
      </Link>
    </nav>
  )
}
💡 Link enables client-side navigation
⚡ Prefetches links in viewport automatically
📌 Use router.push() for programmatic navigation
🔥 usePathname() gets current path

Server & Client Components

Server Components (default)

📄 Codetypescript
// Server Component (default) - No 'use client'
// app/products/page.tsx

interface Product {
  id: string
  name: string
  price: number
}

// Can be async and fetch data directly
export default async function ProductsPage() {
  // Direct database access or API calls
  const products = await fetch('https://api.example.com/products', {
    cache: 'force-cache' // Cache strategy
  }).then(res => res.json())

  // Can access environment variables directly
  const secretKey = process.env.SECRET_API_KEY

  // Can import server-only modules
  const db = await import('@/lib/db')
  const users = await db.getUsers()

  return (
    <div>
      <h1>Products</h1>
      {products.map((product: Product) => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>${product.price}</p>
        </div>
      ))}
    </div>
  )
}

// Server Component with children
export async function ServerWrapper({ children }: { children: React.ReactNode }) {
  const data = await fetchData()
  
  return (
    <div>
      <h1>{data.title}</h1>
      {children} {/* Can be Client Components */}
    </div>
  )
}
💡 Components are Server Components by default
⚡ Can fetch data directly with async/await
📌 Reduces JavaScript sent to client
🔥 Cannot use browser APIs or event handlers

Client Components

📄 Codetypescript
'use client' // This directive makes it a Client Component

import { useState, useEffect } from 'react'

interface ClientComponentProps {
  initialCount?: number
  serverData: string // Props from Server Component
}

export default function InteractiveCounter({ 
  initialCount = 0,
  serverData 
}: ClientComponentProps) {
  const [count, setCount] = useState(initialCount)
  const [isClient, setIsClient] = useState(false)

  // Can use React hooks
  useEffect(() => {
    setIsClient(true)
    
    // Can access browser APIs
    console.log(window.location.href)
    localStorage.setItem('count', count.toString())
  }, [count])

  // Can use event handlers
  const handleClick = () => {
    setCount(prev => prev + 1)
  }

  return (
    <div>
      <p>Server Data: {serverData}</p>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
      
      {/* Conditional rendering for client-only features */}
      {isClient && (
        <p>Window width: {window.innerWidth}px</p>
      )}
    </div>
  )
}

// Composing Server and Client Components
// app/page.tsx (Server Component)
import ClientCounter from './ClientCounter'

export default async function Page() {
  const data = await fetchServerData()
  
  return (
    <div>
      <h1>Server Rendered Title</h1>
      {/* Pass server data to Client Component */}
      <ClientCounter serverData={data} />
    </div>
  )
}
💡 Add "use client" directive at top
⚡ Can use hooks, browser APIs, event handlers
📌 Still pre-rendered on server (SSR)
🟢 Import Server Components as children, not directly

When to use Server vs Client

📄 Codetypescript
// USE SERVER COMPONENTS WHEN:
// ✅ Fetching data
// ✅ Accessing backend resources directly
// ✅ Keeping sensitive info on server (API keys, etc)
// ✅ Large dependencies (reduces client bundle)
// ✅ Static content without interactivity

// Server Component Example
async function UserProfile({ userId }: { userId: string }) {
  // Direct database access
  const user = await db.user.findUnique({ where: { id: userId } })
  
  // Access sensitive environment variables
  const apiKey = process.env.SECRET_API_KEY
  
  // Import heavy libraries only on server
  const bcrypt = await import('bcrypt')
  
  return <div>{user.name}</div>
}

// USE CLIENT COMPONENTS WHEN:
// ✅ onClick, onChange, other event handlers
// ✅ useState, useEffect, other React hooks
// ✅ Browser-only APIs (window, document, localStorage)
// ✅ Class components (if needed)
// ✅ Third-party libs that use browser APIs

// Client Component Example
'use client'

function SearchBar() {
  const [query, setQuery] = useState('')
  
  // Need event handler
  const handleSearch = (e: React.FormEvent) => {
    e.preventDefault()
    // Search logic
  }
  
  // Need browser API
  useEffect(() => {
    const saved = localStorage.getItem('lastSearch')
    if (saved) setQuery(saved)
  }, [])
  
  return (
    <form onSubmit={handleSearch}>
      <input 
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
    </form>
  )
}
💡 Default to Server Components
⚡ Client for interactivity and browser APIs
📌 Server for data fetching and backend access
🔥 Keep Client Components small and focused

Data Fetching

Fetching in Server Components

📄 Codetypescript
// Simple fetch in Server Component
async function ProductsPage() {
  const res = await fetch('https://api.example.com/products')
  const products = await res.json()
  
  return (
    <div>
      {products.map(product => (
        <div key={product.id}>{product.name}</div>
      ))}
    </div>
  )
}

// Parallel fetching
async function DashboardPage() {
  // Fetch in parallel for better performance
  const [users, posts, comments] = await Promise.all([
    fetch('/api/users').then(res => res.json()),
    fetch('/api/posts').then(res => res.json()),
    fetch('/api/comments').then(res => res.json()),
  ])
  
  return (
    <div>
      <UsersList users={users} />
      <PostsList posts={posts} />
      <CommentsList comments={comments} />
    </div>
  )
}

// Sequential when data depends on each other
async function UserPostsPage({ userId }: { userId: string }) {
  // First fetch user
  const user = await fetch(`/api/users/${userId}`).then(res => res.json())
  
  // Then fetch their posts
  const posts = await fetch(`/api/users/${userId}/posts`).then(res => res.json())
  
  return (
    <div>
      <h1>{user.name}'s Posts</h1>
      <PostsList posts={posts} />
    </div>
  )
}
💡 Use async/await directly in components
⚡ Automatic request deduplication
📌 Requests dedupe per render; opt in to caching (force-cache/revalidate)
🟢 Can fetch in parallel with Promise.all

Caching strategies

typescript
// Static data - opt IN to caching (uncached by default since v15)
💡 force-cache: opt IN (fetch is no-store by default since v15)
⚡ no-store: always fresh - the default since v15
📌 revalidate: Time-based revalidation
🔥 on-demand revalidation with revalidatePath/Tag

Loading & streaming

📄 Codetypescript
// app/products/loading.tsx
export default function Loading() {
  return (
    <div className="animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>
      <div className="grid grid-cols-3 gap-4">
        {[...Array(6)].map((_, i) => (
          <div key={i} className="h-40 bg-gray-200 rounded"></div>
        ))}
      </div>
    </div>
  )
}

// Using Suspense for parts of the page
import { Suspense } from 'react'

export default function ProductPage() {
  return (
    <div>
      <h1>Products</h1>
      
      <Suspense fallback={<div>Loading products...</div>}>
        <ProductList /> {/* Async Server Component */}
      </Suspense>
      
      <Suspense fallback={<div>Loading reviews...</div>}>
        <Reviews /> {/* Another async component */}
      </Suspense>
    </div>
  )
}

// Streaming with generateMetadata
export async function generateMetadata(
  { params }: { params: Promise<{ id: string }> }
) {
  // This runs in parallel with the page
  const { id } = await params
  const product = await getProduct(id)
  
  return {
    title: product.name,
    description: product.description,
  }
}

// Progressive enhancement with loading boundaries
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <nav>Static Navigation</nav>
      
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar /> {/* Async component */}
      </Suspense>
      
      <main>
        <Suspense fallback={<ContentSkeleton />}>
          {children}
        </Suspense>
      </main>
    </div>
  )
}
💡 loading.tsx shows while data loads
⚡ Suspense for granular loading states
📌 Streaming allows progressive rendering
🟢 Skeleton UI improves perceived performance

Server Actions

Creating Server Actions

📄 Codetypescript
// app/actions.ts - Separate file for actions
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'

// Simple server action
export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string
  
  // Validate input
  if (!title || !content) {
    throw new Error('Title and content are required')
  }
  
  // Save to database
  const post = await db.post.create({
    data: { title, content }
  })
  
  // Revalidate cache
  revalidatePath('/posts')
  
  // Redirect
  redirect(`/posts/${post.id}`)
}

// With validation using Zod
const CreatePostSchema = z.object({
  title: z.string().min(1).max(100),
  content: z.string().min(10),
  published: z.boolean().default(false),
})

export async function createPostValidated(formData: FormData) {
  const validatedFields = CreatePostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
    published: formData.get('published') === 'true',
  })
  
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }
  
  const post = await db.post.create({
    data: validatedFields.data
  })
  
  revalidatePath('/posts')
  redirect(`/posts/${post.id}`)
}

// Inline server action in Server Component
async function ServerComponent() {
  async function deletePost(id: string) {
    'use server'
    
    await db.post.delete({ where: { id } })
    revalidatePath('/posts')
  }
  
  return <form action={deletePost}>...</form>
}
💡 Functions that run on server, called from client
⚡ Replace API routes for mutations
📌 Use "use server" directive
🔥 Automatically handle form submissions

Using Server Actions in forms

📄 Codetypescript
// Simple form with Server Action
import { createPost } from '@/app/actions'

export default function CreatePostForm() {
  return (
    <form action={createPost}>
      <input
        type="text"
        name="title"
        placeholder="Post title"
        required
      />
      <textarea
        name="content"
        placeholder="Post content"
        required
      />
      <button type="submit">Create Post</button>
    </form>
  )
}

// With pending state
'use client'

import { useFormStatus } from 'react-dom'
import { createPost } from '@/app/actions'

function SubmitButton() {
  const { pending } = useFormStatus()
  
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Creating...' : 'Create Post'}
    </button>
  )
}

export default function FormWithStatus() {
  return (
    <form action={createPost}>
      <input name="title" />
      <textarea name="content" />
      <SubmitButton />
    </form>
  )
}

// With form state and validation
'use client'

import { useActionState } from 'react'
import { createPostValidated } from '@/app/actions'

const initialState = {
  errors: {},
  message: null,
}

export default function FormWithValidation() {
  const [state, formAction] = useActionState(createPostValidated, initialState)
  
  return (
    <form action={formAction}>
      <div>
        <input name="title" />
        {state?.errors?.title && (
          <p className="error">{state.errors.title}</p>
        )}
      </div>
      
      <div>
        <textarea name="content" />
        {state?.errors?.content && (
          <p className="error">{state.errors.content}</p>
        )}
      </div>
      
      <button type="submit">Create Post</button>
    </form>
  )
}
💡 Works with native form action attribute
⚡ Progressive enhancement without JS
📌 useFormStatus for pending states
🟢 useActionState for form state management

Client-side Server Actions

typescript
'use client'
💡 Call Server Actions from Client Components
⚡ Use with onClick handlers
📌 Handle loading and error states
🔥 Optimistic updates for better UX

API Routes

Route handlers

📄 Codetypescript
// app/api/hello/route.ts
import { NextRequest, NextResponse } from 'next/server'

// GET request
export async function GET(request: NextRequest) {
  return NextResponse.json({ message: 'Hello World' })
}

// POST request
export async function POST(request: NextRequest) {
  const body = await request.json()
  
  // Process the data
  const result = await processData(body)
  
  return NextResponse.json(result, { status: 201 })
}

// Other HTTP methods
export async function PUT(request: NextRequest) {
  const body = await request.json()
  return NextResponse.json({ updated: true })
}

export async function DELETE(request: NextRequest) {
  return NextResponse.json({ deleted: true })
}

export async function PATCH(request: NextRequest) {
  return NextResponse.json({ patched: true })
}

// With headers and cookies
export async function GET(request: NextRequest) {
  // Read headers
  const token = request.headers.get('authorization')
  
  // Read cookies
  const theme = request.cookies.get('theme')
  
  // Set response headers
  return NextResponse.json(
    { data: 'example' },
    {
      status: 200,
      headers: {
        'Set-Cookie': 'token=abc123; Path=/; HttpOnly',
        'Cache-Control': 'no-cache',
      },
    }
  )
}
💡 Create API endpoints with route.ts files
⚡ Support all HTTP methods
📌 GET is dynamic (uncached) by default since v15; opt in via export const dynamic = force-static
🔥 Full access to Request/Response APIs

Dynamic route handlers

typescript
// app/api/posts/[id]/route.ts
💡 Access route params in API routes
⚡ Search params available via URL
📌 Combine with database queries
🟢 Type-safe with TypeScript

Middleware

Creating middleware

📄 Codetypescript
// proxy.ts (Next.js 16 - was middleware.ts; in root or src/)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  // Clone the request headers
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-custom-header', 'my-value')
  
  // Create response with modified headers
  const response = NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  })
  
  // Set response headers
  response.headers.set('x-response-header', 'value')
  
  return response
}

// Configure which paths proxy runs on
export const config = {
  matcher: [
    // Match all paths except static files and api
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
    // Or specific paths
    '/dashboard/:path*',
    '/api/:path*',
  ],
}
💡 proxy runs before every request - nodejs runtime, no edge
⚡ Place proxy.ts (was middleware.ts) in root or src/
📌 Can modify request/response
🟢 Use for auth, redirects, headers

Authentication middleware

typescript
// proxy.ts (Next.js 16 - was middleware.ts) - Authentication example
💡 Protect routes with auth checks
⚡ Redirect unauthenticated users
📌 Work with JWT tokens
🔥 Integration with auth libraries

Image Optimization

Next.js Image component

📄 Codetypescript
import Image from 'next/image'

// Basic usage with local image
import heroImg from '@/public/hero.jpg'

export default function Hero() {
  return (
    <Image
      src={heroImg}
      alt="Hero image"
      // width and height are auto-detected for local images
      priority // Load eagerly (for above-fold images)
    />
  )
}

// Remote images need dimensions
export function RemoteImage() {
  return (
    <Image
      src="https://example.com/photo.jpg"
      alt="Remote photo"
      width={800}
      height={600}
      className="rounded-lg"
    />
  )
}

// Responsive with fill
export function ResponsiveImage() {
  return (
    <div className="relative w-full h-[400px]">
      <Image
        src="/banner.jpg"
        alt="Banner"
        fill
        style={{ objectFit: 'cover' }}
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      />
    </div>
  )
}

// With blur placeholder
import myImage from '@/public/photo.jpg'

export function BlurImage() {
  return (
    <Image
      src={myImage}
      alt="Photo"
      placeholder="blur" // Auto-generated for local images
      // blurDataURL={customBlurDataURL} // For remote images
    />
  )
}
💡 Automatic optimization and lazy loading
⚡ Responsive images with srcset
📌 Prevents layout shift with dimensions
🔥 Supports blur placeholder

Deployment

Vercel deployment

📄 Codebash
# Install Vercel CLI
npm i -g vercel

# Deploy to Vercel
vercel

# Deploy to production
vercel --prod

# Link to existing project
vercel link

# Set environment variables
vercel env add DATABASE_URL

# View deployment logs
vercel logs

# Set up domains
vercel domains add example.com
💡 Zero-config deployment for Next.js
⚡ Automatic CI/CD from Git
📌 Preview deployments for PRs
🟢 Edge functions and analytics

Environment variables

📄 Codetypescript
// .env.local (git ignored)
DATABASE_URL=postgresql://localhost:5432/myapp
API_SECRET=secret_key_here

// Public variables (exposed to client)
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX

// env.ts - Type-safe environment variables
import { z } from 'zod'

const envSchema = z.object({
  // Server-side variables
  DATABASE_URL: z.string().url(),
  API_SECRET: z.string().min(1),
  NODE_ENV: z.enum(['development', 'production', 'test']),
  
  // Client-side variables
  NEXT_PUBLIC_API_URL: z.string().url(),
  NEXT_PUBLIC_GA_ID: z.string().optional(),
})

// Validate at build time
const env = envSchema.parse({
  DATABASE_URL: process.env.DATABASE_URL,
  API_SECRET: process.env.API_SECRET,
  NODE_ENV: process.env.NODE_ENV,
  NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  NEXT_PUBLIC_GA_ID: process.env.NEXT_PUBLIC_GA_ID,
})

export default env

// Usage in server components
import env from '@/lib/env'

async function getData() {
  const response = await fetch(env.NEXT_PUBLIC_API_URL + '/data', {
    headers: {
      'Authorization': `Bearer ${env.API_SECRET}`
    }
  })
  return response.json()
}
💡 .env.local for local development
⚡ NEXT_PUBLIC_ prefix for client-side
📌 Runtime vs build-time variables
🟢 Validation with zod or t3-env

Performance Optimization

Bundle optimization

📄 Codetypescript
// next.config.js - Bundle analyzer
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer({
  // Your config
})

// Run: ANALYZE=true npm run build

// Dynamic imports for code splitting
import dynamic from 'next/dynamic'

// Lazy load heavy component
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <div>Loading chart...</div>,
  ssr: false, // Disable SSR for client-only components
})

// Conditional loading
export function Dashboard() {
  const [showChart, setShowChart] = useState(false)
  
  return (
    <div>
      <button onClick={() => setShowChart(true)}>
        Show Chart
      </button>
      {showChart && <HeavyChart />}
    </div>
  )
}

// Dynamic import in event handler
export function MyComponent() {
  const handleClick = async () => {
    const { processData } = await import('@/lib/heavy-processing')
    const result = await processData()
    console.log(result)
  }
  
  return <button onClick={handleClick}>Process</button>
}
💡 Analyze bundle with @next/bundle-analyzer
⚡ Code splitting happens automatically
📌 Dynamic imports for lazy loading
🔥 Tree shaking removes unused code

Advanced Routing & Modern Features

Parallel/Intercepting routes, PPR, instrumentation, after(), revalidation

Parallel Routes (@slots)

Render multiple pages in the same layout in parallel

tsx
// app/dashboard/layout.tsx
export default function Layout({
  children,
  team,
  analytics,
}: {
  children: React.ReactNode
  team: React.ReactNode
  analytics: React.ReactNode
}) {
  return (
    <>
      {children}
      <div className="grid grid-cols-2">
        {team}
        {analytics}
      </div>
    </>
  )
}

// app/dashboard/@team/page.tsx     → renders into {team}
// app/dashboard/@analytics/page.tsx → renders into {analytics}
// app/dashboard/page.tsx           → renders into {children}
💡 Each slot is an independent route tree with its own loading/error
📌 default.tsx is required on slots once you start navigating — else 404
⚡ Great for dashboards: independent loading states per panel
🎯 Pair with intercepting routes to ship URL-addressable modals
routingapp-routermodern

Intercepting Routes (Modals)

Catch a route inside the current layout — perfect for modals

tsx
// app/feed/page.tsx
<Link href="/photo/123">Open</Link>

// app/photo/[id]/page.tsx        ← full page on hard nav / refresh
// app/feed/(..)photo/[id]/page.tsx ← modal on client nav from /feed

export default function PhotoModal({ params }: { params: { id: string } }) {
  return (
    <Modal>
      <Photo id={params.id} />
    </Modal>
  )
}
💡 Same URL renders as modal from /feed and full page on refresh
⚡ (.), (..), (..)(..), (...) are the segment-level interception matchers
📌 Almost always paired with a parallel @modal slot for clean layering
🎯 Shareable + bookmarkable modals — the killer Next.js routing pattern
routingmodalsapp-router

Partial Prerendering (PPR)

Static shell + streamed dynamic holes in the same response

tsx
// next.config.ts  (Next.js 16 - PPR via Cache Components)
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
  cacheComponents: true, // replaces experimental.ppr / dynamicIO / useCache
}
export default nextConfig

// app/product/[id]/page.tsx  (no per-route flag needed in 16)
import { Suspense } from 'react'

export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  return (
    <main>
      <ProductDetails id={id} />          {/* prerendered shell */}
      <Suspense fallback={<CartFallback />}>
        <Cart />                          {/* dynamic hole, streamed in */}
      </Suspense>
    </main>
  )
}
💡 Static shell from the CDN + dynamic holes streamed in one response
📌 Requires <Suspense> around every dynamic chunk - else falls back to full dynamic
⚡ Enabled by cacheComponents: true in 16 (experimental_ppr was removed)
🎯 Killer for product pages, dashboards, anywhere you mix shared + personal UI
pprperformanceexperimental

instrumentation.ts

Hook into the Node/edge runtime — register OTel, init clients, log errors

typescript
// instrumentation.ts  (project root, sibling to next.config.ts)
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    await import('./instrumentation-node')
  }
}

export async function onRequestError(
  err: unknown,
  request: { path: string; method: string },
  context: { routerKind: 'Pages Router' | 'App Router' }
) {
  await fetch('https://logs.example.com/ingest', {
    method: 'POST',
    body: JSON.stringify({ err: String(err), request, context }),
  })
}
💡 register() runs once per process — perfect for OTel/Sentry/DB warmup
📌 onRequestError catches unhandled errors across RSC, actions, handlers, middleware
⚡ Split Node vs Edge with NEXT_RUNTIME for runtime-specific deps
🎯 Pair with `@vercel/otel` for a one-line OpenTelemetry setup
observabilitymodern

after() & Revalidation Helpers

Run work after the response + cache invalidation API

typescript
import { after } from 'next/server'
import { revalidateTag, revalidatePath } from 'next/cache'

export async function POST(req: Request) {
  const data = await req.json()
  const post = await db.post.create({ data })

  // Work that should NOT block the response:
  after(async () => {
    await sendEmail(post.authorEmail, 'Your post is live')
    await track('post.created', { id: post.id })
  })

  // Targeted cache invalidation:
  revalidateTag(`posts:author:${post.authorId}`)
  revalidatePath('/feed')

  return Response.json(post)
}
💡 after() runs callbacks post-response — no impact on user latency
📌 revalidateTag pairs with `fetch(url, { next: { tags: [...] } })`
⚡ revalidatePath busts a page/layout; revalidateTag is surgical
🎯 Use after() for emails, analytics, and webhook fan-out after mutations
cachingserver-actionsmodern