Next.js Fundamentals

App Router, Server Components & Core Patterns

Caterpillar logo

Next.js App Router

File-based Routing System

  • Convention over Configuration: File structure defines routes automatically
  • Special Files: page.tsx, layout.tsx, loading.tsx, error.tsx, route.ts
  • Nested Layouts: Persistent UI across route changes with automatic deduplication
  • Route Groups: Organize routes without affecting URL structure using (folder) syntax
  • Colocated Files: Keep components, styles, and tests next to your pages
The App Router uses React Server Components by default, enabling better performance, smaller bundles, and improved SEO.

Basic Routing

File-based URL Mapping

Next.js uses a file-based routing system where your folder structure directly maps to URL paths. Each folder with a page.tsx file creates a route.

app/
├── page.tsx                    # Homepage at /
├── about/
│   └── page.tsx                # /about
├── contact/
│   └── page.tsx                # /contact
├── products/
│   ├── page.tsx                # /products
│   └── [id]/
│       └── page.tsx            # /products/123, /products/abc
└── api/
    └── users/
        └── route.ts            # API endpoint at /api/users
Key Concepts: Folders create URL segments, page.tsx files make them accessible, and square brackets [id] create dynamic routes.

Server vs Client: Where Your Code Runs

In modern web development, your code runs in two distinct environments. Understanding these environments is crucial for building effective Next.js applications.

Key Concept: The server and client are completely separate environments with different capabilities, security models, and performance characteristics.

The Server Environment:

  • Location: Runs on Vercel's servers, your hosting provider, or your local development machine
  • Capabilities: Full access to databases, file systems, environment variables, and server-only libraries
  • Security: Can safely handle secrets, API keys, and sensitive operations
  • Performance: Powerful computing resources, but adds network latency for dynamic requests

The Client Environment (Browser):

  • Location: Runs on your users' devices - phones, laptops, tablets
  • Capabilities: Access to browser APIs, user interactions, real-time updates
  • Security: Everything is visible to users - no secrets allowed
  • Performance: Limited by user's device, but no network latency for local operations
// Runs on server - has full system access
export default async function ServerComponent() {
  // ✅ Direct database access
  const users = await db.user.findMany()
  
  // ✅ Access to all environment variables
  const apiKey = process.env.SECRET_API_KEY
  
  // ✅ File system operations
  const config = await readFile('./config.json')
  
  // ✅ Server-only libraries
  const encrypted = encrypt(sensitiveData, apiKey)
  
  return <UserList users={users} />
}

Server Environment Deep Dive

Understanding Server-Side Limitations

The server environment is powerful but has important limitations. Understanding these helps you avoid common mistakes that can break your application.

Critical Limitation: The server doesn't know what domain your application is running on during build time or in serverless functions.

Why relative URLs fail on the server:

  • No base URL context: The server doesn't know if it's running on localhost:3000, vercel.app, or your custom domain
  • Build-time execution: During static generation, there's no incoming request to provide URL context
  • Serverless limitations: Functions run in isolation without knowledge of the broader application context

What happens when you use relative URLs:

export async function generateMetadata({ params }) {
  const { slug } = await params
  
  // ❌ FAILS: Server doesn't know the base URL
  const post = await fetch(`/api/posts/${slug}`)
  // Error: fetch failed (missing protocol/host)
  
  return { title: post.title }
}

Server environment advantages:

  • Security: Keep sensitive operations and data away from users
  • Performance: Powerful computing resources for heavy operations
  • SEO: Content is in the HTML when search engines crawl your site
  • Caching: Server-side caching reduces database load

Client Environment Deep Dive

Leveraging Browser Capabilities

The client environment (browser) offers unique capabilities that the server cannot provide. Understanding these capabilities allows you to build rich, interactive user experiences.

Key Insight: The client environment runs on millions of different devices with varying capabilities, network conditions, and security constraints.

Browser-Exclusive Capabilities:

  • Device APIs: Camera, microphone, GPS, accelerometer, and other hardware sensors
  • Local Storage: Persistent data storage that survives browser sessions
  • Real-time Communication: WebSockets, Server-Sent Events, WebRTC
  • Performance APIs: Measure page load times, browser resource usage, and user interactions
  • Background Processing: Web Workers, Service Workers for offline functionality
'use client'

import { useState, useEffect } from 'react'

export default function DeviceCapabilities() {
  const [location, setLocation] = useState(null)
  const [online, setOnline] = useState(true)
  
  useEffect(() => {
    // ✅ GPS access
    navigator.geolocation.getCurrentPosition(
      (position) => setLocation(position.coords),
      (error) => console.error('Location access denied')
    )
    
    // ✅ Network status monitoring
    const handleOnline = () => setOnline(navigator.onLine)
    window.addEventListener('online', handleOnline)
    window.addEventListener('offline', handleOnline)
    
    // ✅ Device capabilities detection
    if ('serviceWorker' in navigator) {
      // Register service worker for offline support
    }
    
    return () => {
      window.removeEventListener('online', handleOnline)
      window.removeEventListener('offline', handleOnline)
    }
  }, [])
  
  return (
    <div>
      <p>Status: {online ? 'Online' : 'Offline'}</p>
      {location && (
        <p>Location: {location.latitude}, {location.longitude}</p>
      )}
    </div>
  )
}

Client Environment Security Constraints:

  • No Secrets: All code and data is visible to users through browser dev tools
  • CORS Restrictions: Limited cross-origin requests for security
  • Sandboxed Execution: Cannot access local file system or run system commands
  • User Permissions: Many APIs require explicit user consent (camera, location, etc.)

Performance Considerations:

  • Bundle Size: All client code is downloaded by users, affecting page load times
  • Device Variability: Must work on slow phones and fast desktops
  • Network Conditions: Handle slow or unreliable internet connections
  • Battery Impact: Heavy computation drains mobile device batteries
Best Practice: Use Client Components selectively and only when you need browser-specific features or user interactions.

Server vs Client Components

The Foundation of Next.js App Router

In Next.js App Router, all components are Server Components by default. This is a major shift from Pages Router where everything was client-side.

Think of Server Components as "backend components" that run on the server before sending HTML to the browser.

Server Components are perfect for:

  • Fetching data from databases or APIs
  • Rendering static content like blog posts
  • Accessing server-only libraries and environment variables
  • SEO-critical content that needs to be in the initial HTML

Client Components handle interactivity:

  • User interactions (clicks, form submissions)
  • React hooks (useState, useEffect)
  • Browser APIs (localStorage, window)
  • Dynamic, interactive features
// No 'use client' - Server Component by default
import { db } from '@/lib/db'

export default async function BlogPosts() {
  // Direct database access on server
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' },
    take: 10
  })
  
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  )
}

Key Differences:

  • Server: No 'use client', direct database access, no hooks
  • Client: Requires 'use client', uses hooks, handles interactions

When to Choose Server vs Client Components

Making the Right Decision

One of the most important decisions in Next.js is choosing between Server and Client Components. This choice affects performance, user experience, and how your code runs.

Key Insight: Server Components run only on the server. Client Components are prerendered to HTML on the server, then hydrate and become interactive in the browser.

Understanding where code executes:

  • Server Components: Execute only on the server during build or request time - never sent to the browser
  • Client Components: Prerendered as HTML on the server (without state/effects), then "hydrate" in the browser to become interactive

Server Components are ideal for:

  • Fetching initial page data: Direct access to databases and APIs without exposing secrets
  • Static content rendering: No interactivity needed, better for SEO
  • Database operations: Keep database connections secure on server
  • Heavy computations: Don't slow down user's device
export default async function ProductPage() {
  const products = await getProducts()

  return <ProductList products={products} />
}

Client Components are needed for:

  • Browser APIs: geolocation, localStorage, etc. - these APIs only exist in browsers
  • User interactions: Event handlers that need to respond to user actions immediately
  • React hooks: useState, useEffect, etc. - hooks manage state and lifecycle in the browser
  • Real-time features: WebSocket connections and continuous updates
'use client'

export default function LocationTracker() {
  const [location, setLocation] = useState(null)
  
  useEffect(() => {
    navigator.geolocation.getCurrentPosition(setLocation)
  }, [])
  
  return <div>Location: {location}</div>
}

Performance implications:

  • Server Components: Smaller JavaScript bundles, faster initial page loads
  • Client Components: Enable interactivity but increase bundle size
Best Practice: Start with Server Components by default. Only add 'use client' when you need browser APIs, event handlers, or React hooks.

Data Fetching in Server Components

The Power and Pitfalls of Server-Side Data

Server Components can fetch data directly on the server, which is incredibly powerful. But there's a common mistake that can severely impact performance.

Performance Killer: Sequential data fetching can make your pages extremely slow!

Let's see the problem in action:

export default async function PostPage({ params }) {
  const { slug } = await params
  // ❌ These requests wait for each other!
  const post = await getPost(slug)
  const comments = await getComments(slug)
  const author = await getAuthor(post.authorId)
  
  // If each request takes 200ms:
  // Total time: 200ms + 200ms + 200ms = 600ms!
  
  return <PostContent post={post} comments={comments} author={author} />
}

Why Promise.all() is recommended:

  • Fetches multiple data sources simultaneously
  • Dramatically reduces total loading time
  • Better user experience with faster page loads
  • More efficient server resource usage
  • Essential for production applications

Client-Server Communication

Server Actions & Form Handling

Server Actions provide a powerful way to handle form submissions and server-side logic without building separate API routes. They seamlessly integrate with forms and provide excellent user experience with progressive enhancement.

Key Concept: Server Actions run on the server but can be called directly from Client Components, bridging the gap between client and server seamlessly.

Benefits of Server Actions:

  • No API routes needed: Handle form submissions directly in server functions
  • Progressive enhancement: Forms work even with JavaScript disabled
  • Type safety: Full TypeScript support between client and server
  • Built-in security: Automatic CSRF protection and validation

Basic Server Action: Create server functions that handle form data and business logic. The 'use server' directive marks functions that run on the server.

'use server'

export async function submitContactForm(formData: FormData) {
  const email = formData.get('email') as string
  const message = formData.get('message') as string
  
  if (!email || !message) {
    return { error: 'Email and message are required' }
  }
  
  try {
    await sendEmail({ email, message })
    return { success: 'Message sent successfully!' }
  } catch (error) {
    return { error: 'Failed to send message' }
  }
}

Enhanced experience with React hooks: Use useActionState and useFormStatus to add loading states, error handling, and better UX while maintaining the progressive enhancement benefits.

'use client'

import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'
import { submitContactForm } from './actions'

function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Sending...' : 'Send Message'}
    </button>
  )
}

export default function ContactForm() {
  const [state, formAction] = useActionState(submitContactForm, null)

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <textarea name="message" required />
      <SubmitButton />

      {state?.error && (
        <div className="error">{state.error}</div>
      )}
      {state?.success && (
        <div className="success">{state.success}</div>
      )}
    </form>
  )
}
Best Practice: Use Server Actions for form submissions and data mutations. They provide better security, type safety, and user experience than traditional API routes.

Error Handling Strategies

Building Resilient Applications

Errors are inevitable in web applications. Next.js provides powerful tools to handle errors gracefully and provide great user experiences even when things go wrong.

Key Concept: Error boundaries in Next.js work at the route segment level, allowing you to isolate errors and show appropriate fallback UI.

Two main approaches to error handling:

  • Error boundaries (error.tsx): Handle runtime errors in Server and Client Components
  • Not found pages (not-found.tsx): Handle missing resources with custom UI

Error boundaries create nested fallback levels: Errors "bubble up" to the nearest error.tsx file, allowing you to provide specific error experiences for different parts of your app. All error boundary components must be Client Components.

'use client'

export default function ErrorPage({ error, reset }) {
  return (
    <div className="error-page">
      <h1>Application Error</h1>
      <p>Something went wrong with the application</p>
      <button onClick={reset}>Try Again</button>
    </div>
  )
}

Not Found handling: When content doesn't exist, use the notFound() function to trigger 404 pages. This provides better UX than generic error messages and proper SEO with 404 status codes.

import { notFound } from 'next/navigation'

export default async function BlogPost({ params }) {
  const { slug } = await params
  const post = await getBlogPost(slug)
  
  if (!post) {
    notFound()
  }
  
  return <PostContent post={post} />
}
Best Practice: Always provide helpful error messages and clear recovery paths. Users should never see generic "something went wrong" without guidance on what to do next.

Dynamic Routes Deep Dive

Building Flexible URL Structures

Dynamic routes allow you to create pages that respond to variable URL segments. This is essential for building scalable applications with user-generated content.

Key Concept: Square brackets [slug] in file names create dynamic segments that capture URL parts as parameters.

Common dynamic route shapes: single segment and catch-all paths. The next slide compares all three bracket patterns, including optional catch-alls.

  • Single segment: Captures one URL segment (e.g., /blog/my-post)
  • Catch-all: Captures multiple segments (e.g., /docs/api/auth/setup)
export default async function BlogPost({ params }) {
  const { slug } = await params
  const post = await getBlogPost(slug)
  
  return (
    <div>
      <h1>{post.title}</h1>
      <p>Post slug: {slug}</p>
      <div>{post.content}</div>
    </div>
  )
}

Catch-all routes example: Perfect for documentation sites and nested categories.

export default async function DocsPage({
  params
}: {
  params: Promise<{ sections: string[] }>
}) {
  const { sections } = await params

  const breadcrumbs = sections.map((section, index) => ({
    title: section.charAt(0).toUpperCase() + section.slice(1),
    href: '/docs/' + sections.slice(0, index + 1).join('/')
  }))

  return (
    <div>
      <nav className="breadcrumbs">
        <Link href="/docs">Docs</Link>
        {breadcrumbs.map((crumb) => (
          <span key={crumb.href}>
            <span className="separator"> / </span>
            <Link href={crumb.href}>{crumb.title}</Link>
          </span>
        ))}
      </nav>
      <main>
        <DocsContent sections={sections} />
      </main>
    </div>
  )
}

When to use each pattern:

  • [slug]: Blog posts, product pages, user profiles
  • [...sections]: Documentation sites, file systems, nested categories
Pro Tip: Use TypeScript interfaces to ensure type safety for your params and searchParams objects.
Up next: Reference tables and a live demo for [slug], [...slug], and [[...slug]].

Dynamic Routing Patterns

Three Types of Dynamic Routes

Next.js provides three different dynamic route patterns, each serving specific use cases. Understanding when to use each pattern is crucial for building flexible applications.

export default async function BlogPost({ params }) {
  const { slug } = await params

  return <article>Post: {slug}</article>
}

Pattern Behaviors:

PatternExample URLsParams Value
/blog/[slug]/blog/hello-world{ slug: 'hello-world' }
/docs/[...slug]/docs/guides/routing{ slug: ['guides', 'routing'] }
/shop/[[...slug]]/shop{ slug: [] }
/shop/[[...slug]]/shop/electronics/phones{ slug: ['electronics', 'phones'] }
Key Difference: [...slug] requires at least one segment, while [[...slug]] matches zero or more segments.
Route Pattern:
app/blog/[slug]/page.tsx
✅ Match Found
params = { "slug": "hello-world" }

When to use each pattern:

PatternUse CaseExample
[slug]Single variable segmentBlog posts, user profiles
[...slug]Multiple segments, requires at least oneDocumentation, nested categories
[[...slug]]Multiple segments, including zeroOptional filters, catch-all routes

Loading States & Suspense

Creating Smooth User Experiences

Loading states prevent your users from staring at blank screens. Next.js provides powerful tools for creating smooth, progressive loading experiences.

Key Concept: Instead of showing a blank page while everything loads, show immediate feedback and let content stream in as it becomes available.

Two approaches to loading states:

  • loading.tsx files: Automatic loading UI for entire route segments
  • Suspense boundaries: Granular loading states for specific components
// This file creates an automatic loading UI for the /dashboard route
export default function DashboardLoading() {
  return (
    <div className="dashboard-skeleton">
      <div className="animate-pulse">
        <div className="h-8 bg-gray-200 rounded mb-6 w-48"></div>

        <div className="grid grid-cols-3 gap-6 mb-8">
          {[...Array(3)].map((_, i) => (
            <div key={i} className="h-24 bg-gray-200 rounded"></div>
          ))}
        </div>

        <div className="h-64 bg-gray-200 rounded mb-6"></div>
      </div>
    </div>
  )
}

Component-level loading with Suspense: Use Suspense boundaries to show different loading states for different parts of your page. Fast content appears immediately while slow content streams in.

import { Suspense } from 'react'

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      
      <UserWelcome />
      
      <Suspense fallback={<ChartSkeleton />}>
        <AnalyticsChart />
      </Suspense>
      
      <Suspense fallback={<TableSkeleton />}>
        <RecentActivity />
      </Suspense>
      
      <Suspense fallback={<div>Loading notifications...</div>}>
        <NotificationsList />
      </Suspense>
    </div>
  )
}

async function AnalyticsChart() {
  const analytics = await getAnalyticsData()
  
  return (
    <div className="chart-container">
      <h2>Analytics</h2>
      <Chart data={analytics} />
    </div>
  )
}

async function RecentActivity() {
  const activity = await getRecentActivity()
  
  return (
    <div className="activity-feed">
      <h2>Recent Activity</h2>
      <ActivityList items={activity} />
    </div>
  )
}

function UserWelcome() {
  return (
    <div className="welcome-banner">
      <h2>Welcome back!</h2>
      <p>Here's what's happening today</p>
    </div>
  )
}
Pro Tip: Use loading.tsx for route-level loading and Suspense for granular component loading. Combine both for the best user experience!
Same-page navigation: When only search params change, Suspense boundaries don't re-trigger by default. Add a key prop containing the changing param (e.g. key={searchParams.q}) to force the loading state to re-appear.

Navigation in Next.js

Client-Side vs Server-Side Navigation

One of Next.js's key features is its navigation system. Understanding how it works is crucial for building fast, modern web applications.

When you use next/link, Next.js performs client-side navigation - no full page reloads!

What happens when you click a Next.js Link:

  • JavaScript updates the URL in the browser
  • Next.js fetches only the new page's content
  • The page transitions smoothly without full refresh
  • Shared layouts persist and don't re-render
import Link from 'next/link'

export default function Navigation() {
  return (
    <nav>
      {/* Basic client-side navigation */}
      <Link href="/about">About Us</Link>

      {/* Dynamic route navigation */}
      <Link href={`/blog/${post.slug}`}>
        {post.title}
      </Link>

      {/* Navigation with query parameters */}
      <Link
        href={{
          pathname: '/products',
          query: { category: 'electronics', sort: 'price' }
        }}
      >
        Electronics
      </Link>
    </nav>
  )
}

Programmatic navigation:

'use client'

import { useRouter } from 'next/navigation'

export default function NavigationButton() {
  const router = useRouter()
  
  const handleClick = () => {
    router.push('/dashboard')     // Navigate forward
    router.replace('/login')      // Replace current page
    router.back()                 // Go back in history
    router.refresh()              // Refresh current route
  }
  
  return <button onClick={handleClick}>Navigate</button>
}
Best Practice: Use useRouter for client-side interactions and redirect for server-side navigation.

Special File Conventions

Reference Guide

FilePurposeType
page.tsxRoute's main content
Server/Client
layout.tsxPersistent UI wrapper
Server/Client
loading.tsxLoading UI fallback
Server/Client
error.tsxError boundary UI
Client Only
not-found.tsx404 error page
Server/Client
route.tsAPI endpoint handler
Server Only

Route Groups

Organize Without Affecting URLs

Route Groups help you organize your application without affecting the URL structure. They're created using parentheses (folder) syntax.

Route Groups are perfect for organizing features by team, feature set, or applying different layouts to different sections.
app/
├── (marketing)/             # Route group - no URL impact
│   ├── layout.tsx           # Marketing-specific layout
│   ├── about/page.tsx       # /about
│   └── pricing/page.tsx     # /pricing
│
├── (auth)/                  # Authentication group
│   ├── layout.tsx           # Minimal auth layout
│   ├── login/page.tsx       # /login
│   └── register/page.tsx    # /register
│
└── dashboard/               # Regular folder
    └── page.tsx             # /dashboard

Key Benefits:

  • Organization: Group related routes without changing URLs
  • Multiple Layouts: Apply different layouts to different sections
  • Team Collaboration: Teams can work on separate route groups
  • Feature Flags: Enable/disable entire feature groups easily
Remember: Folders wrapped in parentheses like (auth) don't create URL segments - /login not /(auth)/login

Error Handling in Next.js

Building Resilient Applications

Next.js provides powerful error handling through special files that create error boundaries automatically.

Error boundaries catch JavaScript errors anywhere in the component tree and display a fallback UI instead of crashing the whole page.

Important rule about error.tsx files:

  • They MUST be Client Components (require 'use client')
  • This is because they need to use React's error boundary features
  • They receive props for the error and a reset function
'use client' // ⚠️ REQUIRED: Error boundaries MUST be Client Components

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div className="error-container">
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  )
}

You can create multiple error boundaries for granular error handling:

When an error occurs, Next.js looks for the nearest error.tsx file in the route hierarchy. This allows you to have different error UIs for different sections of your app.

app/
├── error.tsx              # Global fallback
├── dashboard/
│   ├── error.tsx          # Dashboard-specific errors
│   ├── page.tsx
│   └── settings/
│       ├── error.tsx      # Settings-specific errors
│       └── page.tsx

// Error bubbling behavior:
// • Settings page error → settings/error.tsx
// • Other dashboard error → dashboard/error.tsx  
// • Any other error → root error.tsx

Remember these facts about Next.js file conventions:

  • ✅ error.tsx MUST be a Client Component
  • ❌ page.tsx does NOT need to be a Server Component (can be either)
  • ❌ loading.tsx does NOT only work with Server Components

Environment Variables

Secure Configuration Management

Environment variables in Next.js have a special naming convention that determines whether they're accessible on the client-side or server-only.

The key rule: Only variables prefixed with NEXT_PUBLIC_ are accessible in the browser.

Let's see how this works in practice:

# Server-only variables (secure)
DATABASE_URL="postgresql://user:pass@localhost:5432/db"
JWT_SECRET="your-secret-key"
API_SECRET_KEY="sk_live_abc123"

# Client-exposed variables (public)
NEXT_PUBLIC_API_URL="https://api.example.com"
NEXT_PUBLIC_ANALYTICS_ID="GA_TRACKING_ID"
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_xyz789"

Server Components can access ALL environment variables:

// Server Component - runs on server
export default async function ServerComponent() {
  const dbUrl = process.env.DATABASE_URL           // ✅ Available
  const apiUrl = process.env.NEXT_PUBLIC_API_URL   // ✅ Available
  const secret = process.env.JWT_SECRET            // ✅ Available
  
  // Safe to use sensitive data here
  const userData = await fetchFromDatabase(dbUrl)
  
  return <div>Server data loaded securely</div>
}

Client Components can ONLY access NEXT_PUBLIC_ variables:

'use client'

export default function ClientComponent() {
  const dbUrl = process.env.DATABASE_URL           // ❌ undefined!
  const apiUrl = process.env.NEXT_PUBLIC_API_URL   // ✅ Available
  const secret = process.env.JWT_SECRET            // ❌ undefined!
  
  // Only public variables work here
  const handleApiCall = () => {
    fetch(apiUrl + '/data') // This works
  }
  
  return <button onClick={handleApiCall}>Fetch Data</button>
}
Never put sensitive data in NEXT_PUBLIC_ variables! They're visible to anyone who views your website's source code.
DATABASE_URL:"postgresql://user:pass@localhost:5432/db"
JWT_SECRET:"your-secret-key"
NEXT_PUBLIC_API_URL:"https://api.example.com"
NEXT_PUBLIC_ANALYTICS_ID:"GA_TRACKING_ID"
Best Practice: Implement the server-only package to prevent accidental import of server-only code into Client Components, ensuring sensitive data like API keys remain secure through build-time validation.

Next.js Proxy

Runs before pages are rendered

Proxy (called Middleware prior to Next.js 16) is a powerful Next.js feature that lets you run code before a request is completed. Think of it as a gatekeeper that can inspect, modify, or redirect requests.

Here's a practical example of proxy for authentication:

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function proxy(request: NextRequest) {
  // Authentication check
  const token = request.cookies.get('auth-token')
  
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    if (!token) {
      return NextResponse.redirect(
        new URL('/login', request.url)
      )
    }
  }
  
  // Add security headers
  const response = NextResponse.next()
  response.headers.set('X-Frame-Options', 'DENY')
  response.headers.set('X-Content-Type-Options', 'nosniff')
  
  return response
}

// Configure which paths proxy runs on
export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*']
}

This proxy example demonstrates:

  • Authentication: Checks for auth-token cookie
  • Redirects: Sends unauthenticated users to /login
  • Security Headers: Adds protection against common attacks
  • Path Matching: Only runs on /dashboard and /api routes

Common proxy use cases include:

  • Authentication & Authorization: Protect routes and check permissions
  • Request/Response Modification: Add headers, modify data
  • Bot Protection & Rate Limiting: Prevent abuse
  • A/B Testing & Feature Flags: Dynamic feature rollouts
Note: In Next.js 16, the proxy file runs exclusively in the Node.js runtime. If you need Edge runtime support, continue using the middleware.ts file convention.
1 / 19
Slide 1 of 19