Getting Started Last updated: Feb. 28, 2026, 7:22 p.m.

Next.js is a React framework designed for production, offering a streamlined path from initial concept to a scalable web application. To get started, the standard approach is using create-next-app, which sets up a modern environment with TypeScript, ESLint, and Tailwind CSS by default. This setup ensures you have a consistent development experience with built-in support for the App Router, which is the current architectural standard for Next.js applications.

The foundational shift in "Getting Started" with modern Next.js is the move toward Server Components. Unlike traditional React apps where everything happens on the client, Next.js encourages a "Server-First" mindset. This means that as soon as you create your first page in the app/ directory, you are already benefiting from reduced bundle sizes and improved SEO, as the framework handles the heavy lifting on the server before the browser even receives the code.

Installation

The foundation of a Next.js application begins with the automated setup process, which configures the underlying build system, compiler, and directory structure. While manual installation is possible, the recommended approach utilizes the create-next-app CLI tool. This utility ensures that all peer dependencies—specifically react, react-dom, and next—are synchronized to compatible versions. During the installation process, the CLI prompts for configurations such as TypeScript integration, ESLint linting, and the use of Tailwind CSS. Choosing the "App Router" during this setup is critical, as it enables the modern React Server Components (RSC) architecture and the file-system based routing system located in the /app directory.

Automated Setup

To initialize a project, execute the following command in your terminal:

npx create-next-app@latest my-next-app

System Requirements

Before proceeding, ensure your development environment meets the minimum version requirements to avoid runtime compilation errors.

Requirement Minimum Version Note
Node.js 18.17 or later Required for stable App Router support.
Operating System macOS, Windows (WSL), or Linux WSL is highly recommended for Windows users.
Memory 4GB RAM Recommended for optimal build performance.

Note

If you are migrating an existing project, ensure your package.json reflects the latest versions of the three core packages to leverage the latest optimization features.

Project Structure

Next.js employs a standardized project structure that separates application logic from configuration. The most significant shift in the modern standard is the use of the app directory. Unlike the legacy pages directory, every folder in app represents a route segment, and only specific file names (like page.js or layout.js) are publicly accessible. This allows developers to colocate components, tests, and styles directly within the route folders without accidentally exposing them as valid URLs. Furthermore, the public directory remains the dedicated space for static assets that do not require processing by the bundler.

Core Directory Overview

Directory/File Purpose Visibility
app/ Contains all routes, layouts, and server components. Internal/Public (via files)
public/ Static assets like images, fonts, and robots.txt. Publicly accessible
components/ Recommended location for shared, reusable UI elements. Internal
next.config.js Custom configuration for the Next.js compiler. Configuration

Root Layout Example

The app/layout.js file is the entry point for your application's UI. It must contain the and < body >< /body> tags, which are shared across all routes.

// app/layout.js
export const metadata = {
  title: 'My Documentation Site',
  description: 'Generated by Next.js',
}

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <nav>Navigation Header</nav>
        {children}
      </body>
    </html>
  )
}

Warning: You cannot omit the < html> and < body> tags in the Root Layout. Doing so will result in a hydration error because Next.js will not automatically inject these elements.

React Essentials (Server Components)

The App Router is built on React Server Components (RSC), a paradigm shift that allows components to be rendered on the server and cached for better performance. By default, every file inside the app directory is a Server Component. This architecture significantly reduces the amount of JavaScript sent to the client, as the logic for fetching data and rendering complex HTML stays on the server. When interactivity is required—such as handling clicks, managing state with useState, or using browser APIs—developers must explicitly opt-in to Client Components by placing the "use use client" directive at the very top of the file.

Comparing Component Types

Server Components (Default)
FeatureClient Components
Data Fetching Can be async to fetch data directly. Uses useEffect or SWR/Query.
Bundle Size Zero impact on client-side JS. Adds to the client-side JS bundle.
Interactivity No event listeners (onClick, etc.). Full access to event listeners.
Hooks No access to useState or useEffect. Full access to React hooks.

Server Component Implementation

The following example demonstrates a Server Component fetching data directly from an API. Notice the use of async/await directly within the component function, which is not possible in standard Client Components.

// app/products/page.js
async function getProducts() {
  const res = await fetch('https://api.example.com/products');
  return res.json();
}

export default async function Page() {
  const products = await getProducts();

  return (
    <main>
      <h1>Product Catalog</h1>
      <table>
        <thead>
          <tr>
            <th>Name</th>
            <th>Price</th>
          </tr>
        </thead>
        <tbody>
          {products.map((product) => (
            <tr key={product.id}>
              <td>{product.name}</td>
              <td>${product.price}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </main>
  );
}

Client Component Implementation

To add interactivity, such as a search toggle, you must define a Client Component boundary.

"use client"; // This directive identifies the component as a Client Component

import { useState } from 'react';

export default function SearchBar() {
  const [query, setQuery] = useState('');

  return (
    <div>
      <input 
        type="text" 
        value={query} 
        onChange={(e) => setQuery(e.target.value)} 
        placeholder="Search documentation..."
      />
      <p>Searching for: {query}</p>
    </div>
  );
}

Note

A common best practice is to "push" Client Components to the leaf nodes of your component tree. Keep your data-heavy layouts and pages as Server Components and only use Client Components for small pieces of UI that require state or browser APIs.

Routing Last updated: Feb. 28, 2026, 7:25 p.m.

Routing in the App Router is defined by a file-system based hierarchy where folders define paths and special files (like page.js) define the UI. This structure supports advanced patterns like nested routes, where a parent layout remains interactive while child segments swap out, and dynamic routes that capture URL parameters. Because the router is integrated with Server Components, it allows for seamless transitions and instant loading states through the use of loading.js and error.js files at any level of the tree.

Beyond simple navigation, the router includes powerful features like Parallel Routes and Intercepting Routes. These allow developers to show multiple pages in the same view (like a dashboard with multiple independent sections) or load a route in a specific context (like opening a photo in a modal while keeping the background page visible). This architectural flexibility makes it possible to build complex, highly interactive user interfaces that were previously difficult to manage in a standard SPA.

Defining Routes

In the Next.js App Router, routing is handled through a file-system based router where folders are used to define routes. Each folder in the app directory represents a route segment that maps to a URL segment. To create a publicly accessible path, a folder must contain a special page.js (or .tsk) file. If a folder does not contain a page.js file, it acts solely as a namespace for organization and does not have a corresponding URL. This hierarchy allows for deep nesting, enabling developers to build complex URL structures like /dashboard/settings/profile by simply nesting folders.

The routing system supports Nested Routes, where the UI for a child segment is wrapped by the layout of its parent segments. This creates a predictable relationship between the URL and the component tree. For instance, a folder structure of app/blog/[slug]/page.js will render at the URL /blog/my-post, where [slug] serves as a dynamic segment.

Special File Convention

Next.js reserves specific filenames within route segments to handle UI behaviors. These files allow you to define the UI for a specific segment and its children through a declarative approach.

File Name Role Component Type
page.js The unique UI of a route and makes the path publicly accessible. Server (Default)
layout.js Shared UI for a segment and its children (preserves state on navigation). Server (Default)
loading.js Loading UI for a segment and its children (uses React Suspense). Server (Default)
error.js Error UI for a segment and its children (uses React Error Boundaries). Client
not-found.js UI rendered when the notFound function is triggered or a URL doesn't match. Server (Default)

Creating a Basic Route

To create your first route, you simply create a folder and add a page.js file. The following example demonstrates a static "About" page located at /about.

// app/about/page.js

export default function AboutPage() {
  return (
    <section>
      <h1>About Our Project</h1>
      <p>
        This page is automatically mapped to the /about URL because of its 
        location in the file system. The component is rendered on the server 
        by default, ensuring fast First Contentful Paint.
      </p>
    </section>
  );
}

Creating Nested Routes

Nesting is achieved by placing folders inside other folders. The component tree will follow this structure, with layout.js files wrapping the child page.js or nested layouts.

// app/dashboard/settings/page.js
// This file maps to the URL: /dashboard/settings

export default function SettingsPage() {
  return (
    <main>
      <h2>User Settings</h2>
      <p>Manage your account preferences and security configurations here.</p>
    </main>
  );
}

Warning:Every route segment must eventually lead to a page.js file to be accessible. If you create a folder named /services but only include a header.js inside it without a page.js, navigating to /services will result in a 404 error.

Note

Component files (like Button.js) can be placed inside route folders without being accessible as routes. Next.js only recognizes the specific special filenames listed in the table above as route entry points. This is known as Colocation.

Pages and Layouts

In the App Router, the user interface of a route is constructed using a hierarchy of special files. While Pages represent the unique content of a specific URL, Layouts provide a way to share UI across multiple segments while maintaining application state and avoiding unnecessary re-renders. This architectural separation allows developers to build complex, persistent navigation systems and nested interfaces with minimal effort.

Pages

A Page is the UI that is unique to a specific route. It is defined by exporting a default component from a page.js file. Pages are the leaf nodes of your routing tree; they are responsible for displaying the primary content the user requested. In the App Router, Pages are Server Components by default, meaning they can perform asynchronous data fetching directly within the component body before the HTML is sent to the client.

// app/dashboard/page.js
export default async function DashboardPage() {
  // You can fetch data directly in a Server Component Page
  const data = await fetch('https://api.example.com/stats', { cache: 'no-store' });
  const stats = await data.json();

  return (
    <section>
      <h1>Dashboard Overview</h1>
      <p>Welcome back! Here are your latest statistics.</p>
      <div className="stats-grid">
        <p>Active Users: {stats.activeUsers}</p>
      </div>
    </section>
  );
}

Layouts

A Layout is UI that is shared between multiple pages. When navigating between sibling routes (e.g., from /dashboard/settings to /dashboard/analytics), the layout remains static, preserving its internal state (such as search input values or scroll position) and preventing the shared elements from re-rendering.

Layouts are defined by exporting a default React component from a layout.js file. The component must accept a children prop, which Next.js populates with the child segment—this could be a nested layout or a page.

Root Layout (Required)

The Root Layout is the top-most layout in the app directory. It is mandatory for every Next.js application and must define the < html> and < body> tags. This is the ideal place to instantiate global providers, such as Theme Providers or Analytics scripts.

Nested Layouts

Layouts defined inside a folder (e.g., app/dashboard/layout.js) apply to all route segments within that folder. They wrap the child segments automatically, allowing you to create "sub-shells" for specific sections of your application.

// app/dashboard/layout.js
export default function DashboardLayout({ children }) {
  return (
    <div className="dashboard-container">
      <aside className="sidebar">
        <nav>
          <a href="/dashboard">Overview</a>
          <a href="/dashboard/settings">Settings</a>
        </nav>
      </aside>
      <main className="content-area">
        {/* The child page or nested layout will be rendered here */}
        {children}
      </main>
    </div>
  );
}

Key Comparisons: Pages vs. Layouts

Feature Page (page.js) Layout (layout.js)
Purpose Unique UI for a specific route. Shared UI for multiple routes.
Re-rendering Re-renders on every navigation. Does not re-render on navigation between children.
State State is reset on navigation. State is preserved on navigation.
Mandatory Required to make a route public. Only the Root Layout is strictly required.
Children Prop Does not receive children. Must accept and render (children).

Templates

While Layouts persist state, there are rare cases where you want the UI to "reset" every time a user navigates. Templates (template.js) are similar to layouts in that they wrap child segments, but unlike layouts, they create a new instance for each child on navigation. This is useful for CSS animations or useEffect hooks that need to trigger every time a page is viewed.

// app/template.js
export default function Template({ children }) {
  return <div className="animate-in">{children}</div>;
}

Warning: You cannot pass data between a Layout and its children (Pages). If you need the same data in both the Layout and the Page, you should fetch the data in both. Next.js automatically deduplicates fetch requests, so this does not result in a performance penalty.

Note

Only the Root Layout can contain the < html> and < body> tags. If you attempt to include these in a nested layout, Next.js will throw a runtime error during the build process.

Linking and Navigating

In the Next.js App Router, navigation is handled through a hybrid approach that combines server-side routing with client-side transitions. There are two primary ways to navigate between routes: the < Link> component and the useRouter hook. Unlike traditional applications where a link click triggers a full page refresh, Next.js performs Client-side Navigation. This means only the segments that change are re-rendered, while the shared segments (like layouts) remain intact.

Next.js also employs Prefetching, which automatically downloads the code for a linked route in the background before the user even clicks the link. Combined with Caching, this ensures that transitions between pages feel instantaneous.

The < Link Component

The < Link component is a built-in React component that extends the HTML < a> tag to provide declarative, client-side navigation. It is the primary and recommended method for navigating between routes. Because it is a component, it works in both Server and Client Components.

// app/components/Navbar.js
import Link from 'next/link';

export default function Navbar() {
  return (
    <nav>
      {/* Basic navigation */}
      <Link href="/">Home</Link>
      
      {/* Dynamic route navigation */}
      <Link href="/blog/hello-world">Read Post</Link>
      
      {/* Using an object for complex URLs */}
      <Link
        href={{
          pathname: '/search',
          query: { q: 'nextjs' },
        }}
      >
        Search
      </Link>
    </nav>
  );
}

The useRouter Hook

For programmatic navigation—such as redirecting a user after a form submission—you must use the useRouter hook. This hook can only be used inside Client Components. It provides methods to manually trigger transitions, refresh the current route, or go back in the browser history.

"use client";

import { useRouter } from 'next/navigation';

export default function LoginForm() {
  const router = useRouter();

  const handleSubmit = async (e) => {
    e.preventDefault();
    // Logic for authentication...
    
    // Programmatic redirect
    router.push('/dashboard');
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Login</button>
      <button type="button" onClick={() => router.back()}>
        Go Back
      </button>
    </form>
  );
}

Navigation Methods and Behavior

Method Behavior Use Case
<Link href="..."> Client-side transition with prefetching. Standard links, navigation bars, and footers.
router.push(href) Navigates to a new route; adds a new entry to the browser history. Redirects after user actions (e.g., login, checkout).
router.replace(href) Navigates to a new route without adding a new entry to the history stack. Redirects where you don't want the user to go "back."
router.refresh() Refreshes the current route. Requests fresh data from the server. Updating data without losing client-side state.
router.prefetch(href) Manually prefetches a route for faster transitions. Optimizing high-probability future navigations.

How Navigation Works

Next.js optimizes navigation using a sophisticated process that minimizes server requests and maximizes speed.

  1. Prefetching: When a < Link> component enters the user's viewport, Next.js prefetches the route in the background. For static routes, the entire segment is prefetched; for dynamic routes, only the shared layout tree is prefetched.
  2. Caching: Next.js has an in-memory Client-side Cache (Router Cache). Prefetched route segments and previously visited routes are stored here. When navigating, the browser checks this cache first.
  3. Partial Rendering: During navigation, only the route segments that change are re-rendered. For example, when moving from /dashboard/settings to /dashboard/analytics, the shared dashboard/layout.js is preserved, and only the settings/analytics page is swapped.
  4. Soft Navigation: By default, Next.js performs a "soft" navigation, meaning the browser does not reload the page, and React state in the persistent layouts is maintained.

Note

Prefetching is only enabled in production. In development mode, Next.js fetches the route on every click to ensure you see the most recent changes during the coding process.

Warning: The useRouter hook must be imported from next/navigation, not next/router. next/router import is legacy and only works with the older Pages Router.

Loading UI and Streaming

In the Next.js App Router, Loading UI and Streaming are built-in features designed to improve the Perceived Performance of your application. Traditionally, a server must fetch all data for a page before it can render any HTML to the client. This "all-or-nothing" approach often leads to a blank screen or a spinning loader for the entire page. Next.js solves this by leveraging React Suspense, allowing you to show an instant loading state while the content of a route segment is being fetched and rendered on the server.

The loading.js File

The simplest way to implement a loading state is by creating a loading.js file within a route folder. Next.js automatically wraps the route'spage.js file (and any nested children) in a React Suspense Boundary. The UI defined in loading.js is rendered immediately on the first request and during navigation, while the actual page content is "streamed" from the server as it becomes ready.

// app/dashboard/loading.js
export default function DashboardLoading() {
  // This UI will be shown immediately while the dashboard data fetches
  return (
    <div className="shimmer-wrapper">
      <div className="shimmer-title"></div>
      <div className="shimmer-content"></div>
      <div className="shimmer-content"></div>
      <style jsx>{`
        .shimmer-wrapper { padding: 20px; }
        .shimmer-title { height: 32px; width: 200px; background: #eee; margin-bottom: 20px; }
        .shimmer-content { height: 16px; background: #eee; margin-bottom: 10px; }
      `}</style>
    </div>
  );
}

Streaming with Suspense

While loading.js handles the entire page's loading state, you can achieve more granular control using Streaming. Streaming allows you to break down the page's HTML into smaller chunks and progressively send them from the server to the client. This means non-blocking parts of the page (like a navigation bar or header) can appear instantly, while data-heavy components (like a product list) appear as soon as their data resolves.

You can implement this by wrapping specific components in < Suspense> and providing a fallback UI.

// app/dashboard/page.js
import { Suspense } from 'react';
import { PostFeed, WeatherWidget } from './components';

export default function Page() {
  return (
    <section>
      <h1>My Dashboard</h1>
      
      {/* WeatherWidget will stream in separately */}
      <Suspense fallback={<p>Loading Weather...</p>}>
        <WeatherWidget />
      </Suspense>

      {/* PostFeed will stream in separately */}
      <Suspense fallback={<p>Loading Posts...</p>}>
        <PostFeed />
      </Suspense>
    </section>
  );
}

Comparison of Loading Strategies

Strategy Implementation Scope Use Case
loading.js File-based convention Entire route segment General loading states for a specific URL.
<Suspense> Component-based Specific UI components Granular control for multiple independent data fetches.
Static Rendering Default behavior Entire page Instant loads for content that doesn't change per user.

SEO and Rendering

Next.js is designed to be SEO-friendly even when streaming. Because streaming is handled via React's server-side rendering capabilities, the server waits for the Shell (the parts of the page outside of Suspense boundaries) to be ready before starting the stream. Search engines see the initial HTML, and the streamed content is injected into the DOM in a way that modern crawlers can still index.

Key Technical Benefits

  • Time to First Byte (TTFB): Significantly reduced because the server can start sending the header and layouts before the data-heavy page content is ready.
  • First Contentful Paint (FCP): Improved as the user sees meaningful UI (like layouts and navigation) almost immediately.
  • Reduced Server Load: By streaming, the server doesn't have to hold the entire page in memory before sending it.

Note

Navigating between routes will not trigger a full-page reload. If a layout is already rendered, only the content inside the loading boundary of the new segment will show the loading state.

Error Handling

In the Next.js App Router, error handling is treated as a first-class concern through a recovery-oriented architecture. Instead of allowing a single JavaScript error to crash the entire application, Next.js uses Error Boundaries to isolate failures to the specific route segment where they occurred. This allows the rest of the application (such as the global navigation and sidebar) to remain functional while providing the user with an interface to recover from the error.

The error.js File

The error.js file convention allows you to gracefully handle unexpected runtime errors in nested routes. By creating an error.js file, you automatically wrap the associated page.js and all its children in a React Error Boundary. When an error is thrown within that segment, the page.js component is swapped out for the component exported from error.js.

"use client"; // Error components must be Client Components

import { useEffect } from 'react';

export default function Error({ error, reset }) {
  useEffect(() => {
    // Log the error to an error reporting service like Sentry
    console.error(error);
  }, [error]);

  return (
    <div className="error-container">
      <h2>Something went wrong!</h2>
      <p>{error.message || "An unexpected error occurred."}</p>
      <button
        onClick={
          // Attempt to recover by trying to re-render the segment
          () => reset()
        }
      >
        Try again
      </button>
    </div>
  );
}

How Recovery Works

The error.js component receives two primary props: error (the error object itself) and reset. The reset function is a key feature of Next.js error handling; when called, it attempts to re-render the contents of the error boundary. If the re-render is successful, the error component is replaced by the original page content. This is particularly useful for transient errors, such as a temporary network blip during a data fetch.

Error Handling Granularity

The placement of error.js in the file system determines the scope of the error boundary. This allows for a hierarchical "fallback" system.

Placement Scope Impact
app/dashboard/error.js Only errors within /dashboard/*. Sidebar and Root Nav remain interactive.
app/error.js All errors across the entire app. Catch-all for any unhandled route errors.
app/global-error.js Catch-all including the Root Layout. Used to handle errors in the html or body tags.

Handling Errors in Layouts

It is important to understand that error.js handles errors for the children of the layout it is nested in, but it does not catch errors thrown in its sibling layout.js. This is because the error boundary is nested inside the layout component to ensure the layout remains interactive during an error.

To handle errors specifically within a Root Layout, you must use the global-error.js file, which is specifically designed to wrap the entire application, including the < html> and < body> tags.

// app/global-error.js
"use client";

export default function GlobalError({ error, reset }) {
  return (
    <html>
      <body>
        <h2>Critical System Error</h2>
        <button onClick={() => reset()}>Hard Reset</button>
      </body>
    </html>
  );
}

Server-Side vs. Client-Side Errors

Next.js distinguishes between where an error originates to ensure security.

  • Client Components: Full error details, including the stack trace and message, are available to the error.js component.
  • Server Components: During production, Next.js strips sensitive error messages from Server Components to prevent leaking backend data. The error object will contain a generic message and a digest hash, which you can use to find the corresponding full error in your server logs.

Warning: error.js files must be Client Components. If you forget to add the "use client" directive, the error boundary will fail to initialize, and the error will bubble up to the next available boundary or crash the app.

Note

During development, Next.js will show a full-screen "Redbox" overlay with the stack trace for easier debugging. The error.js UI is what users see in the production build.

Route Groups & Dynamic Routes

In the Next.js App Router, advanced routing patterns allow developers to transcend simple folder-to-URL mappings. Route Groups enable organizational flexibility without affecting the URL structure, while Dynamic Routes allow for the creation of programmatic paths based on data, such as IDs or slugs. Understanding these two concepts is essential for building scalable, production-grade applications with complex information architectures.

Route Groups

By default, every folder in the app directory contributes to the URL path. However, you can mark a folder as a Route Group by wrapping its name in parentheses: (folderName). This tells the Next.js router to ignore the folder name when generating the URL.

Route Groups are primarily used for:

  • Organizing routes into logical sections (e.g., (auth), (marketing), (dashboard)) without adding extra segments to the URL.
  • Opting into specific Layouts: You can create multiple Root Layouts or different UI shells for different sets of routes.
// Example Directory Structure:
// app/(marketing)/about/page.js    -> URL: /about
// app/(marketing)/layout.js        -> Marketing-specific layout
// app/(auth)/login/page.js         -> URL: /login
// app/(auth)/layout.js             -> Auth-specific layout (no navbar)

In the example above, even though about is inside (marketing), the URL is simply /about. This allows the "Marketing" layout to wrap the about page without forcing the URL to be /marketing/about.

Dynamic Routes

When you don't know the exact segment names ahead of time (e.g., blog post slugs or product IDs), you can use Dynamic Segments. These are created by wrapping a folder name in square brackets: [slug] or [id]. Dynamic segments are passed as a params prop to the layout.js, page.js, route.js, and generateMetadata functions.

Basic Dynamic Segment

To create a route for a blog, you would create app/blog/[slug]/page.js.

// app/blog/[slug]/page.js

export default async function Page({ params }) {
  // params is a Promise in Next.js 15+
  const { slug } = await params;
  
  return (
    

Viewing Post: {slug}

This content is dynamically generated based on the URL segment.

); }

Catch-all Segments

You can extend dynamic segments to Catch-all subsequent segments by adding an ellipsis inside the brackets: [...folderName]. For example, app/shop/[...slug]/page.js will match /shop/clothes, /shop/clothes/tops, and /shop/clothes/tops/t-shirts.

Optional Catch-all Segments

By wrapping the catch-all segment in double square brackets, [[...slug]], you make the parameter optional. This means the route will also match the base folder (e.g., /shop in addition to /shop/clothes).

Comparison of Dynamic Segment Types

Type Syntax Example URL Resulting params
Dynamic Segment [slug] /blog/hello { slug: 'hello' }
Catch-all [...slug] /shop/a/b { slug: ['a', 'b'] }
Optional Catch-all [[...slug]] /shop { }

Generating Static Params

For performance and SEO, you may want to pre-render dynamic routes at build time rather than on-demand. The generateStaticParams function is used in combination with dynamic route segments to define the list of route segment parameters that will be statically generated.

 // app/blog/[slug]/page.js

// This function runs at build time
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then((res) => res.json());

  return posts.map((post) => ({
    slug: post.id,
  }));
}

export default async function Page({ params }) {
  const { slug } = await params;
  // ... rest of the component
}

Warning: In Next.js 15 and later, params and searchParams are asynchronous. You must await them or use React's use hook to access their properties to avoid hydration errors or build-time warnings.

Note

Route Groups are also useful for creating "Private Folders." While you can use _folderName to opt a folder out of routing entirely, Route Groups allow you to keep folders in the routing tree while keeping the URL clean.

Parallel & Intercepting Routes

Parallel and Intercepting Routes are advanced routing patterns in the Next.js App Router that allow for complex UI behaviors, such as dashboards with independent sections or modal overlays that maintain a shareable URL.These features enable "soft" transitions where parts of the page update without a full page refresh, while still supporting "hard" loads (like refreshing the page or sharing a link) that preserve the application state.

Parallel Routes

Parallel Routes allow you to simultaneously render one or more pages in the same layout that can be navigated independently. They are defined using named slots, created with the @folder convention. These slots are passed as props to the shared parent layout.This is particularly useful for dashboards where different sections (e.g., analytics, user feed, and team settings) need to be managedseparately, or when you want to implement independent loading and error states for different parts of a single view.

Implementing Parallel Routes

To define a parallel route, create a folder starting with @. In the example below, the layout will receive analytics and team as props alongside the standard children (which represents the primary page).

// app/dashboard/layout.js

export default function Layout({ children, analytics, team }) {
  return (
    <>
      <nav>Dashboard Header</nav>
      <section className="main-content">{children}</section>

      <div className="side-panels">
        <aside>{analytics}</aside>
        <aside>{team}</aside>
      </div>
    </>
  )
}

The default.js File

When a user performs a hard refresh or navigates directly to a URL, Next.js needs to know what to render for slots that don’t match the current URL.The default.js file serves as a fallback UI. If Next.js cannot +find a match for a slot during a full page reload, it will render the content of default.js.

Intercepting Routes

Intercepting routes allow you to “intercept” a navigation request from within the application to show a different UI than the user is navigating to.However, if the user performs a hard refresh or accesses the URL directly, the original route is loaded.The most common use case is the Modal Pattern. When clicking a photo in a gallery, an intercepting route can show the photo in a modal keeping the gallery in the background). If the user shares that link, the recipient sees the full photo page instead of the gallery-plus-modal.

Interception Conventions

Interception relies on a path-matching syntax similar to relative file paths:

  • (.) matches segments at the same level.
  • (..) matches segments one level above.
  • (...) matches segments two levels above.
  • (....) matches segments from the root app directory.

Example: Photo Modal

If you have a route structure where app/feed/page.js links to app/photo/[id]/page.js, you can intercept that navigation by creating app/feed/(.)photo/[id]/page.js.

// app/feed/(.)photo/[id]/page.js

import Modal from '@/components/Modal';
import PhotoFrame from '@/components/PhotoFrame';

export default function PhotoModal({ params }) {
  // This UI only displays when navigating from within the /feed page
  return (
    <Modal>
      <PhotoFrame id={params.id} />
    </Modal>
  );
}
Feature Primary Purpose Navigation Behavior
Parallel Routes Rendering multiple pages in one layout. Independent navigation within slots.
Intercepting Routes Showing a route in a different UI. Intercepts internal links; renders full page on reload.
Combined Advanced Modal/Dashboard patterns. Allows a modal to have its own URL and independent state.

Default and Matching Logic

Parallel and Intercepting routes often work together. For a modal to work correctly on refresh, you typically use a Parallel Slot to hold the modal content and an Intercepting Route to fill that slot when a link is clicked.

Navigation Type Parallel Slot Behavior Intercepting Behavior
Soft Navigation Maintains current state of other slots. Intercepts the target route.
Hard Navigation Requires default.js for non-matching slots. Renders the original, non-intercepted page.js.

warning you do not provide a default.js file for a parallel slot and the user refreshes the page on a sub-route that the slot does not recognize, Next.js will render a 404 for that slot.

Note

Parallel routes are excellent for conditional rendering. You can check user roles or permissions in the layout and decide whether to render the @admin slot or a @guest slot.

Route Handlers (API Endpoints)

Parallel and Intercepting Routes are advanced routing patterns in the Next.js App Router that allow for complex UI behaviors, such as dashboards with independent sections or modal overlays that maintain a shareable URL.These features enable "soft" transitions where parts of the page update without a full page refresh, while still supporting "hard" loads (like refreshing the page or sharing a link) that preserve the application state.

Parallel Routes

Parallel Routes allow you to simultaneously render one or more pages in the same layout that can be navigated independently. They are defined using named slots, created with the @folder convention. These slots are passed as props to the shared parent layout.This is particularly useful for dashboards where different sections (e.g., analytics, user feed, and team settings) need to be managedseparately, or when you want to implement independent loading and error states for different parts of a single view.

Implementing Parallel Routes

To define a parallel route, create a folder starting with @. In the example below, the layout will receive analytics and team as props alongside the standard children (which represents the primary page).

// app/dashboard/layout.js

export default function Layout({ children, analytics, team }) {
  return (
    <>
      <nav>Dashboard Header</nav>
      <section className="main-content">{children}</section>

      <div className="side-panels">
        <aside>{analytics}</aside>
        <aside>{team}</aside>
      </div>
    </>
  )
}

The default.js File

When a user performs a hard refresh or navigates directly to a URL, Next.js needs to know what to render for slots that don’t match the current URL.The default.js file serves as a fallback UI. If Next.js cannot +find a match for a slot during a full page reload, it will render the content of default.js.

Intercepting Routes

Intercepting routes allow you to “intercept” a navigation request from within the application to show a different UI than the user is navigating to.However, if the user performs a hard refresh or accesses the URL directly, the original route is loaded.The most common use case is the Modal Pattern. When clicking a photo in a gallery, an intercepting route can show the photo in a modal keeping the gallery in the background). If the user shares that link, the recipient sees the full photo page instead of the gallery-plus-modal.

Interception Conventions

Interception relies on a path-matching syntax similar to relative file paths:

  • (.) matches segments at the same level.
  • (..) matches segments one level above.
  • (...) matches segments two levels above.
  • (....) matches segments from the root app directory.

Example: Photo Modal

If you have a route structure where app/feed/page.js links to app/photo/[id]/page.js, you can intercept that navigation by creating app/feed/(.)photo/[id]/page.js.

// app/feed/(.)photo/[id]/page.js

import Modal from '@/components/Modal';
import PhotoFrame from '@/components/PhotoFrame';

export default function PhotoModal({ params }) {
  // This UI only displays when navigating from within the /feed page
  return (
    <Modal>
      <PhotoFrame id={params.id} />
    </Modal>
  );
}
Feature Primary Purpose Navigation Behavior
Parallel Routes Rendering multiple pages in one layout. Independent navigation within slots.
Intercepting Routes Showing a route in a different UI. Intercepts internal links; renders full page on reload.
Combined Advanced Modal/Dashboard patterns. Allows a modal to have its own URL and independent state.

Default and Matching Logic

Parallel and Intercepting routes often work together. For a modal to work correctly on refresh, you typically use a Parallel Slot to hold the modal content and an Intercepting Route to fill that slot when a link is clicked.

Navigation Type Parallel Slot Behavior Intercepting Behavior
Soft Navigation Maintains current state of other slots. Intercepts the target route.
Hard Navigation Requires default.js for non-matching slots. Renders the original, non-intercepted page.js.

warning you do not provide a default.js file for a parallel slot and the user refreshes the page on a sub-route that the slot does not recognize, Next.js will render a 404 for that slot.

Note

Parallel routes are excellent for conditional rendering. You can check user roles or permissions in the layout and decide whether to render the @admin slot or a @guest slot.

Middleware

Middleware in Next.js allows you to run code before a request is completed. Based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly. Middleware runs before cached content and routes are rendered, making it an ideal solution for authentication, bot protection, and server-side redirects.

In the Next.js App Router, Middleware is executed on the Edge Runtime. This is a lightweight subset of Node.js APIs designed for low latency and high performance, ensuring that global users experience minimal delay when their requests are intercepted.


Implementation and Execution

To define Middleware, you must create a single file named middleware.js (or .ts) in the root of your project (the same level as app or src). Unlike Route Handlers, you cannot have multiple middleware files spread across different folders; instead, you use conditional logic or a matcher config to control which paths the Middleware affects.

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  const token = request.cookies.get('session-token');

  // Logic: Redirect to login if no session token is found on protected routes
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  // Continue to the intended page
  return NextResponse.next();
} 

Matching Strategy

By default, Middleware will run for every route in your project. To optimize performance and ensure it only runs where necessary, you should define a matcher configuration. This allows you to filter out static assets (like images or CSS) and internal Next.js paths.

Pattern Type Syntax Example Description
Specific Path /about/:path* Matches all routes starting with /about/.
Negative Lookahead /((?!api|_next/static|favicon.ico).*) Matches all routes except API, static files, and favicon.
Multiple Paths ['/dashboard/:path*', '/account/:path*'] Runs Middleware for both the dashboard and account sections.
// middleware.js configuration
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}; 

Common Middleware Operations

Middleware uses the NextResponse API to control the flow of the request. Below are the primary methods used to handle incoming traffic.

Operation Method Description
Redirect NextResponse.redirect() Sends a 307/308 redirect to a different URL (changes the browser URL).
Rewrite NextResponse.rewrite() Serves a different page for the current URL (the URL in the browser stays the same).
Headers NextResponse.next({ request: { headers } }) Injects custom headers into the request before it matches the page.
Response new NextResponse(...) Returns a response immediately, bypassing the page (useful for maintenance modes).

Example: Setting Headers and Cookies

You can use Middleware to handle internationalization or security headers dynamically.

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  // 1. Get response object
  const response = NextResponse.next();

  // 2. Set a custom header for the browser
  response.headers.set('x-custom-security-header', 'active');

  // 3. Set a cookie (e.g., for A/B testing or localization)
  response.cookies.set('user-region', 'US');

  return response;
} 

Warning: Middleware runs in the Edge Runtime, not the full Node.js environment. This means you cannot use certain Node.js APIs (like fs for the file system) or many heavy npm packages. Always check the Edge Runtime API compatibility list.

Note

Middleware execution occurs before the Router Cache is checked. This ensures that even if a page is cached, your authentication logic or redirects are still enforced for every user request.

Internationalization (i18n)

Internationalization in the Next.js App Router is built directly into the routing system rather than relying on external configurations. Next.js supports Localized Routing, which allows you to serve different versions of your application based on the user's language and region. This is achieved through a combination of Route Groups, Dynamic Segments, and Middleware. By structuring your application with a [lang] dynamic segment at the root of the app directory, you can ensure that all URLs are prefixed with a locale code (e.g., /en/about or /fr/about).


Implementing Localized Routing

To implement i18n, you must move your existing page structure into a dynamic segment folder, typically named [lang]. This segment captures the locale from the URL and makes it available as a parameter to all nested layouts and pages.

// app/[lang]/page.js

export default async function Page({ params }) {
  const { lang } = await params;
  const dict = await getDictionary(lang); // Helper to load JSON files

  return (
    

{dict.welcome}

{dict.description}

); }

Automated Locale Detection

While users can navigate to localized URLs manually, it is a best practice to automatically redirect them based on their browser preferences. This logic is handled in middleware.js. By parsing the Accept-Language header from the incoming request, you can determine the user's preferred locale and redirect them to the corresponding path if they are visiting the root URL.

// middleware.js
import { NextResponse } from 'next/server';
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';

let locales = ['en-US', 'nl-NL', 'nl'];
let defaultLocale = 'en-US';

function getLocale(request) {
  const negotiatorHeaders = {};
  request.headers.forEach((value, key) => (negotiatorHeaders[key] = value));

  // Use negotiator and intl-localematcher to find the best match
  let languages = new Negotiator({ headers: negotiatorHeaders }).languages();
  return match(languages, locales, defaultLocale);
}

export function middleware(request) {
  const { pathname } = request.nextUrl;
  
  // Check if the pathname is missing any locale
  const pathnameIsMissingLocale = locales.every(
    (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
  );

  if (pathnameIsMissingLocale) {
    const locale = getLocale(request);
    return NextResponse.redirect(
      new URL(`/${locale}${pathname.startsWith('/') ? '' : '/'}${pathname}`, request.url)
    );
  }
}

export const config = {
  matcher: [
    // Skip all internal paths (_next)
    '/((?!_next|api|favicon.ico).*)',
  ],
}; 

Dictionary Pattern

Because Server Components do not support React Context, the common way to handle translations is the Dictionary Pattern. You create JSON files for each supported language and a utility function to import them asynchronously based on the current locale.

Locale File Path Content Example
English /dictionaries/en.json { "welcome": "Welcome", "cart": "Cart" }
French /dictionaries/fr.json { "welcome": "Bienvenue", "cart": "Panier" }
Spanish /dictionaries/es.json { "welcome": "Bienvenido", "cart": "Carrito" }
// app/[lang]/dictionaries.js
const dictionaries = {
  en: () => import('./dictionaries/en.json').then((module) => module.default),
  fr: () => import('./dictionaries/fr.json').then((module) => module.default),
};

export const getDictionary = async (locale) => dictionaries[locale](); 

Static Generation of Locales

To ensure optimal performance and SEO, you should use generateStaticParams at the root layout or page level. This allows Next.js to pre-render all localized versions of your site at build time, eliminating the need for server-side translation lookups during a user's request.

// app/[lang]/layout.js

export async function generateStaticParams() {
  return [{ lang: 'en' }, { lang: 'fr' }, { lang: 'es' }];
}

export default async function RootLayout({ children, params }) {
  const { lang } = await params;
  return (
    
      {children}
    
  );
} 

Warning: When implementing i18n in Middleware, ensure your matcher config excludes static assets (images, robots.txt, etc.). If you don't, the Middleware might attempt to redirect a request for logo.png to /en/logo.png, resulting in broken assets.

Note

For SEO purposes, always use the lang attribute in your < html> tag. Search engines use this to serve the correct language version to users in search results. You can also use the metadata API to set alternates (hreflang) for each page.

Data Fetching Last updated: Feb. 28, 2026, 7:25 p.m.

In the modern App Router, data fetching is simplified by using standard JavaScript async/await directly inside your React Server Components. This removes the need for legacy methods like getServerSideProps and allows you to fetch data closer to where it is used. By leveraging the native fetch API, Next.js extends the browser standard to include automatic caching and revalidation, ensuring that your data is both fast to access and easy to keep up-to-date.

The framework also introduces Streaming and Suspense, which allow parts of a page to render as soon as their data is ready, rather than waiting for the entire page's data to load. This significantly improves the Perceived Performance of your application. By combining these fetching strategies with Server Actions—which handle mutations like form submissions—Next.js provides a full-stack data story that maintains type safety and security without the overhead of a separate API layer.

Fetching, Caching, and Revalidating

Data fetching in the Next.js App Router is built directly upon the native Web fetch() API, extended to provide fine-grained control over Caching and Revalidation. By default, Next.js promotes fetching data on the server within Server Components. This approach allows you to secure sensitive API keys, reduce client-side JavaScript, and minimize the "waterfall" of network requests by fetching data closer to the source.

Fetching Data on the Server

In the App Router, you can define your components as async functions to perform data fetching directly. This eliminates the need for legacy methods like getServerSideProps or getStaticProps. When a request is made, Next.js executes the fetch on the server and streams the rendered HTML to the client.

// app/blog/page.js
async function getPosts() {
  const res = await fetch('https://api.example.com/posts');
  
  if (!res.ok) {
    // This will activate the closest `error.js` Error Boundary
    throw new Error('Failed to fetch data');
  }
 
  return res.json();
}

export default async function Page() {
  const posts = await getPosts();

  return (
    <main>
      {posts.map((post) => (
        <article key={post.id}>{post.title}</article>
      ))}
    </main>
  );
}

Caching Data

Caching reduces the number of requests made to your data source, improving performance and reducing costs. Next.js extends the fetch options object to include a cache property. By default, fetch requests are memoized (deduplicated) during a single render pass, and in many cases, the result is stored in the Data Cache on the server.

Cache Option Behavior Use Case
force-cache (Default) Next.js looks for a matching request in the Data Cache. Static data that doesn't change often (e.g., Blog posts).
no-store Fetch the resource on every request without checking the cache. Dynamic data that changes frequently (e.g., Stock prices).
// Example: Opting out of caching for real-time data
fetch('https://api.example.com/live-data', { cache: 'no-store' });

Revalidating Data

Revalidation is the process of purging the Data Cache and re-fetching the latest data. This allows you to serve static content most of the time while ensuring the data remains fresh. Next.js supports two types of revalidation:

1. Background (Time-based) Revalidation

This method automatically revalidates data after a certain amount of time has passed. You use the next.revalidate option in the fetch call.

 // Revalidate this request at most every hour (3600 seconds)
fetch('https://api.example.com/data', { next: { revalidate: 3600 } });

2. On-Demand Revalidation

This allows you to manually purge the cache based on an event, such as a headless CMS webhook or a user action (via a Server Action). You can revalidate by path or by Tag.

Method Syntax Description
Tag-based revalidateTag('tag-name') Purges all fetch requests associated with a specific tag.
Path-based revalidatePath('/blog') Purges the cache for all data on a specific URL path.
// app/api/revalidate/route.js
import { revalidateTag } from 'next/cache';

export async function POST(request) {
  const tag = (await request.json()).tag;
  revalidateTag(tag); // Purges cache for fetches tagged with this string
  return Response.json({ revalidated: true });
}

Fetching with Third-party Libraries

If you are using a database client, ORM (like Prisma), or an SDK that does not use fetch (and thus cannot use the next.revalidate options), you can use the React cache function for request memoization and the unstable_cache API for persistent caching.

import { cache } from 'react';
import { unstable_cache } from 'next/cache';
import db from './database';

// Memoize the request for a single render pass
export const getItem = cache(async (id) => {
  const item = await db.item.findUnique({ where: { id } });
  return item;
});

// Persistently cache the database query result across requests
export const getCachedItem = unstable_cache(
  async (id) => await db.item.findUnique({ where: { id } }),
  ['items-cache-key'],
  { revalidate: 60, tags: ['items'] }
);

Note

Request Memoization is a React feature that ensures if you fetch the same data in multiple components (e.g., in a Layout and a Page), the network request only happens once. This is separate from the Data Cache, which persists data across multiple users and requests.

Warning: If you use revalidatePath or revalidateTag within a Server Action, the cache is purged immediately, and the page will be re-rendered with the new data on the next visit. However, this does not automatically update the UI for other users already viewing the page unless they refresh or navigate.

Server Actions and Mutations

Server Actions are asynchronous functions that run on the server. They are the primary way to handle data mutations (creating, updating, or deleting data) in the Next.js App Router without having to manually create an API Route. By integrating directly with React's form actions and the useActionState hook, Server Actions provide a cohesive developer experience for handling form submissions and state updates while reducing client-side JavaScript.

Defining Server Actions

A Server Action can be defined in two places:

  1. Inside a Server Component: By adding the "use server" directive at the top of an async function.
  2. In a Separate File: By adding "use server" at the top of the file. This allows you to import and use the action in both Server and Client Components.
// app/actions.js
"use server"

import { revalidatePath } from 'next/cache';

export async function createInvoice(formData) {
  const rawFormData = {
    customerId: formData.get('customerId'),
    amount: formData.get('amount'),
    status: formData.get('status'),
  };

  // 1. Perform database mutation
  await db.invoice.create({ data: rawFormData });

  // 2. Clear the cache for the invoices page
  revalidatePath('/dashboard/invoices');

  // 3. Optional: Redirect the user
  return { message: 'Invoice Created Successfully' };
}

Form Integration and Progressive Enhancement

Server Actions integrate seamlessly with the HTML < form> element's action prop. A major advantage is Progressive Enhancement: the form can be submitted even if JavaScript has not yet loaded or is disabled on the client, as Next.js can handle the submission via a standard HTTP POST request.

// app/invoices/create/page.js
import { createInvoice } from '@/app/actions';

export default function Page() {
  return (
    <form action={createInvoice}>
      <input type="text" name="customerId" placeholder="Customer ID" required />
      <input type="number" name="amount" placeholder="Amount" required />
      <select name="status">
        <option value="pending">Pending</option>
        <option value="paid">Paid</option>
      </select>
      <button type="submit">Create Invoice</button>
    </form>
  );
}

Handling State and Feedback

To manage loading states, errors, and server responses in Client Components, Next.js leverages React hooks designed specifically for actions.

Hook Purpose Usage Level
useActionState Returns the result of an action and its pending state. Component State
useFormStatus Provides status information (like pending) of the parent form. Nested UI Components
useOptimistic Shows a "predicted" state before the server confirms the change. UX/Performance

Example: useActionState (Next.js 15/React 19)

"use client"

import { useActionState } from 'react';
import { createInvoice } from '@/app/actions';

export default function Form() {
  const [state, formAction, isPending] = useActionState(createInvoice, null);

  return (
    <form action={formAction}>
      <input name="amount" type="number" />
      <button disabled={isPending}>
        {isPending ? 'Saving...' : 'Save'}
      </button>
      {state?.message && <p>{state.message}</p>}
    </form>
  );
}

Security and Best Practices

Because Server Actions are public HTTP endpoints, they must be treated with the same security rigor as API Routes.

  • Authentication: Always check the user's session inside the action using auth() or similar utilities before performing mutations.
  • Validation: Use libraries like Zod to validate the formData before interacting with your database to prevent malformed data or injection attacks.
  • Closure Protection: Next.js automatically creates a secure URL for each action. However, avoid passing sensitive data (like user IDs) as hidden form fields; instead, retrieve them from the session on the server.

Note

Optimistic Updates are a powerful way to make your app feel faster. By using the useOptimistic hook, you can immediately update the UI (e.g., adding a comment to a list) while the Server Action is still processing in the background.

Warning: Server Actions have a default size limit for the body (usually 1MB). If you are uploading large files, you may need to adjust the serverActions.bodySizeLimit in your next.config.js

Data Fetching Patterns

Data fetching patterns in Next.js define how and where data is retrieved to optimize for performance, user experience, and resource management. By choosing the right pattern, you can eliminate "waterfalls" (sequential loading) and ensure that your application feels responsive even when dealing with multiple slow data sources.

Parallel vs. Sequential Data Fetching

When fetching data for a page, the order in which you initiate requests significantly impacts the Time to Interactive (TTI).

1. Sequential Data Fetching (The Waterfall)

In this pattern, requests are made one after another. This often happens unintentionally when a component waits for one await to finish before starting the next, or when a child component fetches data only after its parent has finished.

// Sequential: Total time = Fetch 1 + Fetch 2
async function Page() {
  const artist = await getArtist(id); // Waits for this...
  const albums = await getAlbums(artist.id); // ...before starting this
  return <main>{/* UI */}</main>;
}

2. Parallel Data Fetching

To reduce total loading time, you can initiate all requests simultaneously using Promise.all() or by starting the fetches before awaiting them. This ensures the total wait time is only as long as the slowest request.

// Parallel: Total time = Time of the slowest fetch
async function Page() {
  const artistData = getArtist(id);
  const albumsData = getAlbums(id);

  // Initiate both requests in parallel
  const [artist, albums] = await Promise.all([artistData, albumsData]);

  return <main>{/* UI */}</main>;
}

Data Fetching Strategies

Pattern Implementation Best For
Preloading Calling a fetch function before it is needed in the component tree. Optimizing deep component trees.
Client-side Using SWR or TanStack Query in Client Components. User-specific, private, or frequently updating data.
On-the-server Native fetch inside Server Components. SEO, initial page load, and security-sensitive data.
Request Memoization Automatically deduplicating identical requests in one render pass. Avoiding prop drilling for shared data.

The Preload Pattern

The preload pattern is a technique to manually trigger a fetch request as early as possible. This is particularly useful when you have a component that won't render immediately (e.g., it's behind a conditional) but you want its data to start loading regardless.

// components/Item.js
import { getItem } from '@/lib/data';

export const preload = (id) => {
  // void initiates the fetch without awaiting it
  void getItem(id);
}

export default async function Item({ id }) {
  const item = await getItem(id);
  return <div>{item.name}</div>;
}

Comparison: Server-side vs. Client-side Fetching

Deciding where to fetch data depends on the requirements of the specific UI element.

Feature Server Components (fetch) Client Components (use/SWR)
SEO Excellent (Content is in HTML) Poor (Requires JS execution)
Performance Fast (Close to data source) Slower (Round trip from browser)
Interactivity Low (Static once rendered) High (Real-time updates)
Secrets Safe (API keys stay on server) Unsafe (API keys exposed to browser)
Caching Persistent (Data Cache) Temporary (Browser/Memory)

Combining Patterns with Streaming

For the best user experience, you can combine Parallel Fetching with Streaming (covered in Section 2.4). By wrapping slow data fetches in React Suspense boundaries, you can render the page "shell" immediately and fill in the data-heavy sections as they resolve.

Note

Request Memoization is a built-in Next.js feature. If you fetch the same user profile in the layout.js and the page.js, Next.js will only execute the network request once, as long as the URL and options are identical.

Warning: Avoid fetching data in a loop. If you have a list of 50 IDs and fetch them individually inside a .map(), you will create 50 separate network requests. It is always better to use a batch API endpoint (e.g., /api/items?ids=1,2,3) when possible.

Rendering Last updated: Feb. 28, 2026, 7:26 p.m.

Next.js offers a hybrid rendering model, allowing you to choose between Static and Dynamic rendering on a per-route basis. Static rendering happens at build time and is ideal for content that doesn't change often, providing the fastest possible experience via CDNs. Dynamic rendering occurs at request time, making it suitable for personalized data or information that depends on real-time factors like cookies or URL search parameters.

The core of this section is the distinction between Server Components and Client Components. By default, all components in the app/ directory are Server Components, which stay on the server and do not add to the JavaScript bundle size. When interactivity is needed (like buttons or search inputs), you "opt-in" to the client using the 'use client' directive. This strategic split ensures that the browser only downloads the minimal amount of code necessary, leading to superior performance and faster "Time to Interactive."

Server Components

React Server Components (RSC) are the foundational building block of the Next.js App Router. By default, all components created within the app directory are Server Components. This architecture allows components to be rendered on the server and cached, significantly reducing the amount of JavaScript sent to the client and improving performance.

The Role of Server Components

Server Components move the rendering process from the browser to the server. This shift provides several distinct advantages:

  • Zero Client-Side JavaScript: The code used to render Server Components stays on the server. Only the final HTML and a small "flight" data manifest are sent to the browser.
  • Direct Backend Access: Because they run on the server, these components can access databases, file systems, and internal microservices directly without an intermediate API layer.
  • Enhanced Security: Sensitive information (API keys, environment variables, internal logic) never reaches the client-side bundle.
  • Automatic Code Splitting: Large dependencies used in Server Components (like Markdown parsers or date libraries) do not increase the bundle size for the user.

Comparison: Server vs. Client Components

Understanding when to use Server Components versus Client Components is the key to mastering Next.js.

Feature Server Component Client Component
Default in App Router Yes No (requires "use client")
Fetch Data Direct (async/await) Via API or Hooks
Access Backend Resources Direct Indirect (via API)
Keep Secrets (API Keys) Yes No
Large Dependencies No client cost Adds to bundle size
Interactivity (onClick, onChange) No Yes
Browser APIs (window, localStorage) No Yes
React Hooks (useState, useEffect) No Yes

The Component Tree Lifecycle

In a Next.js application, Server and Client Components are often interleaved. Next.js manages this using a "serialized" data format.

  1. On the Server: Next.js renders the Server Component tree into a special format called the RSC Payload.
  2. On the Client: The browser uses this payload to reconstruct the React component tree and "hydrates" only the Client Components.

Composition Pattern

To maintain the performance benefits of Server Components, you should always try to push Client Components to the "leaves" of your component tree. If you need to wrap a Server Component in a Client Component (e.g., for a Context Provider), you must pass the Server Component as children.

// app/layout.js (Server Component)
import { ThemeProvider } from './theme-provider'; // Client Component

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {/* You can nest Server Components (children) inside Client Components */}
        <ThemeProvider>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

Server Component Rendering Strategies

Next.js optimizes Server Components based on whether the data is known at build time or request time:

  • Static Rendering (Default): Components are rendered at build time. The result is cached and served from a CDN. This is ideal for blogs, landing pages, and documentation.
  • Dynamic Rendering: Components are rendered at request time. This is triggered when a component uses dynamic functions (like cookies() or headers()) or uncached data fetches.

Note

Server Components are not a replacement for Client Components. They are complementary. Use Server Components for data fetching and static content, and Client Components for stateful interactivity and browser-specific logic.

Warning: You cannot import a Server Component into a Client Component. You can only pass a Server Component as a child or a prop to a Client Component.

Client Components

Client Components allow you to add client-side interactivity to your application. In the Next.js App Router, they are pre-rendered on the server (SSR) to generate HTML, but their JavaScript is sent to the browser to enable React hooks, event listeners, and access to browser APIs.

To define a Client Component, you must place the "use client" directive at the very top of the file, before any imports. This directive marks a "boundary" between server-only code and client-side code.

When to Use Client Components

While Server Components should be your default for data fetching and layout structure, Client Components are necessary for any part of the UI that requires a persistent connection to the browser or immediate user feedback.

Use Case Recommended Component
Interactivity Client (e.g., onClick(), onChange())
State & Lifecycle Client (e.g., useState(), useEffect(), useReducer())
Browser APIs Client (e.g., window, localStorage, geolocation, canvas)
Class Components Client (Server Components must be functional)
Data Fetching Server (unless refreshing frequently based on user state)
Static UI Server (reduces client-side bundle size)

The "use client" Directive

The "use client" Directive directive is an instruction to the bundler. When Next.js encounters this directive in a file, it treats that file—and any files imported into it—as part of the client bundle.

"use client";

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

Composition Patterns: Nesting Server and Client Components

A common point of confusion is how to combine Server and Client Components. Because Client Components are rendered in the browser, they cannot "understand" Server Component code. Therefore, you cannot import a Server Component into a Client Component.

Supported Pattern: Passing Server Components as Props

If you need a Server Component to sit "inside" a Client Component, you must pass it as the children prop (or any other prop). This allows the Server Component to be rendered on the server first and then placed into the Client Component's slot.

// app/client-wrapper.js (Client Component)
"use client";
export default function ClientWrapper({ children }) {
  return <div className="interactive-sidebar">{children}</div>;
}

// app/page.js (Server Component)
import ClientWrapper from './client-wrapper';
import ServerContent from './server-content';

export default function Page() {
  return (
    <ClientWrapper>
      {/* This works: ServerContent is rendered on the server */}
      <ServerContent />
    </ClientWrapper>
  );
}

Optimization: Moving Client Components to the Leaves

To keep your application fast, you should aim to minimize the amount of JavaScript sent to the browser. This is done by "moving Client Components to the leaves" of your component tree.

Instead of making an entire layout a Client Component just because it has a search bar, make only the SearchBar a Client Component and keep the rest of the layout as a Server Component.

Common Pitfalls

  • Omitting "use client": If you use a hook like useState in a file without the directive, Next.js will throw a "useState is not a function" error because it tried to execute it on the server.
  • Third-party Libraries: Many existing React libraries (e.g., UI component libraries) haven't added the "use client" directive to their components yet. If you use them in a Server Component, you may need to wrap them in your own Client Component first.

Note

Client Components are still pre-rendered on the server to generate an initial HTML preview. This ensures that users see content immediately, even before the JavaScript bundle finishes downloading and "hydrates" the interactivity.

Composition Patterns

Composition patterns in Next.js define how Server Components and Client Components interact within the same UI tree. Because Server Components cannot be imported into Client Components, you must follow specific structural rules to maintain performance and ensure backend-only code stays off the browser.

The "Lifting" Pattern (Passing Server as Props)

The most important rule in Next.js composition is that you can pass a Server Component to a Client Component, but you cannot import it. By using the children prop, the Server Component is rendered on the server, and the resulting payload is sent to the client to be placed inside the "hole" created by the Client Component.

// 1. A Client Component (the "wrapper")
'use client'
export function InteractiveWrapper({ children }) {
  const [isOpen, setIsOpen] = useState(false);
  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      {isOpen && children}
    </div>
  );
}

// 2. A Server Component (the "page")
import { InteractiveWrapper } from './InteractiveWrapper';
import { DataHeavyComponent } from './DataHeavyComponent';

export default function Page() {
  return (
    <InteractiveWrapper>
      {/* This works! DataHeavyComponent renders on the server */}
      <DataHeavyComponent />
    </InteractiveWrapper>
  );
} 

Shared Data Patterns

In the App Router, you should not use React Context for data fetching. Instead, use Request Memoization. If multiple components (Server or Client) need the same data, fetch it in each component. Next.js ensures the actual network request only happens once per render pass.

Pattern Approach Benefit
Prop Drilling Pass data from parent to child. Standard React; works for shallow trees.
Deduplicated Fetch Call fetch() in both components. Removes the need for Context/Props on the server.
Context Providers Wrap children in a Client Component. Required for global state (Theme, Auth) on the client.

Keeping Server-Only Code Private

To prevent sensitive server-side logic (like database secret keys) from accidentally being imported into a Client Component, you can use the server-only package. If a file marked with server-only is imported into a Client Component, Next.js will throw a build-time error.

// lib/database.js
import 'server-only';

export async function getData() {
  // This code will never be bundled for the browser
  return await db.query('...');
} 

Pattern Summary Table

Goal Recommended Pattern
Interleaving Pass Server Components as children to Client Components.
Performance Move Client Components to the "leaves" of the tree.
Data Sharing Fetch data where it's needed; rely on fetch memoization.
Third-party UI Wrap libraries (like Chakra UI) in a local Client Component.

Common Mistakes to Avoid

  • Importing Server into Client: If you see an error like "Module not found: Can't resolve 'fs'", you are likely importing a server-side module into a file marked with "use client".
  • Making Layouts Client Components: Making a Root Layout a Client Component will turn your entire app into a Client-side application, losing most of the benefits of RSC. Only use "use client" in the smallest sub-components possible.

Edge and Node.js Runtimes

In the Next.js App Router, you can choose which runtime your code executes on. A runtime is the environment that provides the APIs and libraries available to your code during execution. Next.js supports two server-side runtimes: Node.js (the default) and the Edge Runtime.

Choosing the right runtime is a balance between having access to the full ecosystem of Node.js libraries and achieving the ultra-low latency of the Edge.

Comparison of Runtimes

Feature Node.js Runtime (Default) Edge Runtime
Best For Complex logic, heavy dependencies. Low-latency, fast global delivery.
Deployment Single or multiple regions. Distributed globally (CDNs).
Cold Boots Slower (larger footprint). Instant (lightweight).
API Support Full Node.js (fs, child_process). Web APIs (Fetch, Streams, Crypto).
Package Support All npm packages. Limited (no native Node modules).
Performance High throughput. Minimal latency for small tasks.

The Node.js Runtime

The Node.js Runtime is the standard environment. It provides access to all Node.js APIs and all npm packages that rely on them. This is the best choice if you are using ORMs like Prisma, performing heavy image processing, or interacting with legacy backend systems that require specific Node.js modules.

The Edge Runtime

The Edge Runtime is based on Web APIs and is designed for high performance at scale. It is deployed to a distributed network of servers (the "Edge"). Because it has a much smaller footprint than Node.js, it has virtually no cold boot time.

Common use cases for Edge:

  • Middleware: Handling redirects, authentication checks, or A/B testing.
  • Middleware: Handling redirects, authentication checks, or A/B testing.
  • Internationalization: Redirecting users based on location.

How to Configure the Runtime

You can specify the runtime for a specific Route Segment (Layout, Page, or Route Handler) by exporting a runtime constant.

// app/api/fast-data/route.js

export const runtime = 'edge'; // Options: 'nodejs' (default) or 'edge'

export async function GET() {
  return new Response(JSON.stringify({ message: "Hello from the Edge!" }), {
    headers: { 'Content-Type': 'application/json' },
  });
}

Limitations of the Edge Runtime

Because the Edge Runtime is designed to be lightweight, it does not support the full suite of Node.js APIs. You cannot use:

  • File System APIs: fs, path.
  • Process APIs: child_process, cluster.
  • V8 built-ins: Certain low-level V8 features.
  • Native Addons: Any npm package written in C++ or that relies on native Node.js bindings.

Runtime Selection Logic

Note

If you are using Middleware, Next.js automatically uses the Edge Runtime. You cannot switch Middleware to the Node.js runtime because it needs to execute before the request reaches the origin server to ensure low latency.

Warning: When using the Edge Runtime, ensure that your database drivers are "Edge-ready." Many traditional database drivers (like the standard PostgreSQL pg client) rely on Node.js socket APIs that aren't available at the Edge. You may need to use HTTP-based database drivers or connection poolers like Prisma Accelerate or Neon.

Caching Last updated: Feb. 28, 2026, 7:26 p.m.

Caching is one of the most complex yet beneficial parts of Next.js architecture. The framework employs a four-layer caching system designed to minimize network requests and improve speed. This includes the Data Cache (storing fetch results), the Request Memoization (avoiding duplicate fetches in a single render pass), the Full Route Cache (storing HTML and RSC payloads), and the Router Cache (storing page segments in the browser during a session).

Managing these caches is handled through Revalidation. You can set data to expire after a certain amount of time (Time-based Revalidation) or trigger an update manually when a specific event occurs, such as a database change (On-demand Revalidation). This granular control allows you to serve static-speed content while ensuring users never see significantly outdated information, effectively bridging the gap between static sites and dynamic applications.

Request Memoization

Request Memoization is a React feature—extended by Next.js—that deduplicates fetch requests for the same data within a single render pass. If you need the same data in multiple components across a page (e.g., in a Layout, a Page, and multiple nested components), you can fetch it in each location without worrying about performance penalties. Next.js ensures the network request is executed only once.

How Memoization Works

When you call fetch() with the same URL and options, the first call executes the network request and stores the result in memory. Subsequent calls during the same server render return the result from memory.

  • Scope: It lasts only for the duration of a single server request.
  • Storage: The cache exists in memory on the server and is wiped once the rendering is complete.
  • API: It applies specifically to the fetch() API with GET methods.

Request Memoization vs. Data Cache

It is critical to distinguish Memoization from the persistent Data Cache. While they work together, they serve different purposes in the request lifecycle.

Feature Request Memoization Data Cache
Source React Feature Next.js Feature
Duration Single Request (Per-render) Persistent (Across requests/users)
Location Server Memory Persistent Storage (Disk/CDN)
Purpose Avoid duplicate work in one tree. Avoid fetching from the data source.
Function Deduplication Caching

Practical Implementation

In the following example, getUser() is called in both the Layout and the Page. Without memoization, this would trigger two network requests. With memoization, only one request is sent.

// lib/data.js
export async function getUser(id) {
  const res = await fetch(`https://api.example.com/user/${id}`);
  return res.json();
}

// app/layout.js
export default async function Layout({ children }) {
  const user = await getUser('123'); // First call: Triggers network request
  return <div>{user.name}{children}</div>;
}

// app/page.js
export default async function Page() {
  const user = await getUser('123'); // Second call: Returns memoized result
  return <h1>Welcome, {user.name}</h1>;
} 

Memoizing Non-Fetch Functions

The built-in memoization only applies to the fetch() API. If you are using a database client (like Prisma), an ORM, or an external SDK, you can use the React cache function to manually memoize data fetching.

import { cache } from 'react';
import db from '@/lib/db';

// Manually memoize a database query
export const getUser = cache(async (id) => {
  const user = await db.user.findUnique({ where: { id } });
  return user;
}); 

Optimization Benefits

  • Eliminates Prop Drilling: You no longer need to fetch data at the top level and pass it down through multiple layers of components just to share data.
  • Component Independence: Components can be truly modular. A UserAvatar component can fetch its own data, even if the Navbar above it already fetched that same user's data.
  • Server-Side Only: This process happens entirely on the server, so no extra memory is consumed on the user's device.

Note

Request Memoization does not apply to POST, PUT, or DELETE requests, as these are typically mutations and should not be deduplicated or cached automatically.

Warning: Memoization only works if the arguments passed to the function are identical. If you call fetch for the same URL but with different headers or cache configurations, Next.js will treat them as distinct requests.

Data Cache

The Data Cache is a persistent, server-side storage mechanism unique to Next.js. Unlike Request Memoization, which only lasts for a single user request, the Data Cache persists data across multiple requests and multiple users. It allows you to store the results of expensive data fetches (like API calls or database queries) and reuse them until they are specifically revalidated or purged.

How the Data Cache Works

Next.js extends the native fetch API by adding a cache property. This property determines how the result of the fetch is stored in the Data Cache.

  • Persistence: The cache lives on the server's disk or in an external store (like an S3 bucket or Redis in a distributed environment).
  • Scope: Shared across all users of the application.
  • Trigger: It is activated by the fetch() function or the unstable_cache API.

Caching Options

You can control the behavior of the Data Cache using the cache and next.revalidate options within your fetch requests.

Option Behavior Best Use Case
force-cache (Default) Always looks for a cached version first. Static content (e.g., Blog posts, FAQs).
no-store Skips the cache and fetches from the source every time. Real-time or personalized data (e.g., Stock prices, User settings).
revalidate: <number> Caches data but refreshes it after X seconds. Content that updates periodically (e.g., News feeds).
// Example: Storing data in the cache indefinitely
fetch('https://api.example.com/data', { cache: 'force-cache' });

// Example: Refreshing data every 60 seconds (Time-based revalidation)
fetch('https://api.example.com/data', { next: { revalidate: 60 } }); 

Comparison: Request Memoization vs. Data Cache

It is common to confuse these two layers. The table below highlights their fundamental differences:

Feature Request Memoization Data Cache
Duration Life of a single render/request. Persistent across requests and users.
Location Server Memory (RAM). Persistent Storage (Disk/CDN).
API React fetch / cache(). Next.js fetch / unstable_cache().
Purpose Deduplicate work in one tree. Avoid hitting the data source.

Revalidating the Data Cache

Revalidation is the process of updating the cached data. There are two primary ways to do this:

  1. Time-based Revalidation: Automatically purges the cache after a set amount of time (e.g., revalidate: 3600).
  2. On-Demand Revalidation: Purges the cache manually in response to an event (like a CMS update or a form submission). This is done using revalidatePath or revalidateTag.
// Tagging a fetch request
fetch('https://api.example.com/products', { next: { tags: ['products'] } });

// Inside a Server Action or Route Handler to purge on-demand
import { revalidateTag } from 'next/cache';

export async function action() {
  await updateProductInDB();
  revalidateTag('products'); // All fetches with the 'products' tag are purged
} 

The unstable_cache API

For data that doesn't use fetch (like database calls via an ORM), Next.js provides unstable_cache. This allows you to apply Data Cache logic to any asynchronous function.

import { unstable_cache } from 'next/cache';

const getCachedUser = unstable_cache(
  async (id) => await db.user.findUnique({ where: { id } }),
  ['user-cache-key'], // Unique key
  { revalidate: 60, tags: ['users'] }
); 

Note

The Data Cache is skipped if the route is dynamically rendered and the fetch request is not explicitly cached.

Warning: In a multi-server (distributed) environment, like a server farm or multiple Vercel regions, the Data Cache is shared across all instances to ensure consistency for your users.

Full Route Cache

The Full Route Cache is a server-side mechanism that caches the renderedoutput (HTML and React Server Component Payload) of a route at build time or during the first request. This is the primary driver behind Next.js's ability to serve static pages at lightning speeds, as it avoids re-executing component logic and re-fetching data for every single visitor.

How it Works

The Full Route Cache works in tandem with the Data Cache. When a route is rendered:

  1. React renders Server Components into the RSC Payload.
  2. Next.js uses that payload and Client Component JavaScript to generate HTML on the server.
  3. Both the HTML and the RSC Payload are stored in the Full Route Cache.

Upon subsequent requests, Next.js serves the cached result directly from the storage, bypassing the rendering process entirely.

Static vs. Dynamic Routes

The Full Route Cache only applies to Statistically Rendered routes. If a route is Dynamically Rendered, it is not stored in the Full Route Cache because its content must change based on the specific request.

Feature Static Rendering (Cached) Dynamic Rendering (Not Cached)
When Rendered Build time or background revalidation. Every request time.
Trigger Default behavior. Using cookies(), headers(), or no-store fetches.
Performance Instant (served from cache/CDN). Dependent on server execution time.
User Data Generic/Shared content. Personalized/Real-time content.

Invalidation and Revalidation

Since the Full Route Cache stores the final rendered output, it is fundamentally tied to the data used within those components. If the underlying data changes, the Full Route Cache must be updated.

  • Revalidating Data: When you revalidate the Data Cache (using revalidatePath or revalidateTag), Next.js automatically invalidates the Full Route Cache for the affected pages. The page will be re-rendered on the next request.
  • Redeploying: Every time you redeploy your application, the Full Route Cache is cleared, ensuring users see the newest version of your site.

Opting Out of Full Route Caching

You can opt out of the Full Route Cache (making the route dynamic) by:

  • Using Dynamic Functions: cookies(), headers(), or accessing searchParams in a page.
  • Using Segment Config: Setting export const dynamic = 'force-dynamic' or revalidate = 0.
  • Using Uncached Data: Performing a fetch with cache: 'no-store'.
// Example: Forcing a route to stay dynamic (skipping Full Route Cache)
export const dynamic = 'force-dynamic';

export default function Page() {
  return <div>Current Time: {new Date().toLocaleTimeString()}</div>;
} 

Comparison: Data Cache vs. Full Route Cache

Feature Data Cache Full Route Cache
What is cached? Raw data (JSON) from fetches. Rendered HTML and RSC Payload.
Where is it stored? Persistent Storage (Disk/CDN). Persistent Storage (Server).
Goal Minimize data source requests. Minimize server rendering work.
Dependency Independent of rendering. Dependent on Data Cache & Components.

Note

The Full Route Cache is a "last-mile" optimization. Even if your Data Cache is fast, re-rendering a complex component tree for every user adds overhead. By caching the full route, you reduce that overhead to nearly zero.

Warning: Be careful when using force-static on routes that rely on dynamic data via unstable_cache. If the cache key isn't properly managed, users might see stale rendered HTML even if the underlying data has changed.

Router Cache

The Router Cache is a client-side, in-memory cache that stores the RSC Payload of visited and prefetched route segments for the duration of a user's session. Unlike the server-side caches discussed previously, the Router Cache exists entirely within the browser and is designed to make navigation feel instantaneous by eliminating the need for a network request when moving between pages.

How it Works

When a user navigates or a link enters the viewport (triggering a prefetch), Next.js stores the resulting RSC Payload in the browser's memory. On subsequent navigations to that same route, the browser retrieves the content from this local cache instead of asking the server.

  • Scope: Specific to the browser tab and the current user session.
  • Storage: In-memory (lost upon full page refresh).
  • Duration: Lasts as long as the session, though specific segments may expire or be cleared.

Invalidation and Duration

The Router Cache is temporary. It is automatically cleared or updated in several scenarios:

Event Effect on Router Cache
Refresh Entire cache is wiped.
Server Action Cache is invalidated to ensure data consistency after a mutation.
router.refresh() The current route's cache is purged and re-fetched.
Timeout Prefetched segments are stored for 5 minutes (static) or 30 seconds (dynamic) by default.

Prefetching

Prefetching is the primary way the Router Cache is populated before a user even clicks a link. Next.js uses the < Link> component to automatically prefetch routes.

  • Static Routes: The entire RSC Payload for the route is prefetched and cached.
  • Dynamic Routes: Only the shared layout and "loading" states are prefetched. The dynamic page data is fetched only when the user clicks the link.
// Default prefetching behavior
<Link href="/dashboard">Dashboard</Link>

// Disabling prefetching for a specific link
<Link href="/settings" prefetch={false}>Settings</Link> 

Caching Summary: The Four Layers

To master Next.js performance, you must understand how these four caching layers interact:

Cache Location Type What is Cached
Request Memoization Server React Fetch results (in one render)
Data Cache Server Next.js Fetch results (across requests)
Full Route Cache Server Next.js HTML & RSC Payload
Router Cache Client Next.js RSC Payload (session-based)

Managing the Router Cache

While you cannot directly access the Router Cache object, you can influence it using the useRouter hook or Server Actions.

  1. Server Actions: If you perform a data mutation (e.g., updating a profile), calling revalidatePath or revalidateTag on the server will notify the client to purge relevant parts of its Router Cache.
  2. router.refresh(): Forces the client to refresh the current route, ignoring the Router Cache and fetching the latest data from the server.

Note

NoteThe Router Cache is what enables Partial Rendering. Because the layouts are already cached in the browser, only the changing page segment needs to be fetched from the server when you navigate between sibling routes.

Warning: Since the Router Cache is in-memory, it can grow if the user visits many pages. Next.js automatically manages this by evicting the least recently used segments.

Styling Last updated: Feb. 28, 2026, 7:28 p.m.

Next.js provides a flexible styling ecosystem that caters to diverse developer preferences while maintaining high performance. Out of the box, it supports CSS Modules, which prevent class name collisions by scoping styles locally to the component. For those who prefer utility-first workflows, Tailwind CSS is deeply integrated and can be enabled during the initial project setup, offering a rapid way to build responsive designs without leaving your JSX files.

For advanced use cases, the framework supports global CSS, Sass, and various CSS-in-JS libraries. However, it is important to note that many traditional CSS-in-JS libraries require runtime JavaScript, which can conflict with the performance benefits of Server Components. Next.js continues to evolve its support for "zero-runtime" CSS-in-JS solutions, ensuring that your styling choices don't compromise the core speed of the App Router.

CSS Modules

CSS Modules are the default way to style Next.js applications when using traditional CSS. They allow you to write CSS that is locally scoped to a specific component by automatically creating unique class names. This prevents style "leaking" and naming collisions, which are common issues in large-scale applications where two different components might otherwise use the same class name (e.g., .button).

How CSS Modules Work

To use CSS Modules, you name your style files with the .module.css extension. When you import this file into a React component, Next.js maps the class names you defined to unique, hashed strings (e.g., styles.button might become button_a7b2c9).

1. Create the Module

 /* Button.module.css */
.error {
  color: white;
  background-color: red;
  padding: 10px;
  border-radius: 5px;
}

2. Import and Apply

// Button.js
import styles from './Button.module.css';

export default function Button() {
  // The class name will be unique to this component
  return <button className={styles.error}>Delete Item</button>;
} 

Comparison: Global CSS vs. CSS Modules

Feature Global CSS (globals.css) CSS Modules (.module.css)
Scoping Global (entire app) Local (specific component)
Naming Risk of collisions Collisions impossible
Usage Best for resets and variables Best for component-specific UI
Loading Loaded once for the app Loaded only when component is used

Key Features and Benefits

  • Automatic Code Splitting: Next.js only loads the CSS for the components currently rendered on the page. This keeps the initial bundle size small and improves performance.
  • Built-in Support: No configuration is required. Next.js supports CSS Modules out of the box, including support for Sass (.module.scss) if you install the sass package.
  • Standard CSS: You can use all standard CSS features, including Media Queries, Pseudo-classes (:hover), and Variables.

Advanced Usage

Combining Classes

To apply multiple classes or conditional classes, you can use template literals or the popular clsx utility library.

import styles from './Button.module.css';
import clsx from 'clsx';

export default function Button({ isLarge }) {
  return (
    <button className={clsx(styles.base, isLarge && styles.large)}>
      Click Me
    </button>
  );
} 

Global Selectors within Modules

If you occasionally need to target a global selector or a child component that isn't a module, you can use the :global pseudo-selector.

/* Card.module.css */
.card {
  padding: 20px;
}

/* This targets all <h1> tags specifically inside .card */
.card :global(h1) {
  margin-top: 0;
} 

Note

Global CSS files can only be imported in your root layout.js or _app.js file to prevent unexpected side effects across the application.

Warning: You cannot use "Global CSS" (non-module files) inside individual components. If you try to import a standard .css file in a component, Next.js will throw an error to encourage best practices.

Tailwind CSS

Tailwind CSS is a utility-first CSS framework that integrates deeply with Next.js. Instead of writing separate stylesheet files, you apply pre-defined utility classes directly to your JSX elements. This approach speeds up development by keeping styles and logic in the same file and ensures that your final CSS bundle only contains the classes you actually used.

Next.js provides an official integration for Tailwind, and it is the recommended styling method when starting a new project via create-next-app.

Key Concepts of Tailwind in Next.js

Tailwind works by scanning your components for class names and generating a "just-in-time" (JIT) stylesheet. This results in minimal CSS shipping to the browser.

  • Utility-First: Use classes like flex, pt-4, text-center, and bg-blue-500 to build complex UIs.
  • Responsive Design: Prefix any utility with a breakpoint (e.g., md:flex-row) to apply styles at specific screen sizes.
  • Hover & Focus: Use state modifiers like hover:bg-blue-700 or focus:ring.
  • Dark Mode: Built-in support using the dark: prefix, which toggles based on the user's system preferences or a CSS class.

Comparison: CSS Modules vs. Tailwind CSS

Feature CSS Modules Tailwind CSS
Workflow Separate .css files. Inline utility classes in JSX.
Learning Curve Low (Standard CSS). Moderate (Learning class names).
Maintainability Clean JSX, larger CSS files. Busy JSX, tiny CSS files.
Customization Manual. Configurable via tailwind.config.js.
Consistency Hard to enforce (ad-hoc). Easy via defined design tokens.

Implementation Example

In a Next.js component, Tailwind allows you to build a responsive, themed card without leaving your .js file:

// components/Card.js
export default function Card({ title, description }) {
  return (
    <div className="max-w-sm rounded overflow-hidden shadow-lg bg-white dark:bg-gray-800 p-6">
      <h2 className="font-bold text-xl mb-2 text-gray-900 dark:text-white">
        {title}
      </h2>
      <p className="text-gray-700 dark:text-gray-300 text-base">
        {description}
      </p>
      <button className="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition duration-200">
        Read More
      </button>
    </div>
  );
} 

Configuration and Optimization

The behavior of Tailwind is managed in the tailwind.config.js file. This is where you define your design system, including colors, fonts, and spacing.

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        brand: '#1DA1F2', // Custom brand color
      },
    },
  },
  plugins: [],
} 

Best Practices

  • Use the @apply directive sparingly: If you find yourself repeating many classes, you can extract them into a global CSS file using @apply, but generally, it is better to create a reusable React component instead.
  • Install the Tailwind CSS IntelliSense: This VS Code extension provides auto-completion and linting for Tailwind classes, which is essential for productivity.
  • When building dynamic class strings, these libraries help handle conditional logic and prevent class conflicts.

Note

Next.js automatically handles the purging of unused Tailwind CSS classes in production, ensuring that your CSS bundle size remains extremely small (usually under 10KB for an entire site)

Warning: Avoid using string interpolation for Tailwind classes (e.g., text-${color}). Tailwind's compiler needs to see the full class name in your code to include it in the final bundle. Use an object mapping or a complete class name instead.

CSS-in-JS

CSS-in-JS refers to a styling pattern where CSS is authored using JavaScript or TypeScript. While popular in the previous Pages Router, the move to Server Components in the App Router has fundamentally changed how these libraries work. Because Server Components do not have access to the browser's DOM or React's runtime state, traditional "runtime" CSS-in-JS libraries (like styled-components or Emotion) require specific handling.

The Evolution: Runtime vs. Compile-time

The App Router prioritizes performance and zero-bundle-size CSS. This has led to a shift toward Zero-Runtime or Compile-time CSS-in-JS.

Type How it Works App Router Compatibility
Runtime (e.g., styled-components, Emotion) Generates styles in the browser at execution time. Requires "use client" directive.
Zero-Runtime (e.g., StyleX, Panda CSS, Vanilla Extract) Extracts styles into static CSS files during the build process. Fully compatible with Server Components.

Using Traditional CSS-in-JS (Runtime)

If you must use libraries like styled-components or Emotion, you must follow these two rules:

  1. Client Boundaries: You can only use these styles within components marked with the "use client" directive.
  2. Registry Setup: You must set up a "Style Registry" in your root layout to collect styles during the server render and inject them into the HTML < head> to prevent Flash of Unstyled Content (FOUC).
// Example: Using styled-components in a Client Component
"use client";

import styled from 'styled-components';

const StyledButton = styled.button`
  background: blue;
  color: white;
  padding: 1rem;
`;

export default function Button() {
  return <StyledButton>Click Me</StyledButton>;
} 

Modern Zero-Runtime Libraries

Next.js now encourages libraries that offer the developer experience of JavaScript but the performance of static CSS.

1. StyleX

Created by Meta, StyleX is a highly optimized library that ensures styles are predictable and scalable. It is designed to work seamlessly with Server Components.

2. Panda CSS

Panda CSS uses static analysis to generate a type-safe styling engine. It offers a "styled-system" feel without the performance cost of runtime injection.

3. Vanilla Extract

Vanilla Extract acts as "CSS Modules with TypeScript." It lets you write styles in .css.ts files that are compiled away into standard CSS files at build time.

Comparison: Choosing a Styling Method

Criteria CSS Modules / Tailwind Zero-Runtime CSS-in-JS Runtime CSS-in-JS
Performance Best (Zero JS) Excellent (Zero JS) Moderate (JS overhead)
Type Safety Limited / External Native (TypeScript) Native (TypeScript)
Server Components Native Support Native Support Client-only
Bundle Size Minimal Minimal Significant

Best Practices for CSS-in-JS

  • Avoid Global Styles via JS: For global styles (resets, fonts), use a standard globals.css file instead of a CSS-in-JS provider to ensure early loading.
  • Isolate Interactivity: If you use a runtime library, wrap only the interactive elements in Client Components to keep the rest of your page as a high-performance Server Component.
  • Prefer Build-time Tools: For new projects where you want the JS-styling experience, prioritize Panda CSS or Vanilla Extract to maximize App Router benefits.

Note

Next.js provides official documentation and templates for setting up registries for styled-components, emotion, and kuma-ui.

warning: Using runtime CSS-in-JS in a Server Component without the "use client" directive will result in a build-time error because the server cannot "mount" the styles into a browser DOM.

Sass

Next.js has built-in support for Sass (Syntactically Awesome Style Sheets) after the installation of the sass package. It supports both the .scss and .sass extensions. Sass allows you to use advanced features like variables, nested rules, mixins, and functions, which are not yet available in standard CSS, making it a popular choice for managing complex design systems.

Integration and Setup

To enable Sass support, you only need to install the compiler as a development dependency. Next.js will automatically detect the files and process them.

npm install --save-dev sass 

Sass Modules

Like standard CSS, Sass is most effective when used with CSS Modules to ensure local scoping. By naming your files [name].module.scss, you gain the benefits of Sass features while preventing class name collisions.

/* variables.module.scss */
$primary-color: #0070f3;
$font-stack: Helvetica, sans-serif;

.container {
  padding: 2rem;
  font-family: $font-stack;

  .title {
    color: $primary-color;
    &:hover {
      text-decoration: underline;
    }
  }
} 
// Component.js
import styles from './variables.module.scss';

export default function Hero() {
  return (
    <div className={styles.container}>
      <h1 className={styles.title}>Hello Sass!</h1>
    </div>
  );
} 

Features and Usage Patterns

Feature Description Benefit
Variables Define values like $primary-blue. Centralized theme management.
Nesting Nest selectors inside one another. Improved readability and hierarchy.
Mixins Reusable blocks of styles (@mixin). Reduced code duplication (DRY).
Partials Split CSS into smaller files (_file.scss). Better project organization.

Features and Usage Patterns

Feature Description Benefit
Variables Define values like $primary-blue. Centralized theme management.
Nesting Nest selectors inside one another. Improved readability and hierarchy.
Mixins Reusable blocks of styles (@mixin). Reduced code duplication (DRY).
Partials Split CSS into smaller files (_file.scss). Better project organization.

Global Sass vs. Component Sass

Usage File Extension Import Location Use Case
Global globals.scss layout.js (Root) Resets, typography, global variables.
Scoped *.module.scss Any Component Component-specific styles.

Configuring Sass Options

If you need to customize the Sass compiler (e.g., to automatically include a "variables" file in every module so you don't have to @import it manually), you can do so in next.config.js.

// next.config.js
const path = require('path')

module.exports = {
  sassOptions: {
    includePaths: [path.join(__dirname, 'styles')],
    prependData: `@import "variables.scss";`, // Automatically injects into every Sass file
  },
} 

Best Practices

  • Prefer SCSS over Sass: The .scss syntax is a superset of CSS (meaning any valid CSS is valid SCSS), which makes it easier for teams to adopt and integrate existing CSS.
  • Use Modern Imports: Use @use instead of @import for better performance and to avoid namespace collisions in newer versions of Dart Sass.
  • Modularize Variables: Keep your variables, mixins, and functions in separate partials (e.g., _theme.scss) to maintain a clean architecture.

Note

Next.js uses Dart Sass by default. Features like node-sass are deprecated and should be avoided in modern Next.js projects.

Warning: Just like CSS Modules, importing a non-module Sass file (e.g., styles.scss) inside a component other than layout.js will result in an error.

Optimizing Last updated: Feb. 28, 2026, 7:28 p.m.

Optimization in Next.js is "built-in" rather than "plugged-in." This section focuses on specialized components that handle common performance bottlenecks automatically. The Image Component, for example, performs lazy loading, prevents layout shift, and automatically serves images in modern formats like WebP or AVIF. Similarly, the Font Component optimizes web fonts by self-hosting them and removing external network requests, which significantly improves Cumulative Layout Shift (CLS) scores.

Beyond assets, the framework optimizes code execution through Script Loading strategies and Metadata management. By using the Metadata API, you can define SEO-friendly titles, descriptions, and Open Graph images that are streamed to the browser in the correct order. These optimizations work together to ensure that a Next.js site scores high on Core Web Vitals, providing a better experience for users and better rankings in search engines.

Images (next/image)

The Next.js Image Component (next/image) is an extension of the HTML element, evolved to handle the complexities of modern web performance automatically. It solves common issues like slow page loads, layout shift (CLS), and unoptimized file sizes by implementing industry best practices out of the box.

Core Features of next/image

    The component automates several optimization techniques that would otherwise require manual configuration:
  • Size Optimization: Automatically serves correctly sized images for each device, using modern formats like WebP and AVIF.
  • Visual Stability: Automatically prevents Cumulative Layout Shift (CLS) by requiring dimensions or using placeholders.
  • Faster Page Loads: Images are only loaded when they enter the viewport using native browser lazy loading, with optional blur-up placeholders.
  • Asset Flexibility: On-demand image resizing, even for images stored on remote servers (like S3 or a CMS).
    Implementation Patterns
  1. Local Images
  2. For local images (stored in your /public folder), you can import the file directly. Next.js will automatically determine the width and height of the file.

    import Image from 'next/image';
    import profilePic from '../public/me.png';
    
    export default function Page() {
      return (
        <Image
          src={profilePic}
          alt="Picture of the author"
          // width and height are automatically provided
          placeholder="blur" // Optional blur-up effect
        />
      );
    } 
  3. Remote Images
  4. For images hosted on an external URL, you must provide the width and height manually (to prevent layout shift) and configure the domain in your next.config.js for security.

     // app/page.js
    import Image from 'next/image';
    
    export default function Page() {
      return (
        <Image
          src="https://example.com/hero.jpg"
          alt="Hero Image"
          width={800}
          height={500}
        />
      );
    }

Key Props Comparison

Prop Type Description
src String / Object The path to the image (required).
alt String Description for accessibility (required).
width / height Number Dimensions in pixels (required for remote images).
fill Boolean Causes image to fill parent element (ignores width/height).
priority Boolean Loads image immediately (use for LCP images).
quality 1 - 100 Compression quality (default is 75).

The fill Layout Pattern

When you don't know the exact dimensions of an image (e.g., in a responsive gallery), use the fill prop. The image will stretch to fit its parent container. Note: The parent must have position: relative, position: fixed, or position: absolute.

<div style={{ position: 'relative', height: '400px' }}>
  <Image
    src="/background.jpg"
    alt="Background"
    fill
    className="object-cover" // Uses CSS object-fit
  />
</div> 

Security: Remote Patterns

To protect your application from malicious users trying to use your image optimization endpoint for external images, you must define allowed domains in next.config.js.

 // next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 's3.amazonaws.com',
        port: '',
        pathname: '/my-bucket/**',
      },
    ],
  },
}

Note

Priority Images should be used for the largest image visible in the viewport (the LCP element). Adding the priority prop tells Next.js to preload the image, which can significantly improve your Core Web Vitals.

Warning: Avoid using next/image for small icons or purely decorative elements that don't need optimization; standard < img > tags or SVGs are often more efficient for those specific cases.

Fonts (next/font)

Next.js includes a built-in font optimization system via next/font. It automatically self-hosts any Google Font or local font file, eliminating external network requests to Google’s servers. This ensures your fonts are privacy-compliant and prevents Flash of Unstyled Text (FOUT) by pre-loading the font files at build time.

    Key Benefits

  • Self-hosting: Next.js downloads the font files during the build process and serves them from your own domain.
  • Zero Layout Shift: It automatically calculates the size of the font and uses the size-adjust CSS property to match the fallback font, keeping Cumulative Layout Shift (CLS) at zero.
  • Performance: No extra browser requests to external font providers are made.
  • Privacy: Since no requests are sent to Google, it is fully GDPR compliant.

Using Google Fonts

You can import any Google Font as a function. It is recommended to define these in a centralized file (like app/fonts.js) or directly in your layout.js.

 // app/layout.js
import { Inter, Roboto_Mono } from 'next/font/google';

// Configure the font
const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // Prevents FOUT
  variable: '--font-inter', // Useful for Tailwind integration
});

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

Using Local Fonts

For custom fonts not available on Google Fonts, use next/font/local. You must provide the path to your font file relative to the component.

 import localFont from 'next/font/local';

const myFont = localFont({
  src: './my-font.woff2',
  display: 'swap',
});

export default function Page() {
  return <h1 className={myFont.className}>Hello Custom Font</h1>;
}

Implementation Strategies

Feature Google Fonts Local Fonts
Import Source next/font/google next/font/local
Setup Function call with name. Path to file in src.
Subsetting Automated (reduces file size). Manual (based on file).
Variable Fonts Supported (Best performance). Supported.

    Tailwind CSS Integration

    To use next/font with Tailwind, you should use the CSS variable strategy. This allows you to reference the font in your tailwind.config.js.

  1. Define the variable in your font configuration:
    const inter = Inter({
      subsets: ['latin'],
      variable: '--font-inter',
    }); 
  2. Add the variable to the HTML class: < className={inter.variable} > .
  3. Update tailwind.config.js
     module.exports = {
      theme: {
        extend: {
          fontFamily: {
            sans: ['var(--font-inter)'],
          },
        },
      },
    }
    Best Practices
  • Use Variable Fonts: If a font has a variable version, use it. It packs all weights (bold, light, etc.) into one small file instead of loading separate files for each weight.
  • Subset Your Fonts: Only load the characters you need (e.g., subsets: ['latin']) to keep the font file size as small as possible.
  • Limit Font Counts: Each additional font family adds to the initial download. Stick to 1–2 font families for optimal performance.

Note

When using Google Fonts, Next.js will automatically use the variable version of the font if it is available, which significantly reduces the number of files the browser has to download.

Scripts (next/script)

The Next.js Script component (next/script) is an extension of the HTML element. It allows you to optimize the loading of third-party scripts (like Google Analytics, ads, or customer support widgets) by giving you control over when and how they execute. This prevents heavy external scripts from blocking the main thread and slowing down your page's initial load.

Core Strategies: The strategy Prop

The most powerful feature of next/script is the strategy property, which determines the loading priority of the script.

Strategy When it Loads Best Use Case
beforeInteractive Before Next.js code and before page hydration. Critical scripts like bot detection or polyfills.
afterInteractive (Default) After the page is hydrated. Most common; tag managers, analytics, and ads.
lazyOnload During idle time, after all other resources. Chat widgets, social media embeds, or low-priority scripts.
worker (Experimental) Inside a Web Worker. Offloading heavy scripts to a background thread.

Implementation Examples

1. Basic Third-Party Script

Loading an external library like Google Analytics is straightforward. Using the default afterInteractive strategy ensures the script doesn't compete with your application's core logic.

import Script from 'next/script'

export default function Dashboard() {
  return (
    <>
      <Script 
        src="https://example.com/analytics.js" 
        strategy="afterInteractive"
      />
      <h1>User Dashboard</h1>
    </>
  )
} 

2. Executing Inline Scripts

You can also use next/script to execute inline code by wrapping the code in template literals.

<Script id="show-banner" strategy="afterInteractive">
  {`console.log('Banner script loaded');`}
</Script> 

Handling Events

Third-party scripts often require initialization after they finish loading. next/script provides attributes to handle these lifecycle events:

  • onLoad: Executes code once the script has finished loading.
  • onReady: Executes code every time the component is mounted (useful if the script needs to re-run on navigation).
  • onError: Executes code if the script fails to load.
<Script
  src="https://example.com/external-lib.js"
  onLoad={() => {
    console.log('Script has loaded!')
  }}
/> 

Offloading to Web Workers (worker)

The worker strategy (experimental) uses Partytown to move scripts off the main thread and into a Web Worker. This is the ultimate optimization for heavy marketing or tracking scripts that otherwise degrade performance scores.

Requirements:

  • You must enable the nextScriptWorkers flag in next.config.js.
  • You must install the @builder.io/partytown package.

Best Practices

  • Use afterInteractive by default: This is the safest balance between functionality and performance.
  • Avoid beforeInteractive unless necessary: This strategy can delay your page's "Time to Interactive" because it blocks hydration.
  • Always provide an id: When using inline scripts, an id is required so Next.js can track and optimize the script effectively.
  • Global Scripts: If a script needs to run on every page, place it in your Root Layout (app/layout.js).

Note

Scripts loaded via next/script are automatically deduplicated. If multiple components import the same script, Next.js will only load it once.

Metadata (SEO)

The Next.js Script component (next/script) is an extension of the HTML element. It allows you to optimize the loading of third-party scripts (like Google Analytics, ads, or customer support widgets) by giving you control over when and how they execute. This prevents heavy external scripts from blocking the main thread and slowing down your page's initial load.

Core Strategies: The strategy Prop

The most powerful feature of next/script is the strategy property, which determines the loading priority of the script.

Strategy When it Loads Best Use Case
beforeInteractive Before Next.js code and before page hydration. Critical scripts like bot detection or polyfills.
afterInteractive (Default) After the page is hydrated. Most common; tag managers, analytics, and ads.
lazyOnload During idle time, after all other resources. Chat widgets, social media embeds, or low-priority scripts.
worker (Experimental) Inside a Web Worker. Offloading heavy scripts to a background thread.

Implementation Examples

1. Basic Third-Party Script

Loading an external library like Google Analytics is straightforward. Using the default afterInteractive strategy ensures the script doesn't compete with your application's core logic.

import Script from 'next/script'

export default function Dashboard() {
  return (
    <>
      <Script 
        src="https://example.com/analytics.js" 
        strategy="afterInteractive"
      />
      <h1>User Dashboard</h1>
    </>
  )
} 

2. Executing Inline Scripts

You can also use next/script to execute inline code by wrapping the code in template literals.

<Script id="show-banner" strategy="afterInteractive">
  {`console.log('Banner script loaded');`}
</Script> 

Handling Events

Third-party scripts often require initialization after they finish loading. next/script provides attributes to handle these lifecycle events:

  • onLoad: Executes code once the script has finished loading.
  • onReady: Executes code every time the component is mounted (useful if the script needs to re-run on navigation).
  • onError: Executes code if the script fails to load.
<Script
  src="https://example.com/external-lib.js"
  onLoad={() => {
    console.log('Script has loaded!')
  }}
/> 

Offloading to Web Workers (worker)

The worker strategy (experimental) uses Partytown to move scripts off the main thread and into a Web Worker. This is the ultimate optimization for heavy marketing or tracking scripts that otherwise degrade performance scores.

Requirements:

  • You must enable the nextScriptWorkers flag in next.config.js.
  • You must install the @builder.io/partytown package.

Best Practices

  • Use afterInteractive by default: This is the safest balance between functionality and performance.
  • Avoid beforeInteractive unless necessary: This strategy can delay your page's "Time to Interactive" because it blocks hydration.
  • Always provide an id: When using inline scripts, an id is required so Next.js can track and optimize the script effectively.
  • Global Scripts: If a script needs to run on every page, place it in your Root Layout (app/layout.js).

Note

Scripts loaded via next/script are automatically deduplicated. If multiple components import the same script, Next.js will only load it once.

Lazy Loading

Lazy Loading in Next.js is an optimization technique that defers the loading of Client Components and external libraries until they are actually needed. By reducing the amount of JavaScript required to render a page initially, you can significantly improve the First Contentful Paint (FCP) and Time to Interactive (TTI).

Implementation Methods

Method Best Use Case Implementation
next/dynamic Heavy Client Components (e.g., Modals, Charts, Maps). const Component = dynamic(() => import('./Comp'))
import() External libraries used only after a user action (e.g., PDF generation). const lib = await import('library-name')
Suspense Showing a "loading" state while the component is being fetched. <Suspense fallback={<Loading />}>...</Suspense>

Using next/dynamic

next/dynamic is a composite of React.lazy() and Suspense. It allows you to import a component and specify that it should only be sent to the browser when it is required.

1. Basic Usage

In this example, the HeavyChart component will be put into a separate JavaScript bundle (chunk) and won't be loaded until the page renders.

import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <p>Loading Chart...</p>,
});

export default function Page() {
  return (
    <div>
      <h1>Analytics Dashboard</h1>
      <HeavyChart />
    </div>
  );
} 

2. Disabling SSR

Sometimes a component relies on browser-only APIs (like window or document) and will crash if rendered on the server. You can disable server-side rendering for these components using the ssr option.

const MapComponent = dynamic(() => import('@/components/Map'), {
  ssr: false, // This component will only render in the browser
}); 

Lazy Loading External Libraries

If you have a large library (like fuse.js for search or canvas-confetti), you shouldn't include it in the main bundle if it’s only used when a user clicks a button. Instead, use an async import() inside an event handler.

'use client'

export default function SearchButton() {
  const handleSearch = async () => {
    // Library is only downloaded when user clicks
    const Fuse = (await import('fuse.js')).default;
    const fuse = new Fuse(data);
    // ... perform search logic
  };

  return <button onClick={handleSearch}>Search Items</button>;
} 

Visual Comparison: Standard vs. Lazy Loading

Feature Standard Import Lazy Loading (next/dynamic)
Initial JS Payload High (All components included). Low (Deferred components excluded).
Network Requests One large bundle. Multiple small, on-demand chunks.
User Experience Slower initial load. Faster initial load; slight delay on first use.
SEO Impact Minimal (Standard). Minimal (Next.js handles SEO for dynamic components).

Best Practices

  • Target the "Below the Fold" Content: Only lazy load components that the user won't see immediately upon landing on the page (e.g., comments sections, footers, or modals).
  • Use Suspense for Server Components: For data-heavy Server Components, use React Suspense boundaries instead of next/dynamic to stream the UI as data becomes available.
  • Provide Fallbacks: Always use a loading skeleton or spinner to avoid a "popping" effect when the lazy-loaded component finally arrives.

Note

next/dynamic is specifically for Client Components. If you try to use it on a Server Component, it will automatically treat the imported component as a Client Component.

Analytics & Instrumentation

Content goes here...

Configuring Last updated: Feb. 28, 2026, 7:30 p.m.

While Next.js is famous for its "Zero Config" philosophy, the next.config.js file serves as the command center for custom requirements. This configuration file allows you to define environment-specific variables, set up redirects and rewrites, and configure experimental features. It is also where you manage build-time optimizations, such as enabling the "standalone" output mode for Docker deployments or integrating with external services through custom headers.

Configuration also extends to the TypeScript and ESLint integrations. Next.js provides a custom TypeScript plugin that offers "Intellisense" for framework-specific features like route segments and metadata. By fine-tuning these settings, you can enforce coding standards and catch errors early in the development cycle, ensuring that your project remains maintainable as it grows from a small prototype into a large-scale application.

TypeScript

Next.js provides a TypeScript-first experience, offering built-in support that automatically handles configuration, provides type safety across your application, and improves developer productivity with advanced IDE autocompletion.

Automatic Setup

Next.js simplifies the transition to TypeScript. If you create a new project via create-next-app, you can select TypeScript as the default. For existing projects, you simply need to:

  1. Rename your files from .js / .jsx to .ts / .tsx.
  2. Run the development server (npm run dev).
  3. Next.js will detect the files and automatically install the necessary types (@types/react, @types/node) and create a tsconfig.json for you.

Key Features

Feature Description
Strict Type Checking Integrated plugin that provides faster type checking during development.
Static Typed Routes (Experimental) Prevents broken links by ensuring your href values match existing routes.
Server Component Types Full support for async/await patterns in Server Components.
Path Aliases Automatically configures tsconfig.json to support @/ imports.

Type Safety in the App Router

One of the biggest advantages of the App Router is how it handles types for dynamic segments and data fetching.

1. Typing Page and Layout Props

Next.js provides specific types for the props passed into special files like Page, Layout, and generateMetadata.

// app/blog/[slug]/page.tsx
interface PageProps {
  params: Promise<{ slug: string }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export default async function Page({ params, searchParams }: PageProps) {
  const { slug } = await params;
  const query = await searchParams;
  
  return <h1>Post: {slug}</h1>;
} 

2. Typed fetch and Data

Because Server Components are async, you can define the shape of your API responses directly to ensure the rest of your component is type-safe.

interface User {
  id: number;
  name: string;
}

async function getUser(): Promise {
  const res = await fetch('https://api.example.com/user');
  return res.json();
} 

Statically Typed Links (Experimental)

Next.js can provide type safety for your next/link components. This prevents typos in your URLs by alerting you if you try to link to a path that does not exist in your app directory.

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    typedRoutes: true,
  },
}
module.exports = nextConfig 

Best Practices

  • Use tsx for UI Components: Always use .tsx for files containing JSX and .ts for logic-only files.
  • Leverage next/cache types: When using unstable_cache or revalidateTag, ensure your keys are strictly typed to avoid "cache-miss" bugs.
  • Avoid any: Since Next.js provides many built-in types (e.g., NextRequest, Metadata, ResolvingMetadata), utilize them instead of resorting to any.
  • Server Actions: Type your Form Data and return states using libraries like zod to ensure the boundary between client and server is secure.

Note

Next.js uses a custom TypeScript plugin that runs in your IDE. This plugin provides better error messages and ensures that your tsconfig stays compatible with the Next.js build process.

Warning: Do not modify the include or exclude arrays in tsconfig.json manually unless you are sure; Next.js manages these to ensure all necessary files (including those in .next/types) are correctly indexed.

ESLint

Next.js provides an out-of-the-box ESLint experience that integrates directly into the build process. It includes a custom plugin, eslint-plugin-next, which catches common framework-specific mistakes—such as using the wrong < a> tag instead of next/link or improper font loading—before they reach production.

Setup and Configuration

When you run next dev for the first time on a project that doesn't have ESLint, Next.js will prompt you to set it up. It creates an .eslintrc.json file in your root directory.

Core Configuration

Next.js offers two primary configuration levels:

Level Config Setting Description
Base next Includes the core Next.js rules.
Recommended next/core-web-vitals Stricter ruleset that helps meet Core Web Vitals thresholds.
{
  "extends": "next/core-web-vitals"
} 

Key Next.js Rules

The eslint-plugin-next checks for issues that can impact performance, SEO, and accessibility.

  • no-html-link-for-pages: Warns if you use a standard < a> tag to navigate to an internal route, which would cause a full page refresh instead of client-side navigation.
  • no-img-element: Encourages the use of next/image to prevent unoptimized images and layout shifts.
  • no-sync-scripts: Flags synchronous scripts that could block page parsing and slow down performance.
  • google-font-display: Ensures you are using font-display: swap or similar to maintain text visibility during font loading.

Linting During the Build

By default, Next.js runs ESLint every time you execute next build. If there are errors, the build will fail; if there are warnings, the build will finish but display the logs.

Customizing Linting Behavior

You can modify how Next.js handles linting in your next.config.js file:

// next.config.js
module.exports = {
  eslint: {
    // Warning: This allows production builds to successfully complete even if
    // your project has ESLint errors.
    ignoreDuringBuilds: true,
  },
} 

Integration with Other Tools

  • Prettier: To prevent conflicts between ESLint and Prettier, you typically install eslint-config-prettier and add it to your extends array.
  • Custom Directories: If you store your code in directories other than app/, pages/, components/, etc., you can tell Next.js to lint them:
module.exports = {
  eslint: {
    dirs: ['pages', 'utils', 'lib'], // Only lint these directories
  },
} 

Best Practices

  • Always use next/core-web-vitals: This helps catch performance regressions early in the development cycle.
  • Editor Integration: Install the ESLint extension for your IDE (like VS Code) to see real-time feedback as you code.
  • Husky and lint-staged: Use these tools to run linting automatically on your changed files before every git commit to ensure code quality remains high.

Note

Note: If you already have an existing ESLint configuration, Next.js will attempt to merge its rules. Ensure next or next/core-web-vitals is listed last in your extends array to prevent other configs from overriding framework-specific optimizations.

Environment Variables

Next.js has built-in support for environment variables, allowing you to separate secrets and configuration from your code. It handles .env file loading automatically and provides a secure way to distinguish between variables meant only for the server and those that need to be accessible in the browser.

Loading Environment Variables

Next.js looks for specific .env files in your project root and loads them in the following order of priority (from highest to lowest):

  1. .env.development.local (or .env.production.local)
  2. .env.local (Always overrides defaults; ignored by Git)
  3. .env.development (or .env.production)
  4. .env (The default fallback)
Environment Purpose Should Git Ignore?
.env Default values for all environments. No
.env.local Local overrides for secrets (DB passwords, API keys). Yes
.env.production Production-specific configurations. No

Server-side vs. Client-side

By default, environment variables are only available on the Node.js server. This prevents sensitive information, like database credentials or private API keys, from being leaked to the browser.

1. Server-only Variables

These are accessed via process.env.VARIABLE_NAME. If you try to access these in a Client Component, they will be undefined.

// This works in Server Components and Route Handlers
const apiKey = process.env.STRIPE_SECRET_KEY; 

2. Client-side Variables (Public)

To expose a variable to the browser, you must prefix it with NEXT_PUBLIC_. Next.js will "inline" these values into the JavaScript bundle at build time.

// This is accessible in both Client and Server components
const analyticsId = process.env.NEXT_PUBLIC_ANALYTICS_ID; 

Type Safety with TypeScript

To get autocompletion and prevent typos when using environment variables, you can define a global type definition file (env.d.ts or similar).

// types/env.d.ts
declare namespace NodeJS {
  interface ProcessEnv {
    DATABASE_URL: string;
    NEXT_PUBLIC_API_URL: string;
    STRIPE_SECRET_KEY: string;
  }
}

Environment Variables on Vercel (Production)

When deploying to platforms like Vercel, you do not upload your .env.local files. Instead, you manage them through the dashboard:

  • Encryption: Values are encrypted at rest.
  • Environments: You can assign different values to Production, Preview (for PRs), and Development.
  • System Variables: Vercel provides automatic variables like VERCEL_URL to help with dynamic link generation.

Best Practices

  • Never commit .env*.local files: Always add these to your .gitignore to prevent leaking secrets.
  • Default to Server-only: Libraries like t3-env or zod can validate that all required environment variables are present at build time, preventing "undefined" crashes in production.
  • Default to Server-only: Only use the NEXT_PUBLIC_ prefix if the variable is strictly required for client-side logic (e.g., a Firebase public key).
  • Restart on Change: If you modify your .env files while the development server is running, you must restart the server for the changes to take effect.

Warning: Be careful not to log process.env in Client Components, as this can inadvertently expose public variables or cause build-time errors if server-only variables are referenced.

Absolute Imports & Aliases

As Next.js projects grow, managing relative import paths (e.g., ../../../components/Button) becomes cumbersome and error-prone. Absolute Imports and Module Path Aliases allow you to define clean, consistent shortcuts for your directories, making your code more readable and easier to refactor.

How Aliases Work

By default, Next.js (starting from version 13.1) automatically configures a @/* alias that points to the root of your project. This allows you to import components using a consistent prefix regardless of where the file is located in your directory structure.

Feature Relative Imports Absolute Imports / Aliases
Syntax ../../components/Header @/components/Header
Refactoring Breaks if file is moved. Remains valid if file is moved.
Readability Difficult to trace deep nesting. Clear and predictable.
Setup No setup required. Configured in tsconfig.json.

Configuration

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./*"],
      "@components/*": ["components/*"],
      "@utils/*": ["lib/utils/*"],
      "@styles/*": ["styles/*"]
    }
  }
} 
  • pathspaths: The root directory for resolving non-relative module names.
  • paths: A mapping of aliases to specific directories relative to the baseUrl.

Implementation Example

Without aliases, importing a utility function into a deeply nested page looks like this:

// app/dashboard/settings/profile/security/page.js
import { formatData } from '../../../../../lib/utils/format'; 

With the @/ alias configured:

// app/dashboard/settings/profile/security/page.js
import { formatData } from '@/lib/utils/format'; 

Integration with Tools

Next.js ensures that these aliases work seamlessly across all your development tools:

  • Next.js Compiler: Automatically resolves these paths during build and development.
  • VS Code: Automatically picks up the paths from tsconfig.json to provide accurate IntelliSense and "Go to Definition" support.
  • ESLint: If using the Next.js ESLint config, it will recognize these aliases without extra configuration.
  • Jest: If you have a custom Jest setup, you may need to map these aliases in your jest.config.js using moduleNameMapper.

Best Practices

  • Stick to @/: The standard @/ prefix is widely recognized in the Next.js community and is the default for create-next-app.
  • Avoid Over-Aliasing: Don't create an alias for every single folder. Stick to major directories like components, lib, hooks, and types.
  • Use the src Directory: If you use a src folder (e.g., src/app/), ensure your paths mapping reflects that: "@/*": ["./src/*"].
  • Restart Your IDE: If you modify tsconfig.json and don't see immediate autocompletion changes, restart your TypeScript server or your IDE.

Note

Absolute imports are technically different from Aliases. An Absolute Import allows you to import from the root without a prefix (e.g., components/Button), while an Alias uses a special character prefix (e.g., @components/Button). Aliases are generally preferred to avoid confusion with npm packages.

MDX (Markdown)

MDX allows you to write JSX (React components) directly inside your Markdown files. It combines the readable, easy-to-write syntax of Markdown with the dynamic power of React. In Next.js, MDX is a popular choice for building content-heavy sites like blogs, documentation, and marketing pages.

How MDX Works in Next.js

Next.js provides an official package, @next/mdx, that transforms MDX files into pages. When a user navigates to an .mdx file, the server compiles the Markdown into HTML and the JSX into interactive React components.

Feature Standard Markdown (.md) MDX (.mdx)
Content Text, Links, Images. Text, Links, Images + React Components.
Logic Static only. Supports variables, loops, and conditional rendering.
Interactivity None (HTML only). Full (State, Hooks, Click events).
Complexity Simple. Powerful, but requires a build step.

Implementation Setup

To use MDX in the App Router, you need to install the necessary dependencies and create a configuration file.

1. Install Dependencies:

npm install @next/mdx @mdx-js/loader @mdx-js/react 

2. Configure next.config.mjs:

import nextMDX from '@next/mdx'

const withMDX = nextMDX({
  // Options for markdown plugins go here
})

export default withMDX({
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
}) 

3. Add mdx-components.tsx: This file is required in the root of your project to define how Markdown elements (like < h1> or < a>) are rendered.

Writing MDX Content

Once configured, you can create a file at app/blog/page.mdx. You can use standard Markdown and drop in any React component:

import { Chart } from '@/components/Chart'

# My Awesome Post

Here is a list of features:
* Built-in SEO
* Fast loading

<Chart data={...} />

This chart is a live React component rendered inside Markdown! 

Remote MDX vs. Local MDX

Depending on where your content lives, you have two choices for implementation:

Approach Source Best For Recommended Library
Local MDX Files inside the app/ folder. Small blogs, simple docs. @next/mdx
Remote MDX CMS, Database, or GitHub. Large-scale content, dynamic editors. next-mdx-remote or contentlayer

Enhancing MDX with Plugins

MDX supports the unified ecosystem, allowing you to use remark (for Markdown) and rehype (for HTML) plugins to add features like:

  • Syntax Highlighting: Use rehype-highlight or rehype-pretty-code.
  • Table of Contents: Use remark-toc.
  • Math Equations: Use remark-math and rehype-katex.
  • GitHub Flavored Markdown: Use remark-gfm.

Best Practices

  • Use custom components: Map Markdown tags to your own UI library (like Tailwind components) in mdx-components.tsx for a consistent design system.
  • Metadata (Frontmatter): Use libraries like gray-matter or remark-frontmatter if you need to store metadata (date, author, title) at the top of your MDX files.
  • Server Components: Since MDX files in the App Router are Server Components by default, keep them lightweight and only use "use client" for specific interactive components imported into the MDX.

Note

The mdx-components.tsx file must be in your project root (or inside src/) for Next.js to properly map Markdown tags to React components.

Testing Last updated: Feb. 28, 2026, 7:30 p.m.

Testing a Next.js application involves a multi-layered approach to ensure both logic and UI function correctly. For unit and integration testing, Vitest or Jest are commonly used alongside the React Testing Library to verify individual components and hooks. Since the App Router heavily utilizes Server Components, testing strategies often involve mocking the fetch API or using specialized utilities to simulate the server environment.

For End-to-End (E2E) Testing, tools like Playwright or Cypress are recommended. These tools test the entire application flow by automating a browser, which is crucial for verifying that complex features like Authentication, Server Actions, and navigation work as expected from a user's perspective. By integrating these tests into a Continuous Integration (CI) pipeline, you can catch regressions before they are deployed to production.

Vitest

Vitest is a modern, blazing-fast unit testing framework built on top of Vite. In the Next.js ecosystem, it has rapidly become the preferred alternative to Jest due to its superior speed, native support for ES Modules (ESM), and out-of-the-box compatibility with TypeScript.

While Next.js doesn't use Vite for its build process (it uses Webpack or Turbopack), Vitest is highly compatible with the Next.js environment for testing logic, hooks, and individual components.

Why Choose Vitest over Jest?

Feature Vitest Jest
Speed Faster (uses Vite's transformation). Slower (requires heavy transpilation).
HMR Native support for instant re-runs. Limited watch mode.
Compatibility Built-in ESM and TypeScript support. Requires complex Babel/swc config.
API Compatible with Jest (mostly 1:1). Industry standard for a decade.
Environment Seamlessly integrates with JSDOM/Happy DOM. Requires separate setup.

Basic Setup

To integrate Vitest into a Next.js project, you need the core runner and a library for DOM simulation if you're testing components.

1. Installation:

npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom 

Configuration (vitest.config.ts):

import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom', // Simulates browser for component testing
    globals: true,        // Allows using 'describe', 'it', 'expect' without imports
    setupFiles: './vitest.setup.ts',
  },
}) 

Testing Components vs. Logic

1. Unit Testing Logic

For pure functions (utilities, math, data formatting), Vitest is extremely efficient as it doesn't need to mount any UI.

// utils/math.test.ts
import { add } from './math'

test('adds 1 + 2 to equal 3', () => {
  expect(add(1, 2)).toBe(3)
}) 

2. Component Testing

When testing React components, combine Vitest with React Testing Library.

// components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import Button from './Button'

it('updates label on click', () => {
  render(<Button />)
  const btn = screen.getByRole('button')
  fireEvent.click(btn)
  expect(btn).toHaveTextContent('Clicked!')
}) 

Mocking in Next.js

Vitest provides a robust mocking system (vi) to handle Next.js-specific features like next/navigation or next/image.

import { vi } from 'vitest'

// Mocking the Next.js router
vi.mock('next/navigation', () => ({
  useRouter: () => ({
    push: vi.fn(),
  }),
})) 

Best Practices

  • Use jsdom sparingly: Only use the browser environment for component tests. Use the default node environment for utility/logic tests to maximize speed.
  • Avoid Mocking Everything: Try to test components as the user would see them. Only mock external APIs or heavy third-party libraries.
  • In-source Testing: Vitest allows you to write tests directly inside your source code files (similar to Rust). This is great for small utility functions, though optional.
  • Watch Mode: Keep Vitest running in watch mode during development. It only re-runs tests affected by the files you change, providing instant feedback.

Note

Because Next.js App Router uses Server Components, testing them with Vitest can be tricky as they are async. For complex Server Component logic, integration tests with Playwright or Cypress are often more reliable.

Jest

Vitest is a modern, blazing-fast unit testing framework built on top of Vite. In the Next.js ecosystem, it has rapidly become the preferred alternative to Jest due to its superior speed, native support for ES Modules (ESM), and out-of-the-box compatibility with TypeScript.

While Next.js doesn't use Vite for its build process (it uses Webpack or Turbopack), Vitest is highly compatible with the Next.js environment for testing logic, hooks, and individual components.

Why Choose Vitest over Jest?

Feature Vitest Jest
Speed Faster (uses Vite's transformation). Slower (requires heavy transpilation).
HMR Native support for instant re-runs. Limited watch mode.
Compatibility Built-in ESM and TypeScript support. Requires complex Babel/swc config.
API Compatible with Jest (mostly 1:1). Industry standard for a decade.
Environment Seamlessly integrates with JSDOM/Happy DOM. Requires separate setup.

Basic Setup

To integrate Vitest into a Next.js project, you need the core runner and a library for DOM simulation if you're testing components.

1. Installation:

npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom 

Configuration (vitest.config.ts):

import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom', // Simulates browser for component testing
    globals: true,        // Allows using 'describe', 'it', 'expect' without imports
    setupFiles: './vitest.setup.ts',
  },
}) 

Testing Components vs. Logic

1. Unit Testing Logic

For pure functions (utilities, math, data formatting), Vitest is extremely efficient as it doesn't need to mount any UI.

// utils/math.test.ts
import { add } from './math'

test('adds 1 + 2 to equal 3', () => {
  expect(add(1, 2)).toBe(3)
}) 

2. Component Testing

When testing React components, combine Vitest with React Testing Library.

// components/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import Button from './Button'

it('updates label on click', () => {
  render(<Button />)
  const btn = screen.getByRole('button')
  fireEvent.click(btn)
  expect(btn).toHaveTextContent('Clicked!')
}) 

Mocking in Next.js

Vitest provides a robust mocking system (vi) to handle Next.js-specific features like next/navigation or next/image.

import { vi } from 'vitest'

// Mocking the Next.js router
vi.mock('next/navigation', () => ({
  useRouter: () => ({
    push: vi.fn(),
  }),
})) 

Best Practices

  • Use jsdom sparingly: Only use the browser environment for component tests. Use the default node environment for utility/logic tests to maximize speed.
  • Avoid Mocking Everything: Try to test components as the user would see them. Only mock external APIs or heavy third-party libraries.
  • In-source Testing: Vitest allows you to write tests directly inside your source code files (similar to Rust). This is great for small utility functions, though optional.
  • Watch Mode: Keep Vitest running in watch mode during development. It only re-runs tests affected by the files you change, providing instant feedback.

Note

Because Next.js App Router uses Server Components, testing them with Vitest can be tricky as they are async. For complex Server Component logic, integration tests with Playwright or Cypress are often more reliable.

Playwright

While Unit Testing (Vitest/Jest) focuses on individual functions and components, Playwright is designed for End-to-End (E2E) testing. It launches a real browser to navigate your Next.js application exactly like a user would. This is the most reliable way to test critical user flows, such as authentication, checkout processes, and complex forms.

Why Use Playwright for Next.js?

Playwright is the industry-standard choice for Next.js because it supports all modern rendering patterns (SSR, SSG, and ISR) and works seamlessly with the App Router.

Feature Benefit
Multi-Browser Test simultaneously on Chromium (Chrome/Edge), WebKit (Safari), and Firefox.
Auto-Waiting Automatically waits for elements to be visible/actionable before clicking, reducing "flaky" tests.
Trace Viewer Records a full video and "timeline" of the test, allowing you to inspect the DOM at every step of a failure.
Mobile Emulation Built-in profiles to test how your site looks and behaves on an iPhone or Android device.

Setup and Configuration

Next.js makes it easy to add Playwright. You can use the official initializer:

1. Installation:

npm init playwright@latest 

Configuration (playwright.config.ts): Ensure Playwright knows to start your Next.js server before running tests so you don't have to start it manually.

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
}); 

Writing an E2E Test

Playwright tests are written in a natural, async syntax. Unlike unit tests, you don't mock the router or the database; you interact with the real UI.

// tests/auth.spec.ts
import { test, expect } from '@playwright/test';

test('user can log in successfully', async ({ page }) => {
  // 1. Navigate to the page
  await page.goto('/login');

  // 2. Interact with the form
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Log in' }).click();

  // 3. Assert the outcome
  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByText('Welcome back!')).toBeVisible();
}); 

Testing Server Components and SEO

Since Playwright renders the full page, it is the best tool for verifying that Server Components are fetching data correctly and that Metadata is properly appearing in the < head>.

test('SEO metadata is correct', async ({ page }) => {
  await page.goto('/blog/nextjs-guide');
  
  // Check the document title (Server-side generated)
  await expect(page).toHaveTitle(/Next.js Guide/);
  
  // Check OpenGraph tags
  const description = await page.locator('meta[name="description"]').getAttribute('content');
  expect(description).toBe('A comprehensive guide to Next.js.');
}); 

Best Practices

  • Use Locators, not Selectors: Always prefer page.getByRole() or page.getByLabel() over CSS selectors like .btn-primary. This makes tests more resilient to design changes and ensures your site is accessible.
  • Test the "Happy Path" first: Prioritize the most important journeys (e.g., "Sign Up" or "Add to Cart").
  • Use Codegen: Run npx playwright codegen to open a browser window that records your actions and automatically generates the test code for you.
  • Parallel Execution: Playwright runs tests in parallel by default. Ensure your test database can handle multiple simultaneous requests or use unique IDs for test data.

Note

For CI/CD environments (like GitHub Actions), Playwright provides an official Docker image and Action that handles the installation of browser binaries automatically.

Cypress

Cypress is a popular, developer-friendly end-to-end (E2E) testing framework. Unlike Playwright, which controls the browser from the "outside" via a protocol, Cypress runs inside the browser alongside your application code. This gives it unique access to the application’s state, network requests, and DOM elements, making it an excellent tool for testing interactive Next.js Client Components and complex user flows.

Key Characteristics of Cypres

Cypress is known for its "all-in-one" approach, providing a built-in test runner with a graphical user interface (GUI) that allows you to time-travel through test steps.

Feature Description Benefit
Real-time Reloads Automatically re-runs tests as you save your spec files. Fast developer feedback loop.
Time Travel Hover over commands to see exactly what the app looked like at that moment. Simplified debugging of failed tests.
Network Control Easily stub or spy on API requests using cy.intercept(). Test edge cases (like 500 errors) without a real backend.
Component Testing Dedicated mode to test React components in isolation. Faster than full E2E while still using a real browser.

Setup and Installation

1. Installation

npm install -D cypress 

2. Open Cypress:

npx cypress open 

Writing an E2E Test

// cypress/e2e/navigation.cy.js
describe('Navigation', () => {
  it('should navigate to the about page', () => {
    // Start from the index page
    cy.visit('http://localhost:3000/')

    // Find a link with an href attribute containing "about" and click it
    cy.get('a[href*="about"]').click()

    // The new url should include "/about"
    cy.url().should('include', '/about')

    // The new page should contain an h1 with "About page"
    cy.get('h1').contains('About page')
  })
}) 

Cypress vs. Playwright in Next.js

Comparison Cypress Playwright
Execution Runs inside the browser. Runs outside (via CDP/WebDriver).
Language JavaScript/TypeScript only. JS, Python, Java, C#.
Speed Generally slower for large suites. Extremely fast (parallelization).
Browsers Chrome, Firefox, Edge, Electron. Chromium, WebKit (Safari), Firefox.
Multiple Tabs Does not support multi-tab testing. Native support for multiple pages/tabs.

Best Practices

  • Set a baseUrl: Define your Next.js dev server URL (e.g., http://localhost:3000) in cypress.config.js to avoid hardcoding it in every test.
  • Use data- attributes: Instead of selecting by CSS classes that might change (like Tailwind classes), use data-cy or data-testid for more resilient selectors.
  • Clean State: Don't rely on the state left by a previous test. Use beforeEach() to reset cookies, local storage, or your database.
  • Intercept API Calls: Use cy.intercept() to prevent your tests from hitting real production APIs, which makes tests faster and more predictable.

Note

For Next.js projects, it is recommended to run your build (npm run build) and start the production server (npm run start) before running E2E tests to ensure you are testing the code exactly as it will behave in production.

Authentication Last updated: Feb. 28, 2026, 7:30 p.m.

Authentication in Next.js has evolved into a streamlined process thanks to the Middleware and Server Actions patterns. Most modern apps leverage libraries like Auth.js (formerly NextAuth) or specialized services (Clerk, Kinde) to handle session management, OAuth providers, and secure cookie storage. Because authentication logic can run in Middleware, you can protect entire sections of your app (like a /dashboard route) before a user even reaches the server-side rendering logic.

The security model of the App Router ensures that sensitive user data stays on the server. By using Server Components to check for an active session, you can conditionally render UI without exposing authentication tokens to the client-side JavaScript bundle. This "Server-Side Auth" approach reduces the attack surface of your application while providing a faster experience, as the user doesn't have to wait for a client-side "loading" state to verify their identity.

Authentication Patterns

Authentication in Next.js has evolved significantly with the introduction of the App Router and Server Components. Instead of relying solely on client-side checks, Next.js allows you to verify identity on the server, providing a more secure and seamless user experience.

Core Authentication Strategies

Next.js typically employs one of two primary patterns depending on where the session data is stored and managed.

Strategy Mechanism Best Use Case
Session-Based A secure, httpOnly cookie stores a session ID; the server verifies this ID against a database or cache. Highly secure apps, financial applications.
Token-Based (JWT) A signed JSON Web Token is stored in a cookie. The server verifies the signature without a database lookup. Scalable apps, distributed microservices.

The Next.js Auth Flow

The modern pattern moves logic from the browser to Middleware and Server Components. This prevents "flicker" (where a user briefly sees protected content before being redirected).

1. Middleware Protection

Middleware allows you to run code before a request is completed. It is the best place to check for a session cookie and redirect unauthenticated users.

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const session = request.cookies.get('session')?.value

  if (!session && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
} 

2. Server Component Verification

For fine-grained access control (e.g., checking user roles), you can verify the session directly inside a Server Component.

// app/dashboard/page.tsx
import { getSession } from '@/lib/auth';
import { redirect } from 'next/navigation';

export default async function Dashboard() {
  const session = await getSession();

  if (!session) redirect('/login');

  return <h1>Welcome, {session.user.name}</h1>;
} 

Popular Authentication Libraries

While you can build your own system using iron-session or jose, most developers use established libraries that handle the complexities of OAuth, email/password, and security.

Library Type Pros
Auth.js (NextAuth) Self-hosted Massive provider list (Google, GitHub), built for Next.js, easy to set up.
Clerk Managed Service Pre-built UI components (User Button, Profile), handles MFA and session management.
Kinde Managed Service Extremely fast setup, excellent for SaaS with multi-tenancy support.
Supabase Auth Backend-as-a-Service Integrated perfectly with Supabase DB and Row Level Security (RLS).

Client vs. Server Access

  • Server Side: Use cookies to read the session. This is the "source of truth" and should be used for all data fetching and layout protection.
  • Client Side: Use a SessionProvider (in Auth.js) or hooks like useUser() (in Clerk) to show UI-specific details like the user's avatar or a "Logout" button.

Best Practices

  • Use httpOnly Cookies: Never store session tokens in localStorage. Cookies with the httpOnly flag are inaccessible to JavaScript, protecting users from XSS attacks.
  • Prefer Server-side Redirects: Use Middleware or redirect() in Server Components rather than useEffect for a faster, more secure experience.
  • CSRF Protection: Next.js Server Actions include built-in protection, but ensure your authentication library handles CSRF tokens for standard API routes.
  • Layered Security: Check authentication in Middleware (global), Layouts (layout-wide), and Server Actions (mutation-specific).

Note

When using Server Actions for login/logout, always use revalidatePath('/') or revalidateTag() to clear the cache and ensure the UI reflects the new authentication state immediately.

Session Management

Session management is the process of securely tracking a user's authenticated state across multiple requests. In the Next.js App Router, this is handled primarily through secure cookies, allowing the server to identify the user before rendering any HTML.

Session Storage Strategies

Next.js developers typically choose between Stateless (JWT) and Stateful (Database) sessions. Both rely on httpOnly cookies for transport but differ in how the server validates the user.

Feature Stateless (JWT) Stateful (Database)
Storage Encrypted token stored in the cookie itself. Session ID in cookie; data in DB (Redis/Postgres).
Validation Server checks the signature (No DB hit). Server queries DB for every request
Revocation Difficult (must wait for expiry or use blacklists). Instant (delete the row from the DB)
Scalability High (ideal for serverless/edge). Medium (requires low-latency DB like Redis).

The Session Lifecycle

1. Creation (Login)

Upon successful credential verification, the server generates a session and sets a Set-Cookie header in the response.

// Example of setting a secure cookie in a Server Action
import { cookies } from 'next/headers'

export async function login(data) {
  const sessionId = await createSessionInDb(data.userId)
  
  (await cookies()).set('session', sessionId, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
  })
} 

2. Persistence & Validation

On every subsequent request, the browser automatically sends the cookie. Next.js Middleware or Server Components read this cookie to validate the session.

3. Deletion (Logout)

To end a session, you must remove the cookie and, if using a stateful approach, delete the record from your database.

export async function logout() {
  (await cookies()).set('session', '', { expires: new Date(0) })
} 

Security Best Practices for Sessions

Managing sessions manually requires strict adherence to security flags to prevent common attacks like XSS and CSRF.

  • httpOnly: Prevents client-side JavaScript from accessing the cookie, mitigating XSS risks.
  • Secure: Ensures the cookie is only sent over encrypted HTTPS connections.
  • SameSite: Set to Lax or Strict to prevent the cookie from being sent in cross-site requests (mitigates CSRF).
  • Rolling Sessions: Automatically update the expiration date of a session if the user is active, keeping them logged in without requiring a re-login.

Session Management in Popular Libraries

Library Management Style Default Storage
Auth.js Flexible JWT by default; supports Database strategy.
Clerk Managed JWT-based short-lived tokens with long-lived refresh tokens.
Supabase Managed JWT stored in cookies; handles refreshing automatically.
Iron Session Stateless Encrypted and signed cookies (no DB needed).

Implementation Tips

  • Server-Side is Truth: Always perform your session checks in Server Components or Middleware. Use client-side hooks only for UI state (e.g., hiding a login button).
  • Handle Expiry Gracefully: Use a try/catch block around session validation logic to catch expired tokens and redirect the user to /login.
  • Database Choice: If using stateful sessions, Redis is the preferred choice due to its sub-millisecond read speeds, which prevents session checks from slowing down your page loads.

Note

With the App Router, you should avoid passing session data through props from layouts to pages. Instead, call your getSession() function directly in any component that needs it. Next.js will memoize the call so it only runs once per request.

Deploying Last updated: Feb. 28, 2026, 7:30 p.m.

Deploying a Next.js app is designed to be a "push-to-deploy" experience, particularly on Vercel, the creators of the framework. Vercel automatically detects Next.js features like ISR, Middleware, and Image Optimization, deploying them to the global Edge Network for maximum speed. This managed environment handles the scaling, SSL certificates, and preview deployments automatically, allowing developers to focus purely on writing code.

However, Next.js is not locked into a single provider. Using the output: 'standalone' configuration, you can package your application into a lightweight Docker container that can be hosted on any cloud provider, such as AWS, Google Cloud, or Azure. This flexibility ensures that whether you choose a fully managed serverless environment or a self-hosted VPS, your application remains portable and performant.

Production Builds

Moving from development to production in Next.js involves a specific compilation process that optimizes your code for performance, security, and scalability. While next dev focuses on developer experience (Fast Refresh, detailed error messages), next build focuses on the end-user experience (minification, caching, and code splitting).

The Build Workflow

When you run the build command, Next.js performs several sophisticated optimization steps to transform your source code into a production-ready application.

Phase Action Purpose
Compiling Transforms JSX/TypeScript into optimized JavaScript. Browser compatibility and performance.
Minifying Removes whitespace, comments, and shortens variable names. Reduces file size for faster downloads.
Code Splitting Breaks the app into small chunks instead of one giant file. Loads only the JS needed for the current page.
Static Generation Pre-renders pages that don't require dynamic data at request time. Instant page loads and SEO benefits.
Optimization Optimizes images, scripts, and fonts. Improves Core Web Vitals (LCP, CLS).

Key Commands

You will typically interact with the production lifecycle using these three commands:

  • next build: Creates an optimized production build in the .next folder.
  • next start: Starts a Node.js server to serve the production build (requires next build to have run first).
  • next export: (Legacy/Static) For the App Router, this is now handled via output: 'export' in next.config.js.

Analyzing Build Output

After running next build, the terminal displays a summary of all created routes. This report is crucial for understanding how your application will behave in production.

Symbol Meaning Behavior
? (Static) Static HTML Delivered as a static file (fastest).
ƒ (Dynamic) Server-rendered Rendered on-demand for each request.
? (Lambda) Edge/Serverless Optimized for serverless function environments.

Production Optimizations

Next.js automatically applies several optimizations during the build process that are disabled in development:

  • Tree Shaking: Removes unused code from your bundles.
  • Image Preloading: Injects hints into the HTML to start loading "Priority" images immediately.
  • Persistent Caching: Adds content-based hashes to filenames (e.g., main-a1b2c3.js), allowing browsers to cache them indefinitely until the content changes.
  • Middleware Bundling: Minifies and optimizes your middleware.ts to run as fast as possible at the Edge.

Best Practices for Production

  • Check Bundle Sizes: Use the built-in report to identify "large" pages (indicated in red/yellow). Investigate using Lazy Loading (Section 7.5) to reduce these sizes.
  • Validate Environment Variables: Ensure all production secrets are set in your hosting provider (Section 8.3). A common build error is missing variables required for static page generation.
  • Run a Local Production Test: Always run npm run build && npm run start locally before pushing to GitHub. This helps catch errors that only appear in production (like CSS collisions or hydration mismatches).
  • Use a Build Hook: If your site uses a CMS, set up a Webhook to trigger a new production build whenever content changes.

Pro Tip: Use the @next/bundle-analyzer package to see a visual map of what is taking up space in your JavaScript chunks. It's the best way to find heavy dependencies that can be optimized.

Static Exports

A Static Export allows you to build your Next.js application as a collection of static HTML, CSS, and JavaScript files. This removes the requirement for a Node.js server at runtime, enabling you to host your application on any static hosting provider (e.g., Nginx, Apache, GitHub Pages, or S3).

Key Characteristics

When you enable static exports, Next.js transitions from a dynamic framework to a powerful Static Site Generator (SSG).

Feature Static Export (output: 'export') Standard Next.js
Runtime No server required (Client-only). Requires Node.js or Edge runtime.
Data Fetching Happens at Build Time. Can happen at Build or Request.
Dynamic Routes Must be pre-defined via generateStaticParams. Can be generated on-the-fly.
Features Restricted (No Middleware, No Headers). Full feature set supported.

How to Enable Static Exports

To change the build output, modify your next.config.js or next.config.mjs file:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  // Optional: Add trailing slashes to URLs (e.g., /about/)
  trailingSlash: true,
  // Optional: Disable image optimization or use a custom loader
  images: { unoptimized: true },
}

module.exports = nextConfig 

Supported and Unsupported Features

Because there is no server to process requests, certain Next.js features that require a server-side logic layer are unavailable.

Supported ? Unsupported ?
Server Components (rendered at build time) Middleware
Client Components & Hooks API Routes / Route Handlers
Static Site Generation (SSG) Incremental Static Regeneration (ISR)
Metadata API Server Actions
Layouts & Nesting Dynamic headers or redirects

Handling Dynamic Routes

For routes like /blog/[slug], Next.js needs to know which pages to generate during the build. You must use the generateStaticParams function to provide these values.

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then((res) => res.json());

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

export default function Page({ params }) {
  return <h1>Post: {params.slug}</h1>;
} 

Deployment Workflow

  1. Develop: Write your app using standard Next.js patterns.
  2. Build: Run npm run build. Next.js populates the out/ directory.
  3. Upload: Copy the contents of the out/ directory to your hosting provider.
  4. Serve: The provider serves the HTML files directly.

Best Practices

  • Image Optimization: The default next/image requires a server. For static exports, you must either set unoptimized: true or use a third-party loader like Cloudinary or Imgix.
  • Client-side Data Fetching: If you need "dynamic" data (like a user's profile), fetch it from the browser using useEffect or SWR inside a Client Component, rather than trying to use Server-side rendering.
  • Client-side Redirects: Since server-side redirects don't work, use window.location or a Meta tag redirect if necessary.

Docker Support

Dockerizing a Next.js application allows you to package your app, its dependencies, and the environment into a single container image. This ensures that your app runs identically across development, staging, and production environments, and is the standard approach for deploying to platforms like AWS (ECS/App Runner), Google Cloud Run, or Kubernetes.

The "Output Standalone" Feature

By default, a Next.js production build includes many development dependencies that aren't needed at runtime. Next.js offers a Standalone mode that automatically traces all required files and creates a minimal server shell.

To enable this, update next.config.js:

module.exports = {
  output: 'standalone',
} 

This reduces the image size significantly (often from 1GB+ down to ~150MB) by only including the files necessary for production.

Multi-Stage Dockerfile Pattern

Using a multi-stage Dockerfile is the best practice for Next.js. It separates the build environment (which needs npm, compilers, and dev-deps) from the runtime environment (which only needs the compiled code).

Example Dockerfile

# 1. Base image
FROM node:18-alpine AS base

# 2. Install dependencies
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# 3. Build the application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# 4. Production runner
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production

# Create a non-root user for security
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copy standalone build and static files
COPY --from=builder /app/public ./public
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"] 

Deployment Comparison: Docker vs. Managed

Feature Docker (Self-Hosted) Managed (Vercel/Netlify)
Control Full control over the OS and Node version. Abstracted; limited environment control.
Scaling Manual or via K8s/Auto-scaling groups. Fully automatic (Serverless).
Cost Fixed per server instance. Usage-based (can scale with traffic).
Setup Requires CI/CD and Docker knowledge. Zero-config "Git to Deploy".

Docker Best Practices

  • Use Alpine Linux: Always use -alpine versions of Node images to minimize the attack surface and keep the image size small.
  • Respect .dockerignore: Ensure files like .git, node_modules, and local .env files are ignored to prevent bloated images and security leaks.
  • Security: Never run your container as the root user. Use the USER instruction as shown in the example above to mitigate potential exploits.
  • Environment Variables: Don't bake secrets into the image. Instead, pass them at runtime using your orchestration tool (e.g., docker run -e API_KEY=...).
  • Health Checks: Implement a health check in your container orchestration to ensure traffic is only routed to "ready" containers.

Useful Commands

Action Command
Build Image docker build -t nextjs-app .
Run Container docker run -p 3000:3000 nextjs-app
Analyze Size docker images

Note

When using output: 'standalone', Next.js does not automatically copy the public or .next/static folders because these are ideally served by a Content Delivery Network (CDN). In a Docker container, you must copy them manually (as seen in the Dockerfile example) so server.js can find them.

Components Last updated: Feb. 28, 2026, 7:31 p.m.

Next.js provides a set of specialized React components that extend standard HTML elements with performance-optimized behaviors. These include Link for client-side navigation, Image for automated asset optimization, and Script for managing third-party libraries. Using these components is essential for maintaining the performance benefits of the framework, as they are designed to work in harmony with the Next.js routing and rendering engines.

In addition to these, there are structural components like Suspense for handling loading states and the Head (legacy) or Metadata (modern) systems for managing document attributes. Each component is designed to solve a specific web development pain point—such as layout shift or slow page transitions—by automating the "best practice" implementation behind the scenes.

<Image>

The Next.js Image Component (next/image) is an extension of the HTML element, evolved to handle the complexities of modern web performance automatically. It is designed to help you achieve high Core Web Vitals scores, specifically targeting Largest Contentful Paint (LCP) and Cumulative Layout Shift (CLS).

Core Optimizations

When you use the < Image> component, Next.js performs several "magic" optimizations behind the scenes that would otherwise require manual configuration.

Feature Description Benefit
Size Optimization Automatically serves correctly sized images for each device (mobile vs. desktop). Reduces data usage and speed times.
Visual Stability Prevents Cumulative Layout Shift (CLS) by reserving space for the image. Better user experience; no jumpiness.
Faster Page Loads Images are only loaded when they enter the viewport using native browser lazy loading. Saves bandwidth for off-screen images.
Modern Formats Automatically converts images to WebP or AVIF if the browser supports them. Smaller file sizes without quality loss.

1.1. Local Images

For images stored within your project (e.g., in the public folder), you should import them. Next.js will automatically determine the width and height, preventing layout shift.

import Image from 'next/image'
import profilePic from '../public/me.png'

export default function Page() {
  return (
    <Image
      src={profilePic}
      alt="Picture of the author"
      // width and height are provided automatically
      placeholder="blur" // Optional: adds a blurred low-res placeholder
    />
  )
} 

2. Remote Images

For images hosted on an external URL (like a CMS or S3), you must manually provide width and height attributes so Next.js can calculate the aspect ratio.

<Image
  src="https://example.com/hero.jpg"
  alt="Hero Image"
  width={800}
  height={600}
/> 

Security Note

To use remote images, you must define authorized domains or patterns in your next.config.js to prevent malicious external usage.

Essential Props

Prop Type Requirement Description
src String / Object Required The path to your image.
alt String Required Accessibility description for screen readers.
width / height Number Required (Remote) Dimensions in pixels to define aspect ratio.
priority Boolean Optional If true, the image is considered high priority and preloads. Use for LCP images.
fill Boolean Optional Causes the image to fill its parent container (parent must have position: relative).

Best Practices

  • Priority for LCP: Always add the priority prop to your "above the fold" images (like logos or hero banners) to signal the browser to load them immediately.
  • Use sizes with fill: If using fill, always provide a sizes string (e.g., sizes="(max-width: 768px) 100vw, 50vw") to help Next.js pick the best image size for the user's screen.
  • Avoid over-using unoptimized: Only use unoptimized={true} for small animated GIFs or when your hosting provider does not support the Next.js Image Optimization API.
  • WebP/AVIF: Ensure your hosting environment (like Vercel) supports automatic image optimization to take advantage of modern formats.

<Link>

The < Link> component is a React component that extends the HTML < a> element to provide prefetching and client-side navigation. It is the primary way to move between routes in Next.js, ensuring that transitions feel instantaneous by avoiding full page reloads.

Key Features

Using < Link> instead of standard < a> tags unlocks the core performance benefits of the Next.js router.

Feature Description Benefit
Client-side Navigation Intercepts the click to update the URL without a full browser refresh. Maintains application state like a native app.
Prefetching Automatically loads the code for the linked page in the background. Makes the next page load feel instant.
Route Grouping Works seamlessly with App Router features like parallel routes and intercepts. Simplifies complex UI patterns.
Accessibility Automatically handles keyboard navigation and screen reader attributes. Ensures your site remains compliant with A11y standards.

How Prefetching Works

Next.js is highly strategic about how it preloads data to save bandwidth while maintaining speed.

  • When it happens: Prefetching occurs when a component enters the user's viewport (using the Intersection Observer API).
  • Static vs. Dynamic:
  • For Static Routes, the entire page is prefetched.
  • For Dynamic Routes, only the shared layout tree is prefetched to minimize unnecessary data fetching.

Basic Usage

The only required prop is href. Next.js handles the rest.

import Link from 'next/link'

export default function Navbar() {
  return (
    <nav>
      {/* Basic navigation */}
      <Link href="/">Home</Link>
      
      {/* Dynamic navigation */}
      <Link href={`/blog/${post.slug}`}>Read More</Link>
      
      {/* Navigation with query params */}
      <Link href="/search?query=nextjs">Search</Link>
    </nav>
  )
} 

Common Props

Prop Type Default Description
href String / Object Required The path or URL to navigate to.
replace Boolean false Replaces the current history state instead of adding a new one.
scroll Boolean true Scrolls to the top of the new page after navigation.
prefetch Boolean true Can be set to false to disable background loading for that specific link.

Checking Active Links

To style a link based on whether it is currently active, you combine with the usePathname() hook from next/navigation.

'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'

export function NavLinks() {
  const pathname = usePathname()

  return (
    <Link 
      href="/dashboard" 
      className={pathname === '/dashboard' ? 'active' : ''}
    >
      Dashboard
    </Link>
  )
} 

Best Practices

  • Use for Internal Links: Always use for internal navigation. Use standard < a> tags only for external websites or file downloads.
  • Disable Prefetching for Large Pages: If you have a list with hundreds of links to heavy pages, consider setting prefetch={false} to avoid excessive network activity.
  • Avoid Nested Anchors: Do not place an < a> tag inside a < Link>. As of Next.js 13, the < Link> component automatically renders an < a> tag for you.
  • Forwarding Refs: If you are wrapping in a custom component, ensure you use React.forwardRef so Next.js can correctly attach the Intersection Observer for prefetching.

<Script>

The Next.js Script Component (next/scrpt) is an extension of the HTML < scrpt> element. It allows you to optimize the loading of third-party scripts—such as Google Analytics, AdSense, or customer support widgets—by giving you control over when and how they are executed.

Loading Strategies

Strategy When it loads Best Use Case
beforeInteractive Before Next.js code and before page hydration. Critical scripts like bot detection or themes.
afterInteractive (Default) Immediately after the page becomes interactive. Google Analytics, Tag Manager, etc.
lazyOnload During idle time, after all resources are fetched. Chat widgets, social media feedback tools.
worker (Experimental) Offloads the script to a Web Worker. Scripts that don't need access to the DOM (like some analytics).

Basic Usage

import Script from 'next/script'

export default function Layout({ children }) {
  return (
    <>
      <section>{children}</section>
      
      {/* This script will load after the page is interactive */}
      <Script
        src="https://example.com/analytics.js"
        strategy="afterInteractive"
      />
    </>
  )
} 

Executing Code After Loading

<Script
  src="https://example.com/library.js"
  onLoad={() => {
    console.log('Script has loaded!')
    window.initializeLibrary()
  }}
  onError={(e) => {
    console.error('Script failed to load', e)
  }}
/> 

Inline Scripts

<Script id="show-banner" strategy="afterInteractive">
  {`document.getElementById('banner').style.display = 'block';`}
</Script> 

Best Practices

  • Default to afterInteractive: Most third-party scripts do not need to block the initial page render.
  • Use lazyOnload for Chat: Chat bubbles and support widgets are heavy. Loading them only after the main content is ready significantly improves performance.
  • Avoid beforeInteractive in Pages: strategy only works in _document.js (Pages Router) or the root layout.tsx (App Router). Using it elsewhere will cause errors.
  • Check for Window: If your script adds something to the window object, ensure your React code checks for its existence before calling it to prevent undefined errors.

Note

If you are using the worker strategy, you must enable the experimental nextScriptWorkers flag in your next.config.js and install partytown.

<Form>

Introduced in Next.js 15, the < Form> component is an extension of the standard HTML < form> element. It is designed to simplify client-side navigation on form submission, particularly for search forms or filters where you want to update the URL without a full page reload.

Key Advantages

Feature Description Benefit
Client-Side Navigation Updates the URL and transitions to the next page using the Next.js router. Faster transitions; maintains state.
Prefetching Automatically prefetches the destination page when the form enters the viewport. Submission feels instantaneous.
Progressive Enhancement Works as a standard HTML form if JavaScript is disabled. High reliability and SEO-friendly.
Automatic Routing Automatically handles the query string generation from input names. No manual router.push for GET requests.

Basic Usage: Search Forms

The most common use case for < Form> is a GET request, such as a search bar. When submitted, Next.js automatically appends the input values to the URL.

import Form from 'next/form'

export default function SearchPage() {
  return (
    <Form action="/search">
      {/* On submit, this navigates to /search?query=value */}
      <input name="query" />
      <button type="submit">Submit</button>
    </Form>
  )
} 

Comparison: Standard < form> vs. Next.js < Form>

Feature HTML <form> Next.js <Form>
Navigation Browser-level reload. Client-side (SPA) transition.
Prefetching None. Prefetches the action path.
State Retention Page state is lost. React state is preserved.
GET Requests Standard URL update. Optimized URL update via Next.js router.

Usage with Server Actions

import Form from 'next/form'
import { updateUsername } from './actions'

export default function ProfileForm() {
  return (
    <Form action={updateUsername}>
      <input name="username" placeholder="New Username" />
      <button type="submit">Update</button>
    </Form>
  )
} 

Best Practices

  • Use for Navigation: Use < Form> specifically for forms that navigate the user to a new result page (like search, filters, or category selection).
  • Provide name Attributes: The name attribute on inputs is critical; it becomes the key in the URL query string (e.g., name="q" results in ?q=...).
  • Loading States: Pair < Form> with useFormStatus in a child component to provide immediate visual feedback while the navigation or action is pending.
  • Avoid for Small UI Toggles: If a form only toggles a small piece of UI state (like a "Show More" checkbox) without needing a URL update, a standard < form> or simple state variable may be more appropriate.

Note

Just like the < Link> component, the < Form> component only prefetches the action path when it becomes visible in the viewport, ensuring optimal performance without wasting bandwidth.

Font Module

The Next.js Font Module (next/font) automatically optimizes your fonts (including custom fonts and Google Fonts) and removes external network requests for improved privacy and performance. It is designed to eliminate Layout Shift by ensuring your text remains stable during the font loading process.

Key Optimizations

Feature Description Benefit
Zero External Requests Google Fonts are downloaded at build time and self-hosted with your app. Better privacy and no external network requests.
Automatic Self-Hosting Custom and Google fonts are served from your own domain. Faster loading via the same HTTP/2 connection.
Preloading Automatically preloads the specific font files used in your layout. Reduces FOIT (Flash of Invisible Text).
Size Adjustment Uses an "adjust-fallback" mechanism to match the size of a system font to your web font. Eliminates layout shift (CLS).

Using Google Fonts

// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter', // Defines a CSS variable
})

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-roboto-mono',
})

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
      <body>{children}</body>
    </html>
  )
} 

Using Local Fonts

import localFont from 'next/font/local'

const myFont = localFont({
  src: './my-font.woff2',
  display: 'swap',
})

export default function Page() {
  return (
    <h1 className={myFont.className}>
      This uses a custom local font.
    </h1>
  )
}
 

Configuration Options

Option Type Description
subsets Array List of character sets to load (e.g., ['latin']). Reduces file size.
weight String / Array Required for non-variable fonts (e.g., '400' or ['400', '700']).
style String / Array Defines if the font is normal or italic.
display String Controls font-display CSS (usually set to swap).
variable String Name of the CSS variable to use with Tailwind or CSS Modules.

Best Practices

  • Use Variable Fonts: Whenever possible, choose variable fonts (like Inter or Roboto Flex). They include all weights in a single, smaller file, reducing total bandwidth.
  • Subset Your Fonts: Always specify subsets: ['latin'] to prevent the browser from downloading unnecessary characters for languages you aren't using.
  • Leverage CSS Variables: Instead of using .className on every element, use the variable option and apply the font family in your global CSS or Tailwind config.
  • Limit Font Families: Every unique font family adds to your JS/CSS bundle. Stick to 1–2 families for optimal performance.

File Conventions Last updated: Feb. 28, 2026, 7:31 p.m.

The App Router relies on a specific set of special file names within the app/ directory to define the behavior of a route. Files like page.js create the unique UI for a path, while layout.js defines shared UI that persists across navigations. This convention-over-configuration approach makes the project structure predictable and easy to navigate, even for new developers joining a large codebase.

Beyond the basic UI files, Next.js uses files like loading.js to automatically create Suspense boundaries, error.js to catch runtime exceptions, and not-found.js to handle 404 states. There are also files for metadata (opengraph-image.js) and API logic (route.js). Understanding this "alphabet" of file types is the key to mastering the App Router, as it allows you to declare complex architectural behaviors simply by naming your files correctly.

page.js

In the Next.js App Router, page.js (or .tsx) file is the most fundamental building block of your application. Its the file that makes a route publicly accessible and defines the unique UI for that specific path.

Characteristics

A page.js file is responsible for rendering the UI of a route. While Layouts provide the shared structure, the Page provides the specific content.

Property Description
Routing A route is not valid until a page.js file is added to a folder.
Server by Default Pages are Server Components by default, allowing direct data fetching.
Nesting Pages are rendered inside the children prop of their parent layout.js.
Hooks Can use "use client" if interactivity or client-side hooks (like useState) are needed.

Basic Structure

A page is a default exported React component. In the App Router, these components can be async to facilitate server-side data fetching.

TypeScript

// app/blog/[slug]/page.tsx

export default async function Page({ params }) {
  const { slug } = await params; // Access dynamic route segments

  return (
    <article>
      <h1>Post: {slug}</h1>
      <p>This is the unique content for this route.</p>
    </article>
  );
}
    

Page Props

Next.js automatically passes two specific props to every page.js file:

Prop Type Description
params Promise An object containing the dynamic route parameters from the URL (e.g. id or slug).
searchParams Promise An object containing the URL query parameters (e.g. ?query=abc).

Note

As of Next.js 15, params and searchParams are asynchronous and must be await-ed before accessing their properties.

Interaction with Layouts

The page.js is always the leaf node of a route segment. When a user navigates, Next.js wraps the page in its corresponding layout, which is then wrapped in the parent layout, all the way to the Root Layout.

Best Practices

  • Server-First Mentality: Keep your page.js as a Server Component. Fetch data directly in the component to minimize client-side JavaScript.
  • SEO Integration: Use the generateMetadata function or the metadata object within the page file to set unique titles and descriptions for that specific route.
  • Loading States: Place a loading.js file in the same folder as your page.js to provide an instant fallback UI while the page’s data is being fetched.
  • Error Boundaries: Use an error.js file in the folder to catch and handle runtime errors specific to that page without crashing the entire app.

layout.js

A layout.js file defines UI that is shared across multiple pages. Unlike a page, a layout does not remount when a user navigates between sibling routes. This makes layouts ideal for persistent elements like navigation bars, sidebars, and footers.

Key Characteristics

Layouts create a hierarchical structure for your application, allowing you to nest UI patterns efficiently.

Property Behavior
State Preservation Maintains component state (e.g. search input text) during navigation.
No Re-Rendering The layout remains static while the child page content changes.
Nesting Layouts are nested by default. A child layout is wrapped by its parent layout.
Required A layout.js file is required in the app root (Root Layout).

The Root Layout

The root layout (app/layout.tsx) is the top-most layout. It must define the <html> and <body> tags, as Next.js does not automatically create them.


// app/layout.tsx

export default function RootLayout({ 
  children 
}: { 
  children: React.ReactNode 
}) {
  return (
    <html lang="en">
      <body>
        <nav>Global Navbar</nav>
        <main>{children}</main>
        <footer>Global Footer</footer>
      </body>
    </html>
  );
}
  

Nested Layouts

You can create layouts inside specific folders to apply UI only to that segment and its children. For example, a dashboard might have a unique sidebar that shouldn’t appear on marketing pages.


// app/dashboard/layout.tsx

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="dashboard-container">
      <aside>Dashboard Sidebar</aside>
      <section>{children}</section>
    </div>
  );
}
  

Layout Props

Layouts receive one primary prop:

  • children — This is a React node that will be populated with either a child layout (if one exists) or the page.js file of the current route.
  • params — Just like pages, layouts can access dynamic route parameters (e.g. id). They cannot access searchParams.

Comparison: Layout vs Template

While layouts persist state, sometimes you may want a piece of UI to completely reset on navigation. For that, Next.js provides Templates (template.js).

Feature layout.js template.js
Persistence Persists across routes. Re-mounts on every navigation.
State State is maintained. State is reset.
Use Case Navbars, Sidebars, Persistent Footers. Entry animations, useEffect logging.

Best Practices

  • Data Fetching: Fetch data in layouts if multiple child pages need the same information (e.g. checking user authentication or fetching a navigation menu).
  • Avoid Over-nesting: While nesting is powerful, too many layers of layouts can make your CSS and layout logic difficult to manage.
  • Metadata: You can define a metadata object in a layout. If a child page also defines metadata, Next.js will intelligently merge them.
  • Server Components: Keep layouts as Server Components. If you need a client-side feature (like a toggleable sidebar), create a "Client Component" and import it into the Server Layout.

loading.js

The loading.js file allows you to create instant loading states using React Suspense. By placing a loading.js file inside a route folder, Next.js automatically wraps the page (and any nested children) in a Suspense boundary, displaying the loading UI immediately while the page content is being fetched.

How It Works

When a user navigates to a route, the layout is rendered immediately, but the page content might be delayed due to data fetching. loading.js fills that gap, providing a better user experience by showing that the application is responsive.

Feature Description
Instant Feedback Displays the fallback UI on the first request and during navigation.
Shared Layouts Layouts remain interactive while the loading state is active (e.g. sidebar stays clickable).
Streaming Next.js streams the page content to the browser as it's ready, replacing the loading UI.

Implementation

Creating a loading state is as simple as exporting a React component from a loading.js (or .tsx) file.


// app/dashboard/loading.tsx

export default function Loading() {
  // You can render a skeleton, a spinner, or a simple "Loading..." text
  return (
    <div className="flex items-center justify-center h-full">
      <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-blue-500"></div>
    </div>
  )
}
  

Streaming with Suspense

loading.js is the "automatic" way to handle loading, but you can also use manual Suspense boundaries for more granular control within a page. This allows you to stream specific components while the rest of the page remains static.

Method Best Use Case
loading.js Entire page loading (e.g., initial route navigation).
Manual <Suspense> Partial page loading (e.g., a "Trending Products" list on a dashboard).

Best Practices

  • Use Skeletons: Instead of a generic spinner, use Skeleton Screens that mimic the layout of the page. This reduces perceived load time and prevents "popping" when content arrives.
  • Keep it Light: The loading component is sent to the browser early. Keep its bundle size small to ensure it renders instantly.
  • Avoid "Flickering": If data fetching is extremely fast, a loading state might appear and disappear in a split second. Consider adding a minimum delay if the UI flicker is jarring.
  • Interactive Layouts: Ensure your navigation links are in the layout.js, not the page.js. This allows users to navigate away even while a page is still in its loading state.

not-found.js

The not-found.js file is used to render a custom UI when the notFound() function is triggered within a route segment or when a URL does not match any existing routes in your application. It allows you to provide a branded, helpful experience instead of the browser's default 404 page.

How It Works

Next.js handles "Not Found" states in two primary ways:

  1. Automatic (Global): If a user visits a URL that doesn't exist (e.g. /this-does-not-exist), Next.js will look for the nearest not-found.js file in the directory tree.
  2. Programmatic: You can manually trigger the "Not Found" state inside a Server Component or Server Action using the notFound() function from next/navigation.
Feature Behavior
Hierarchy Next.js will render the not-found.js closest to the segment where it was triggered.
Root Fallback If no nested not-found.js exists, the root app/not-found.js is used.
Streaming It is rendered on the server and streamed to the client.

Basic Implementation

A not-found.js file should export a simple React component.

// app/not-found.tsx
import Link from 'next/link'

export default function NotFound() {
  return (
    <div className="flex flex-col items-center justify-center min-h-screen">
      <h2>Page Not Found</h2>
      <p>Could not find requested resource</p>
      <Link href="/" className="text-blue-500 underline">
        Return Home
      </Link>
    </div>
  )
} 

Programmatic Usage

This is particularly useful when a dynamic route parameter is valid (e.g., /blog/123), but the data does not exist in your database.

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`)
  if (!res.ok) return undefined
  return res.json()
}

export default async function PostPage({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)

  if (!post) {
    notFound() // This triggers the nearest not-found.js
  }

  return <article>{/* Render post */}</article>
} 

Placement and Nesting

  • Global 404: Place not-found.js directly in the app/ folder to catch all unmatched URLs.
  • Specific 404: Place not-found.js inside a folder (e.g. app/dashboard/not-found.js) to provide a dashboard-specific 404 page that maintains the dashboard sidebar and layout.

Comparison: not-found.js vs. error.js

File Triggered By... Primary Purpose
not-found.js Missing data or invalid URL. Informing the user the resource doesn't exist.
error.js Runtime exceptions (crashes). Recovering from unexpected code failures.

Best Practices

  • Keep it Helpful: Don't just say "404." Provide a search bar or links to popular sections of your site to keep users engaged.
  • Avoid Complex Data Fetching: Since the not-found.js might be triggered when data fetching fails, avoid fetching more complex data inside the 404 page itself.
  • Server Component: By default, not-found.js is a Server Component. You can make it a Client Component if you need to use hooks like usePathname() to show the user exactly which URL failed.

error.js

The error.js file convention allows you to gracefully handle unexpected runtime errors in nested routes. It automatically wraps a route segment and its children in a React Error Boundary, ensuring that an error in one part of the app doesn't crash the entire application.

>How It Works

When an error is thrown within a page or component, Next.js will "catch" it and render the UI defined in the nearest error.js file instead of the failing component.

Feature Description
Isolation Errors are contained to the smallest possible segment. The rest of the app (like the root navbar) remains interactive.
Recovery Provides a way for users to attempt to recover from the error without a full page refresh.
Server & Client Handles errors that occur during both Server Rendering (SSR) and Client-side transitions.

Implementation

An error.js component must be a Client Component because it needs to use hooks to handle the error state and recovery. It receives two primary props: error and reset.

TypeScript

'use client' // Error components must be Client Components

import { useEffect } from 'react'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Log the error to an error reporting service like Sentry
    console.error(error)
  }, [error])

  return (
    <div className="p-4 border-2 border-red-500 rounded-lg bg-red-50">
      <h2 className="text-xl font-bold">Something went wrong!</h2>
      <p className="text-sm text-gray-600 mb-4">{error.message}</p>
      <button
        onClick={() => reset()} // Attempt to re-render the segment
        className="px-4 py-2 bg-red-600 text-white rounded"
      >
        Try again
      </button>
    </div>
  )
}
  

The reset() Function

The reset() function is a powerful tool for UX. When called, it prompts Next.js to try re-rendering the route segment. If the re-render is successful, the error UI is replaced by the actual page content. This is useful for transient errors, such as a temporary network blip.

Global Errors

Because error.js files are nested within layouts, an error.js in a subfolder cannot catch an error in its own parent layout.js. To catch errors in the root layout or root template, you must use a special file called global-error.js.

File Type Scope
error.js Catches errors in children components/pages of that segment.
global-error.js Catches errors in the root layout. Must define its own <html> and <body> tags.

Best Practices

  • Be User-Friendly: Avoid showing raw stack traces to users. Use the error prop for logging, but show a helpful message and a "Try Again" button to the user.
  • Granularity: Place error.js files strategically. If a specific section (like a sidebar) is prone to errors, wrap it in its own folder with an error.js so the rest of the dashboard stays functional.
  • Error Reporting: Use the useEffect hook inside the error component to send error details to services like Sentry, LogRocket, or Datadog.
  • Use digest: Next.js provides a digest property on the error object in production. This is a hashed ID you can provide to users so they can reference it when contacting support, allowing you to find the specific server-side log.

global-error.js

While error.js is effective for catching errors within specific route segments, it has a limitation: it renders inside its parent layout. This means an error.js file cannot catch errors thrown in the Root Layout (app/layout.js)To handle the "worst-case scenario"—a crash at the very top of your application—Next.js provides global-error.js.

purpose and Scope

global-error.js is the ultimate fallback. It is designed to catch errors that occur in the root of your application, which would otherwise result in a blank screen or a generic browser error.

Feature error.js global-error.js
Placement Any folder in the app directory. Only in the root app directory.
Context Renders inside the parent layout. Replaces the entire page, including the root layout.
Requirements Standard React component. Must define its own <html> and <body> tags.
Environment Active during development and production. Only active in production (standard error overlay appears in dev).

Implementation

Because global-error.js replaces the root layout, it must provide the basic HTML shell to ensure the page remains a valid document.


'use client'

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <html lang="en">
      <body className="flex flex-col items-center justify-center min-h-screen">
        <h2>Critical Application Error</h2>
        <p>A global error occurred that prevented the app from loading.</p>
        <button 
          onClick={() => reset()}
          className="mt-4 px-4 py-2 bg-blue-600 text-white rounded"
        >
          Attempt Recovery
        </button>
      </body>
    </html>
  )
}
  

Key Behaviors

  • Production Only: In development, Next.js prioritizes the Error Overlay to help you debug. global-error.js will only trigger in your production build.
  • The reset() function: Just like in error.js, calling reset() will attempt to re-render the root layout and the rest of the application.
  • Root Layout Errors: If your root layout fetches data (e.g. global settings or user profile) and that fetch fails, global-error.js is the only component capable of catching that failure.

Best Practices

  • Keep it Minimal: Since this file only runs when your main layout has failed, avoid importing heavy components or shared libraries that might also be broken. Use standard HTML and inline styles or simple CSS.
  • Duplicate Root Metadata: Because the root layout's metadata won't be applied when this error is triggered, consider adding a basic <title> inside the global-error.js <head>.
  • Reporting: This is the most critical place to implement error logging (e.g. Sentry), as it captures the most severe failures in your application.

Note

Even if you have a global-error.js, it is still highly recommended to have a root app/error.js to handle non-critical errors while keeping the global navigation and branding intact.

route.js

Route Handlers allow you to create custom request handlers for a given route using the Web Request and Response APIs. These are essentially the Next.js version of API Routes, allowing you to build backends, webhooks, and proxy services directly within the app directory.

Key Characteristics

Unlike page.js, which returns HTML UI, route.js returns raw data (JSON, files, etc.).

Feature Description
Separation A route.js file cannot exist in the same folder as a page.js.
HTTP Methods Supports GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS.
Caching GET requests are cached by default unless they use dynamic functions (like cookies or headers).
Runtime Can run on the standard Node.js runtime or the lightweight Edge Runtime.

>Basic Implementation

Route Handlers are defined by exporting an async function named after the HTTP method.

TypeScript

// app/api/hello/route.ts

import { NextResponse } from 'next/server'

export async function GET() {
  return NextResponse.json({ message: 'Hello from the API!' })
}

export async function POST(request: Request) {
  const data = await request.json()
  return NextResponse.json({ received: data }, { status: 201 })
}
    

Request and Response APIs

Next.js extends the standard Web APIs with NextRequest and NextResponse, providing helpful utilities for common backend tasks.

1. Handling Query Parameters

TypeScript

import { type NextRequest } from 'next/server'

export function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const query = searchParams.get('query')

  // Querying /api/search?query=nextjs gives "nextjs"
}
  

2. Accessing Cookies and Headers


import { cookies, headers } from 'next/headers'

export async function GET() {
  const cookieStore = await cookies()
  const token = cookieStore.get('token')

  const headerList = await headers()
  const referer = headerList.get('referer')

  return new Response('Checking auth...')
}
  

Dynamic vs. Static Routes

Next.js optimizes Route Handlers based on their behavior:

Type Condition Behavior
Static Uses GET and no dynamic functions. Evaluated at build time; served as a static file.
Dynamic Uses methods other than GET, or uses cookies(), headers(). Evaluated at request time for every user.

Revalidation (ISR in APIs)

You can revalidate static Route Handlers by using the revalidate option or by setting a route configuration segment.


export const revalidate = 60 // Revalidate this API route every 60 seconds

export async function GET() {
  const data = await fetch('https://api.example.com/data')
  return Response.json(await data.json())
}
  

Best Practices

  • Security: Always validate incoming data (e.g. using Zod) and verify authentication headers for sensitive routes.
  • Use Server Actions Instead?: If you are only calling an API from a Next.js form or button, Server Actions are usually a better choice as they don't require manual endpoint management.
  • CORS: If your API is intended to be called by other domains, you must manually handle the OPTIONS method and set the appropriate Access-Control-Allow-Origin headers.
  • Streaming: You can use ReadableStream to stream data (like AI responses) back to the client for a faster perceived experience.

template.js

While Layouts (layout.js) are designed to persist and maintain state across navigation, Templates (template.js) are designed to create a fresh instance of a component every time a user navigates between sibling routes.

Key Differences: Layout vs. Template

The primary distinction lies in how they handle the React Lifecycle. Layouts wrap a route and persist, whereas Templates wrap each child and re-mount on every navigation.

Feature layout.js template.js
Persistence Persists across sibling routes. Re-mounts on every navigation.
State Maintained (e.g. input values stay). Reset (e.g. input values clear).
Effects useEffect runs only once. useEffect runs on every navigation.
Animations Harder to trigger entry animations. Ideal for CSS entry/exit animations.

How Nesting Works

In the Next.js rendering hierarchy, the Template is nested inside the Layout and outside the Page (and any loading/error boundaries).

Hierarchy: Layout > Template > ErrorBoundary > Suspense > Page

Implementation

Templates are defined similarly to layouts, receiving a children prop.


// app/dashboard/template.tsx

'use client'

import { useEffect } from 'react'

export default function Template({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    console.log('Template mounted: Log page view')
  }, [])

  return (
    <div className="animate-fade-in">
      {/* This div will re-animate every time the route changes */}
      {children}
    </div>
  )
}
    

Common Use Cases

There are specific scenarios where a Template is preferable to a Layout:

  • Page View Analytics: If you need to log a "page view" event for every navigation using a useEffect.
  • CSS Transitions: Triggering entry/exit animations (like a fade-in or slide-in) that should play every time a user clicks a link.
  • Resetting Component State: Features like a feedback form or a search bar that should be completely cleared when moving between pages.
  • Suspense Fallbacks: If you want a specific loading state to trigger every time, even if the layout is already loaded.

Best Practices

  • Default to Layouts: Unless you have a specific reason to re-mount (like animations or logging), always use layout.js. Layouts are more performant because they avoid unnecessary re-renders of the shared UI.
  • Client vs. Server: Templates can be Server or Client Components. However, since they are often used for animations or hooks, they are frequently marked with 'use client'.
  • Avoid Redundancy: Don't put the same logic in both a layout and a template. Use the layout for the structure and the template for the "refresh" logic.

default.js

The default.js file is a specialized convention used within Parallel Routes. It acts as a fallback UI when Next.js cannot restore a slot’s state during a full-page reload or when a slot has no matching sub-route.

Why is it needed?

Parallel Routes allow multiple UI segments (slots) to render simultaneously, such as @analytics or @team.

During a hard refresh (F5), Next.js may understand the main route but may not know what to render inside other slots if they lack matching folders. Without a default.js, this situation results in a 404.

Implementation

A default.js file is simply a standard React component. It may render a placeholder UI or return null if the slot should remain empty.



// app/dashboard/@analytics/default.tsx
export default function DefaultAnalytics() {
  return (
    <div className="p-4 border rounded shadow">
      <h3>Analytics Dashboard</h3>
      <p>Please select a specific metric to view details.</p>
    </div>
  )
}
  

Navigation Behavior

Navigation Type Behavior
Soft Navigation (Link click) Next.js preserves the previous slot state even if no matching route exists.
Hard Navigation (Reload) Next.js cannot restore state. It searches for default.js inside each slot.

Comparison: page.js vs. default.js

In Parallel Routes, it is common to use both page.js and default.js.

File Role
page.js Rendered when the URL matches the folder exactly.
default.js Rendered when the URL matches a sibling or parent route, but the current slot has no specific match.

Example Scenario

Imagine a dashboard with a modal implemented using Parallel Routes:

  • app/dashboard/layout.tsx (Defines slots @main and @modal)
  • app/dashboard/@modal/login/page.tsx
  • app/dashboard/@modal/default.tsx (Returns null)
  1. If the user visits /dashboard/login, the @modal slot renders the login page.
  2. If the user is on /dashboard/login and refreshes, the @main content stays, but the @modal slot uses default.js (rendering null) to ensure the modal disappears rather than showing a 404.

Best Practices

  • Return null for Modals: Slots representing modals should usually return null in default.js.
  • Keep Content Consistent: For persistent UI elements (like sidebars), keep the default UI visually aligned with page.js.
  • Folder Placement: Place default.js alongside the slot’s main page.js.

instrumentation.js

The instrumentation.js (or .ts) file is an optional Next.js convention that allows you to hook into the application runtime for performance monitoring, error tracking, and observability integrations.

It is commonly used with tools like Sentry, New Relic, and OpenTelemetry.

Key Characteristics

This file runs as early as possible in the lifecycle of a Next.js instance, making it the ideal place for initialization logic that needs to happen once per server start.

Feature Description
Execution Runs once when a new Next.js server instance is started.
Placement Must be placed in the project root (same level as next.config.js) or inside the src folder.
Runtime Support Supports both Node.js and Edge runtimes.
Conditionality Requires enabling via experimental.instrumentationHook.

How to Enable

Before using instrumentation.js, you must enable it in your configuration.



// next.config.js
module.exports = {
  experimental: {
    instrumentationHook: true,
  },
}
  

Basic Implementation

The instrumentation.js file exports a register() function. This function runs whenever a new Next.js runtime environment is initialized.



// instrumentation.ts
export async function register() {
  // Detect the active runtime
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { setupNodeMonitoring } = await import('./logging/node')
    setupNodeMonitoring()
  }

  if (process.env.NEXT_RUNTIME === 'edge') {
    const { setupEdgeMonitoring } = await import('./logging/edge')
    setupEdgeMonitoring()
  }
}

Common Use Cases

Use Case Why use instrumentation.js?
OpenTelemetry (OTel) Automatically collect traces and metrics without modifying every page.
Error Tracking Initialize tools like Sentry or Bugsnag before requests are processed.
Database Warming Establish connection pools or warm caches at server startup.
Environment Audit Verify environment variables or log dependency versions.

Best Practices

  • Use Conditional Imports: Always use import() inside register() to prevent runtime conflicts.
  • Keep it Lightweight: Heavy logic can delay application startup.
  • Environment Awareness: Use process.env.NEXT_RUNTIME to guard runtime-specific code.
  • Avoid UI Logic: This file is strictly for runtime configuration, not React components.

Summary Table: Global Lifecycle Files

File Purpose
middleware.ts Runs for every request (authentication, redirects, headers, etc.).
instrumentation.ts Runs once per server start (monitoring, logging, initialization).
layout.tsx Provides shared UI structure and data fetching across routes.

middleware.js

Middleware allows you to run logic before a request is completed. Based on the incoming request, you can rewrite, redirect, modify headers, or respond directly.It is commonly used for cross-cutting concerns such as authentication, localization, bot protection, and request filtering.

Key Characteristics

Middleware runs on the Edge Runtime by default, which is a lightweight, high-performance environment designed for low latency.

Feature Description
Execution Runs for every route unless restricted by a matcher.
Runtime Runs on the Edge Runtime by default.
Speed Extremely fast due to edge execution.
Placement Must be placed in the project root.
Constraints Limited access to Node.js APIs.

Basic Implementation

Middleware uses the standard Web Request and Response APIs.



// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/old-path')) {
    return NextResponse.redirect(new URL('/new-path', request.url))
  }
}
  

Matching Specific Routes

By default, Middleware runs for every request. Use a matcher configuration to limit execution.


export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
    '/dashboard/:path*',
  ],
}
  

Common Use Cases

Use Case Implementation Logic
Authentication Check session cookies and redirect if missing.
A/B Testing Rewrite URLs based on experiment buckets.
Localization Detect language and redirect to locale routes.
Bot Protection Block specific IPs or User-Agents.
Security Headers Add headers like CSP globally.

Helpful Utilities: NextResponse

The NextResponse object provides several methods to manipulate the request flow:

  • redirect(): Changes the URL in the browser (HTTP 307/308).
  • rewrite(): Changes the view without changing the URL (proxying).
  • next(): Continues the middleware chain to the page or next middleware.
  • cookies: management: Use response.cookies.set() or delete() to manage user sessions.

Best Practices

  • Keep it Lightweight:Middleware must execute in milliseconds. Avoid heavy data fetching or complex logic.
  • Use the Matcher:Be as specific as possible with your matcher to prevent Middleware from running on static assets (images, CSS), which wastes execution time.
  • Avoid Database Queries:If you must check a database, use a fast, edge-compatible cache (like Redis/Upstash) instead of a traditional SQL query to maintain performance.
  • Runtime Awareness:Remember that Middleware does not have access to the full Node.js ecosystem. Use the NextRequest and NextResponse helpers provided by Next.js for the best experience.

Functions Last updated: Feb. 28, 2026, 7:31 p.m.

The Next.js API includes a variety of built-in functions that allow you to interact with the server and the request/reponse lifecycle. Functions like cookies() and headers() allow Server Components to read incoming request data, while redirect() and notFound() enable programmatic navigation and error handling. These functions are designed to be used within the "Server Context," ensuring that they are never exposed to the client-side bundle.

Additionally, the framework provides utilities for managing the cache, such as revalidatePath() and revalidateTag(). These are typically used inside Server Actions to clear specific parts of the Data Cache after a mutation, ensuring the user sees updated data immediately. This suite of functions provides the "glue" that connects your UI components to the underlying server infrastructure.

fetch (Extended)

In Next.js, the native Web fetch() API is extended to provide fine-grained control over caching and revalidation on the server.

This transforms every fetch request into a configurable data dependency, rather than just a simple network call.

Core Enhancements

Next.js patches the global fetch() function to integrate with the Data Cache and introduces the next option for cache behavior control.

Feature Standard fetch Next.js fetch
Caching Depends on browser/server headers. Cached by default (Static) or explicitly opted-out (Dynamic).
Revalidation Manual cache busting required. Built-in time-based and tag-based revalidation.
Deduplication Multiple calls = Multiple requests. Automatic deduplication across the render tree.
Environment Primarily client-side. Fully optimized for Server Components.

Request Memoization (Deduplication)

One of the most powerful features of Next.js fetch is automatic deduplication.

If multiple components fetch the same resource using the same URL and options within a single render pass, Next.js executes the network request only once.

Note

Deduplication applies only to GET requests within the same React component tree. It does not persist across multiple user requests.

Caching Strategies

You can control server-side caching behavior using the cache option.


TypeScript

// 1. Force Cache (Default)
// Data is cached until revalidation occurs
fetch('https://api.example.com/data', { cache: 'force-cache' })

// 2. No Store (Dynamic)
// Data is fetched on every request
fetch('https://api.example.com/data', { cache: 'no-store' })

Revalidation (ISR)

Revalidation is the process of clearing the Data Cache and fetching fresh data. Next.js provides multiple revalidation strategies depending on your data requirements.

Next.js supports two primary revalidation mechanisms:

1. Time-based Revalidation

Useful for data that changes infrequently and where freshness isn't mission-critical (e.g., blog posts).


TypeScript

// Revalidate at most once per hour (3600 seconds)
fetch('https://api.example.com/data', {
  next: { revalidate: 3600 },
})

2. On-demand Revalidation (Tag-based)

Useful for instantly refreshing cached data when the source changes (e.g., CMS webhook).


TypeScript

// 1. Tag the request
fetch('https://api.example.com/data', {
  next: { tags: ['collection'] },
})

// 2. Trigger revalidation elsewhere
import { revalidateTag } from 'next/cache'

revalidateTag('collection')

Data Fetching Patterns

Pattern Behavior Use Case
Sequential Requests happen one after another (await first, then await second). When the second request depends on the result of the first.
Parallel Requests are initiated at the same time using Promise.all. When requests are independent (e.g., fetching User Profile and Recent Orders).

TypeScript


// Parallel Fetching Example
async function Page() {
  const userPromise = getUser(id)
  const postsPromise = getPosts(id)

  // Both requests start in parallel
  const [user, posts] = await Promise.all([userPromise, postsPromise])

  return (/* ... */)
}

Best Practices

  • Fetch where you use it: Don't pass data down through many layers of props. Fetch it directly in the Server Component that needs it; deduplication ensures you won't pay a performance penalty.
  • Keep secrets on the Server: Since fetch in Server Components never runs on the client, you can safely use private API keys without exposing them to the browser.
  • Handle Errors: Wrap your fetch calls in try/catch or check res.ok. If a fetch fails in a Server Component, it will trigger the nearest error.js boundary.
  • Use no-store for User Data: If the data is specific to a logged-in user (like a shopping cart), ensure you use cache: 'no-store' or cookies() to prevent private data from being cached and served to others.

cookies

The cookies() function is a built-in Next.js utility that allows you to read and manage HTTP cookies from Server Components, Route Handlers, and Server Actions. Because cookies are part of the request headers, using this function automatically opts the route into Dynamic Rendering.


Reading Cookies

To read cookies in a Server Component, you call the cookies() function. As of Next.js 15, this function is asynchronous and must be awaited.

TypeScript


import { cookies } from 'next/headers'

export default async function Page() {
  const cookieStore = await cookies()
  const theme = cookieStore.get('theme')

  return <div>Current theme: {theme?.value}</div>
}
  

Writing Cookies

Cookies can only be modified (set or deleted) in Server Actions or Route Handlers. You cannot set cookies directly inside a Server Component because the component renders after the response headers have already been sent to the client.

Feature Server Components Server Actions / Route Handlers
Read (get, has) ? Supported ? Supported
Write (set, delete) ? Not Supported ? Supported

Example: Setting a Cookie in a Server Action

TypeScript


'use server'

import { cookies } from 'next/headers'

export async function createSession(data: string) {
  const cookieStore = await cookies()

  cookieStore.set('session', data, {
    httpOnly: true, // Security: prevents JS access
    secure: true,   // Send only over HTTPS
    maxAge: 60 * 60 * 24 * 7, // 1 week
  })
}

Common Methods

The cookies() object provides several utility methods for managing data:

Method Description
get(name) Returns an object containing the name and value of the cookie.
getAll() Returns an array of all cookies.
has(name) Returns a boolean indicating if the cookie exists.
set(name, value, options) Sets a new cookie (Actions/Route Handlers only).
delete(name) Removes a specific cookie (Actions/Route Handlers only).

Impact on Rendering & Caching

Using cookies() has significant implications for how Next.js serves your page:

  • Dynamic Rendering: Since the server needs to check the request's cookies to generate the UI, the page cannot be pre-rendered as a static HTML file at build time. It will be rendered on every request.
  • Data Cache: Fetch requests made in the same scope as cookies() are generally not cached by default unless you explicitly provide a cache strategy, as the data is assumed to be user-specific.

Best Practices

  • Security First: When setting sensitive data (like auth tokens), always use httpOnly: true and secure: true options to protect against XSS and man-in-the-middle attacks.
  • Check for Existence: Always handle the case where a cookie might be missing (e.g., cookieStore.get('name')?.value) to avoid runtime errors.
  • Keep it Small: Browsers have limits on the total size of cookies (usually ~4KB). For larger datasets, store a session ID in the cookie and keep the actual data in a database or server-side cache like Redis.
  • Use for Personalization: Use cookies for low-stakes UI preferences (like dark mode or collapsed sidebars) to ensure the layout is correct before the client-side JavaScript even loads.

headers

The headers() function is a Next.js utility that allows you to read incoming HTTP request headers from Server Components, Route Handlers, and Server Actions. Like cookies(), accessing headers is a dynamic operation that opts the route into Dynamic Rendering.

Key Characteristics

Headers provide essential metadata about the request, such as the user's browser agent, accepted languages, or custom security tokens.

Feature Description
Read-Only You can only read headers. To set response headers, use Middleware or Route Handlers.
Async (Next.js 15) The function is asynchronous and must be awaited.
Dynamic Rendering Using headers() forces the page to be rendered on the server for every request.

Basic Implementation

You can use the headers() function to inspect standard or custom headers.

TypeScript


import { headers } from 'next/headers'

export default async function Page() {
  const headersList = await headers()
  const userAgent = headersList.get('user-agent')

  return (
    <div>
      <h1>Device Info</h1>
      <p>{userAgent}</p>
    </div>
  )
}
  

Common Use Cases

Header Use Case
user-agent Detect if the user is on mobile/desktop or identify search engine bots.
referer Tracking where the user came from (useful for analytics or security).
accept-language Determining the best language to display before the client hydrates.
x-forwarded-for Identifying the client's IP address (often used with load balancers).
Custom Headers Reading metadata passed from Middleware (e.g., x-user-role).

Accessing Headers in Different Contexts

The availability and behavior of headers depend on where they are called:

  • Server Components: Useful for tailoring the initial HTML based on request data.
  • Route Handlers: Often used to validate API keys or check content types.
  • Server Actions: Useful for logging request metadata during a mutation.

Best Practices

  • Avoid Overuse: Since headers() makes a route dynamic, only use it when necessary. If the same result can be achieved with client-side detection (like media queries), prefer that to keep the page static.
  • Check for Null: Always assume a header might be missing. Use optional chaining or null checks when accessing values.
  • Security: Do not trust headers implicitly for security-critical logic (like IP-based auth) without verifying that your infrastructure correctly sanitizes them.
  • Use with Middleware: Use Middleware to set custom headers (e.g., x-pathname) that you can later read in your Server Components using headers().

notFound

The notFound() function is a specialized Next.js utility used to programmatically trigger a 404 Not Found state. When invoked, it halts the execution of the current route segment and renders the nearest not-found.js file.

Key Characteristics

This function is essential for handling cases where a route is technically valid (the URL pattern matches) but the specific data requested does not exist.

Feature Description
Termination Immediately stops the component's rendering and throws a controlled error.
Contexts Can be used in Server Components, Route Handlers, and Server Actions.
UI Fallback Displays the closest not-found.js in the file hierarchy.
Type In TypeScript, it has a return type of never.

Basic Implementation

The most common use case is within a dynamic route where a specific ID or slug isn't found in your database.

import { notFound } from 'next/navigation'

async function fetchUser(id: string) {
  const res = await fetch(`https://api.example.com/users/${id}`)
  if (!res.ok) return undefined
  return res.json()
}

export default async function UserProfile({ params }: { params: { id: string } }) {
  const user = await fetchUser(params.id)

  if (!user) {
    notFound() // Component execution stops here
  }

  return 
Welcome, {user.name}
}

Usage Scenarios

Scenario Logic
Missing Resource A database query for a product, post, or user returns null or undefined.
Unauthorized Access A user tries to access a private ID that doesn't belong to them (returning 404 instead of 403 can prevent "ID fishing").
Invalid Parameters A dynamic segment (e.g., [lang]) receives a value that isn't in your supported list.

Comparison: notFound() vs. redirect()

While both stop the current render, they serve different architectural purposes:

Function HTTP Status Use Case
notFound() 404 The resource is gone or never existed.
redirect() 307 (Temporary) The resource has moved or the user needs to be elsewhere (e.g., /login).

Best Practices

  • Conditional Triggering: Always check your data before rendering. Calling notFound() early prevents the rest of your component logic from running with null data, avoiding "cannot read property of undefined" errors.
  • Security: Use notFound() to hide the existence of resources that a user isn't allowed to see. This prevents malicious actors from determining which IDs are valid.
  • Pair with not-found.js: Ensure you have a not-found.js file at the root or within the specific segment to provide a branded, helpful experience rather than the default Next.js 404 page.

redirect

In Next.js, the redirect() and permanentRedirect() functions allow you to navigate users to a different URL programmatically. These functions are designed to work across Server Components, Client Components, Route Handlers, and Server Actions.

Key Differences

Next.js provides two distinct functions based on the intended HTTP status code and SEO impact.

Function HTTP Status Use Case
redirect() 307 (Temporary) Temporary moves, such as redirecting an unauthenticated user to a login page.
permanentRedirect() 308 (Permanent) Permanent moves, such as when a blog post slug has changed or a site structure has been reorganized.

Usage in Server Components

When used in a Server Component, these functions throw a special internal error that Next.js catches to handle the redirect before the page is fully rendered.

TypeScript


import { redirect } from 'next/navigation'

async function fetchTeam(id: string) {
  const res = await fetch(`https://api.example.com/team/${id}`)
  if (!res.ok) return null
  return res.json()
}

export default async function ProfilePage({ params }) {
  const team = await fetchTeam(params.id)

  if (!team) {
    // Redirect to a general team list if specific team isn't found
    redirect('/teams')
  }

  return <div>{team.name}</div>
}
  

Usage in Server Actions

Redirects are commonly used after a successful data mutation, such as submitting a form or creating a new record.

TypeScript


'use server'

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

export async function createPost(formData: FormData) {
  // 1. Perform logic (save to DB)
  const id = await savePost(formData)

  // 2. Clear cache for the list page
  revalidatePath('/posts')

  // 3. Send user to the new post
  redirect(`/posts/${id}`)
}

Behavior and Limitations

  • Client Components: These functions can be used in Client Components, but they should generally be called within an event handler or useEffect. However, for client-side navigation, the useRouter hook is often preferred.
  • Try/Catch Blocks: Because these functions work by throwing an "error" that Next.js catches, you should not wrap them in a standard try/catch block unless you re-throw the error.

    Correction: If you must use a try/catch, ensure you check if the error is a "redirect" error and let it pass through.
  • Absolute vs. Relative: You can redirect to relative paths (e.g., /dashboard) or absolute external URLs (e.g., https://example.com).

Comparison: redirect vs. middleware vs. next.config.js

Method Best For...
redirect() Logic inside a specific page, action, or component.
Middleware Global logic (e.g., Auth, Geo-blocking) before reaching any page.
next.config.js Static, permanent URL rewrites or legacy path management.

Best Practices

  • Use 307 (Temporary) by Default: Use redirect() for most application logic. Only use permanentRedirect() when you are certain the old URL will never be used again, as browsers and search engines cache 308 redirects aggressively.
  • Server-Side Preference: Perform redirects on the server whenever possible. This prevents the "flash" of unauthorized content and provides a faster experience as the browser doesn't have to download the initial page's JavaScript.
  • Post-Mutation: Always use a redirect after a successful POST request (Server Action) to follow the "Post-Redirect-Get" pattern, which prevents users from accidentally resubmitting forms if they refresh the page.

permanentRedirect

The permanentRedirect() function is a specialized utility used to navigate a user to a different URL while explicitly signaling to browsers and search engines that the original URL is no longer in use.

Core Functionality

When called, permanentRedirect() returns an HTTP 308 (Permanent Redirect) status code. This is a "stronger" version of the standard redirect() (307).

Feature redirect() permanentRedirect()
HTTP Status 307 (Temporary) 308 (Permanent)
Browser Behavior Does not cache; checks the original URL next time. Caches the redirect; next visits go to the new URL.
SEO Impact Link equity stays with the original URL. Link equity (SEO juice) transfers to the new URL.
Methods Preserves request method (e.g., POST stays POST). Preserves request method (e.g., POST stays POST).

When to Use permanentRedirect()

You should use this function when a resource has been moved indefinitely.

  • URL Refactoring: You renamed a route from /user-profile to /profile.
  • Slug Updates: A blog post title changed, and you want the old slug to permanently point to the new one to avoid broken links in search results.
  • Domain Migration: You are moving specific paths from one subdomain or domain to another.

When to Use permanentRedirect()

You should use this function when a resource has been moved indefinitely.

  • URL Refactoring: You renamed a route from /user-profile to /profile.
  • Slug Updates: A blog post title changed, and you want the old slug to permanently point to the new one to avoid broken links in search results.
  • Domain Migration: You are moving specific paths from one subdomain or domain to another.

Implementation Example

This is typically used in Server Components or Route Handlers during a transition period or after a database update.

TypeScript


import { permanentRedirect } from 'next/navigation'

async function getProject(oldSlug: string) {
  // Logic to find if the slug has been updated in the DB
  const project = await db.project.findUnique({ where: { oldSlug } })

  if (project?.newSlug) {
    // Tell the world this URL is officially gone
    permanentRedirect(`/projects/${project.newSlug}`)
  }

  return project
}

export default async function ProjectPage({ params }) {
  const project = await getProject(params.slug)
  // ... render page
}

Important Considerations

  • Caching Danger: Because browsers cache 308 redirects aggressively, they are difficult to undo. If you accidentally set a permanent redirect to the wrong URL, users' browsers may keep sending them there even after you fix the code. Use it only when you are certain the move is final.
  • Server Actions: While valid, it is less common in Server Actions (which usually handle temporary flow logic like "Login ? Dashboard").
  • Development Mode: In Next.js development mode, the redirect still occurs, but the permanent cache behavior of the browser is often bypassed to make debugging easier.

Best Practices

  • Prefer 307 for Logic: For auth guards, form submissions, or "Not Found" fallbacks, always use redirect().
  • SEO Stewardship: Use permanentRedirect() specifically to help Google and other crawlers update their indexes without losing the page's ranking.
  • Check try/catch: Like redirect(), this function works by throwing an internal error. Do not swallow this error in a try/catch block, or the redirect will fail to execute.

revalidatePath

The revalidatePath() function is a core Next.js utility used for On-Demand Revalidation. It allows you to manually purge the cached data for a specific path, forcing the server to fetch fresh data the next time that path is visited.

How It Works

Next.js uses a Data Cache to store the result of fetch requests and rendered pages. While time-based revalidation (ISR) happens automatically, revalidatePath gives you programmatic control—usually triggered after a user action like submitting a form.

Feature Description
Purge Scope Clears the cache for the specific URL or an entire route segment.
Execution Typically used inside Server Actions or Route Handlers.
Immediate Effect The cache is invalidated immediately; the next request will be a "cache miss" and trigger a re-render.
Environment Server-side only.

Basic Implementation

The most common pattern is to call revalidatePath after a database mutation to ensure the UI reflects the change.

TypeScript
'use server'

import { revalidatePath } from 'next/cache'

export async function submitComment(formData: FormData) {
  const postId = formData.get('postId')
  
  // 1. Update the database
  await addCommentToDb(formData)

  // 2. Clear the cache for the specific post page
  revalidatePath(`/blog/${postId}`)
}

Revalidation Types

You can control the "depth" of the revalidation using the optional second argument.

Syntax

Type Behavior
revalidatePath('/blog/[slug]') page (Default) — Revalidates only the specific path. Does not affect nested routes.
revalidatePath('/blog', 'layout') layout — Revalidates the path and all children (nested segments) under it.

Comparison: revalidatePath vs. revalidateTag

Function Targeted By Best For...
revalidatePath URL / Path Simple sites where you know exactly which page needs updating.
revalidateTag Custom Data Tag Complex sites where one data change affects many different URLs (e.g., updating a product price).

Important Behaviors

  • Client-side Router Cache: When revalidatePath is called in a Server Action, it also automatically clears the Router Cache (client-side) for that path, ensuring the user sees the update immediately without a manual refresh.
  • No Return Value: This function returns void. It is a "side effect" function.
  • Build Time vs. Runtime: While it's most useful at runtime, clearing the cache ensures that the next visitor gets the most up-to-date content, maintaining the benefits of Static Site Generation with the freshness of Dynamic rendering.

Best Practices

  • Be Specific: Use the 'page' type whenever possible to avoid unnecessary server load. Only use 'layout' if the change affects multiple nested routes (e.g., updating a sidebar).
  • Call After Mutations: Always place revalidatePath after your database logic is successful to prevent the cache from being cleared if the update fails.
  • Use with Redirects: If you are redirecting a user after a form submission (e.g., from /edit back to /profile), call revalidatePath before the redirect() to ensure the destination page is fresh.

revalidatePath

The revalidateTag() function is a more granular alternative to revalidatePath. It allows you to purge the cache for specific data fetches across your entire application, regardless of which URL they appear on. This is the foundation of Tag-Based Revalidation.

How It Works

When you use fetch, you can assign one or more "tags" to the request. revalidateTag() then acts as a "kill switch" for any cached data associated with those specific labels.

Step Action Logic
1. Tagging fetch(url, { next: { tags: ['posts'] } }) Labels the data in the Data Cache.
2. Triggering revalidateTag('posts') Marks all entries with the "posts" tag as stale.
3. Update Next request for that data Re-fetches from the source and updates the cache.

Basic Implementation

1. Tagging the Data (Server Component)

TypeScript
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts-collection'] } // Assigning the tag
  })

  return res.json()
}

2. Revalidating the Data (Server Action)

TypeScript
'use server'

import { revalidateTag } from 'next/cache'

export async function addPost(formData: FormData) {
  await savePost(formData) // Mutation logic
  
  // Clear every fetch request tagged with 'posts-collection'
  // globally across the entire app
  revalidateTag('posts-collection')
}

Comparison: Path vs. Tag Revalidation

Feature revalidatePath revalidateTag
Target A specific URL/Route. A specific data dependency.
Scope All data on that page. Only the data with that specific tag.
Complexity Simple, folder-based. Highly precise, identifier-based.
Use Case Updating a "Profile" page. Updating "Product Price" appearing on 100+ pages.

Key Benefits

  • Decoupled Logic: You don't need to know every URL where a specific piece of data is used. As long as the data is tagged, revalidateTag will find and purge it.
  • Efficiency: It only purges the specific data that changed. Other cached data on the same page (like a sidebar or footer) remains cached and fast.
  • Multi-Tagging: You can assign multiple tags to a single fetch (e.g., tags: ['products', 'electronics', 'sony']), allowing you to purge by category or by specific item.

Best Practices

  • Meaningful Naming: Use clear, descriptive tags (e.g., user-123-profile instead of just user).
  • Webhook Integration: This is ideal for Headless CMS integrations. When a "Publish" event occurs in your CMS, your webhook can call a Route Handler in Next.js that triggers revalidateTag.
  • Server-Side Only: Like revalidatePath, this must be called in a server-side environment (Server Action or Route Handler).
  • Order of Operations: Always perform your data mutation before calling revalidateTag to ensure the cache isn't cleared if the database update fails.

useParams

The useParams() hook is a Client Component hook that allows you to read the dynamic parameters of the current route. It is the client-side equivalent of the params prop received by Server Components.

Key Characteristics

Because this hook relies on the current browser state, it can only be used in components marked with the 'use client' directive.

Feature Description
Return Value An object containing the current route's dynamic parameters.
Re-rendering The component will re-render whenever the dynamic parameters change.
Usage Ideal for client-side logic like fetching data from a client-side API or filtering lists.

Basic Implementation

/[category]/[id] ? useParams() returns { category, id }

TypeScript
'use client'

import { useParams } from 'next/navigation'

export default function ProductClientComponent() {
  const params = useParams()

  // If the route is /shop/shoes/nike-air
  // params will be { category: 'shoes', id: 'nike-air' }

  return (
    <div>
      <h1>Category: {params.category}</h1>
      <p>Product ID: {params.id}</p>
    </div>
  )
}

Handling Dynamic Segments

The structure of the object returned by useParams() depends on how your folders are named in the app directory:

Folder Name Route Example useParams() Result
[slug] /hello-world { slug: 'hello-world' }
[...slug] (Catch-all) /blog/2026/01 { slug: ['blog','2026','01'] }
[[...slug]] (Optional) / (root) {}

Comparison: Server vs Client Params

Context Method Best For
Server Component props.params Initial data fetching, SEO metadata, and server-side logic.
Client Component useParams() Interactive UI, client-side data fetching, and event handling.

Best Practices

  • Type Safety: If using TypeScript, cast the params or use an interface to ensure you are accessing keys that actually exist.
  • Avoid for SEO: If you need the parameter to generate page title or metadata, use props.params in Server Components instead.
  • Optional Chaining: When working with optional catch-all routes, always use optional chaining (e.g., params.slug?.length).

usePathname

The usePathname() hook is a Client Component hook that allows you to read the current URL's pathname. It is particularly useful for UI logic that depends on the current location, such as highlighting the active link in a navigation bar.

Key Characteristics

Since usePathname() reads from the browser's address bar, it is a client-side feature and requires the 'use client' directive.

Feature Description
Return Value A string representing the current URL's path (e.g., "/dashboard/settings").
Re-rendering The component re-renders every time the route changes.
Context Works only in Client Components.

Basic Implementation

TypeScript
'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'

export default function NavLinks() {
  const pathname = usePathname()

  const links = [
    { name: 'Home', href: '/' },
    { name: 'About', href: '/about' },
    { name: 'Dashboard', href: '/dashboard' },
  ]

  return (
    <nav className="flex gap-4">
      {links.map((link) => {
        const isActive = pathname === link.href
        
        return (
          <Link
            key={link.name}
            href={link.href}
            className={
              isActive ? 'text-blue-600 font-bold' : 'text-gray-500'
            }
          >
            {link.name}
          </Link>
        )
      })}
    </nav>
  )
}

Common Use Cases

Use Case Implementation
Active Link Styling Adding a specific CSS class to the navigation link that matches the current path.
Breadcrumbs Splitting the pathname string to generate a list of parent routes.
Conditional Layouts Hiding a specific UI element (like a newsletter signup) on specific pages (e.g., /login).
Analytics Tracking Using a useEffect that triggers a page-view event whenever the pathname changes.

Comparison: usePathname vs. useParams

Hook Returns... Example (URL: /shop/shoes?size=10)
usePathname() The full path string. "/shop/shoes"
useParams() The dynamic segments as an object. { category: 'shoes' }

Best Practices

  • Breadcrumb Parsing: When creating breadcrumbs, remember that usePathname() includes the leading slash. You may need to filter out empty strings after using .split('/').
  • Performance: Because this hook causes a re-render on every navigation, keep the component using it relatively shallow to avoid heavy re-rendering of entire page trees.
  • Server-Side Alternative: If you need the pathname on the server, you cannot use this hook. Instead, pass the pathname from Middleware as a custom header and read it using the headers() function.

useRouter

The useRouter() hook is a Client Component hook that allows you to programmatically change routes, refresh the current page, or navigate through the browser history. In the App Router, this hook is imported from next/navigation.

Key Characteristics

Unlike the useRouter hook from the older Pages Router (next/router), the App Router version is simpler and focused exclusively on navigation actions.

Feature Description
Client-Only Requires the 'use client' directive.
Programmatic Navigation Used for navigation that happens after an event (e.g., button click, form submission, timer finishing).
Prefetching Automatically handles prefetching of routes you navigate to for faster transitions.

Basic Implementation

TypeScript
'use client'

import { useRouter } from 'next/navigation'

export default function Page() {
  const router = useRouter()

  return (
    <div>
      <button onClick={() => router.push('/dashboard')}>
        Go to Dashboard
      </button>
    </div>
  )
}

Available Methods

The useRouter() hook returns an object with the following methods:

Method Description Use Case
push(href) Navigates to the provided route and adds a new entry to browser history. Standard navigation to a new page.
replace(href) Navigates to a new route without adding a new history entry. Redirect after login or form submission.
refresh() Refreshes the current route and re-fetches Server Component data. Updating UI after data changes without a full reload.
back() Navigates to the previous page in browser history. Custom "Back" button.
forward() Navigates to the next page in browser history. Browser-like navigation controls.
prefetch(href) Preloads a route in advance for faster navigation. Optimizing performance for anticipated navigation.

router.refresh() vs. Full Reload

The refresh() method is unique to the App Router. It is highly efficient because:

  1. Re-fetches server data: from the server for all Server Components in the current tree.
  2. preserves client-side state : (e.g., text in an < input>, scroll position, React state).
  3. It does not perform a hard browser reload; only the data and server-rendered UI are updated.

Best Practices

  • Use <Link> First: For standard navigation (clickable text or images), always use the <Link> component. It provides better SEO, accessibility, and automatic prefetching. Use useRouter() only for event-driven navigation.
  • Avoid Over-Refreshing: Only call router.refresh() when server-side data has changed but you are not navigating away from the page.
  • Check the Import: Always import the hook from next/navigation. Importing from next/router will cause errors when using the App Router (app directory).

useSearchParams

The useSearchParams() hook is a Client Component hook that allows you to read the query string parameters of the current route. It provides a read-only version of the Web URLSearchParams interface.

Key Characteristics

Because search parameters are only available in the browser during client-side navigation, this hook requires the 'use client' directive.

Feature Description
Return Value A read-only URLSearchParams object.
Re-rendering The component re-renders when the query string changes.
Client-Only Requires the 'use client' directive.
Usage Ideal for reading query values like filters, search input, or pagination (e.g., ?page=2).

Basic Implementation

TypeScript
'use client'

import { useSearchParams } from 'next/navigation'

export default function SearchBar() {
  const searchParams = useSearchParams()
  
  // Example URL: /shop?category=shoes&sort=asc
  const category = searchParams.get('category') // "shoes"
  const sort = searchParams.get('sort')         // "asc"

  return (
    <div>
      <p>Category: {category}</p>
      <p>Sorted by: {sort}</p>
    </div>
  )
}

Updating Search Parameters

The useSearchParams() hook is read-only. To update the URL parameters, use the useRouter() hook or a <Link> component to navigate to a new URL.

'use client'

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

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

  const handleFilter = (value: string) => {
    // 1. Create a mutable copy of current params
    const params = new URLSearchParams(searchParams.toString())
    
    // 2. Update the parameter
    params.set('sort', value)

    // 3. Push the new URL
    router.push(`${pathname}?${params.toString()}`)
  }

  return (
    <button onClick={() => handleFilter('desc')}>
      Sort Descending
    </button>
  )
}

Static Rendering & Suspense

One important behavior of useSearchParams() is its impact on static rendering. During the build process, Next.js cannot know what search parameters will be available because they are provided by the user at runtime.

  • The Problem: If a component uses useSearchParams(), Next.js will opt that part of the page out of static rendering.
  • The Solution: Wrap the component using useSearchParams() inside a <Suspense> boundary so the rest of the page can remain statically generated.
TypeScript
// app/search/page.tsx
import { Suspense } from 'react'
import SearchBar from './search-bar'

export default function Page() {
  return (
    <main>
      <h1>Search Results</h1>

      {/* Wrap component using useSearchParams */}
      <Suspense fallback={<div>Loading search...</div>}>
        <SearchBar />
      </Suspense>

    </main>
  )
}

Best Practices

  • Always use Suspense: Wrap any client component using useSearchParams() inside a <Suspense> boundary to prevent the entire page from becoming dynamic.
  • Server-side Alternative: If you need search parameters in a Server Component, use the searchParams prop automatically passed to the page (e.g., export default function Page({ searchParams }))
  • Avoid using search params for local state: Only use search parameters for state that should be shareable via URL (like filters, search queries, or pagination). For UI-only state (like toggles or modals), use useState

generateMetadata

In the Next.js App Router, metadata (like the <title> and <meta> tags) can be defined either statically or dynamically. The generateMetadata function is used specifically for dynamic metadata that relies on dynamic information, such as route parameters or fetched data.

Key Characteristics

generateMetadata is a server-side function. It allows you to fetch data and return a Metadata object before the page is rendered, ensuring search engines and social media crawlers see the correct information.

Feature Description
Environment Runs only on the Server.
Context Used in layout.js or page.js.
Async Support Can be async, allowing for database or API lookups.
Deduplication Fetch requests inside generateMetadata are automatically memoized alongside the page render.

asic Implementation

TypeScript
import { Metadata, ResolvingMetadata } from 'next'

type Props = {
  params: Promise<{ id: string }>
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}

export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {

  // 1. Read route params
  const id = (await params).id

  // 2. Fetch data (this fetch is memoized if also called in the Page)
  const product = await fetch(`https://api.example.com/products/${id}`).then((res) => res.json())

  // 3. (Optional) Read parent metadata
  const previousImages = (await parent).openGraph?.images || []

  return {
    title: product.title,
    description: product.description,
    openGraph: {
      images: [product.imageUrl, ...previousImages],
    },
  }
}

export default function Page({ params }: Props) {
  /* ... page content ... */
}

Parameters

The function receives two main arguments:

  • params: An object containing the dynamic route parameters. (Note: In Next.js 15+, this is a Promise.)
  • searchParams: An object containing the URL query string parameters.
  • parent: A promise of the metadata resolved from the parent segments (e.g., from a layout.js higher up the tree).
Strategy Usage Best For...
Static Metadata export const metadata = { ... } Pages with fixed content (e. Contact).
Dynamic Metadata export async function generateMetadata() { ... } Pages with variable content (posts, Products).

Metadata Fields

generateMetadata can return a wide range of fields to optimize SEO and social sharing:

  • title: Page title (can use template strings from parent layouts).
  • description: A summary of the page for search results.
  • openGraph: Metadata for Facebook/LinkedIn (images, site name, etc.).
  • twitter: Metadata specifically for X/Twitter cards.
  • robots: Instructions for search engine crawlers (index/noindex).

Best Practices

  • Don't fetch twice: If you fetch the same data in generateMetadata and your Page component, Next.js will automatically deduplicate the request. You don't need to worry about the performance hit of "fetching twice."
  • Title Templates: Use title.template in your root layout.js to automatically append your brand name to dynamic titles (e.g., title: { template: '%s | MyStore', default: 'MyStore' }).
  • Server-Side Only: Do not attempt to use browser-only APIs (like window.location) inside generateMetadata. Use the provided params and searchParams instead.
  • Fallback Values: Always provide fallback metadata in case a data fetch fails or returns an empty object to avoid empty <title> tags.

generateStaticParams

The generateStaticParams function is used in combination with Dynamic Route Segments to statically generate routes at build time instead of on-demand at request time.

It replaces the getStaticPaths function from the Pages Router, allowing you to define the list of path parameters that should be pre-rendered by the server.


Key Characteristics

When you use generateStaticParams, Next.js will run the function during the next build process to determine which paths to generate.

Feature Description
Performance Turns dynamic routes into fast, static HTML files served via CDN.
Deduplication Fetch requests inside this function are automatically memoized.
Smart Caching If data used in this function changes, the routes can be updated via ISR.
Context Only works in Server Components within dynamic segments (e.g., [slug]/page.tsx).

Basic Implementation

// app/blog/[slug]/page.tsx

export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then((res) => res.json())
 
  return posts.map((post) => ({
    slug: post.slug,
  }))
}

export default function Page({ params }: { params: { slug: string } }) {
  const { slug } = params
  // ... render the post
}

Behavior with Nested Segments

If you have nested dynamic routes (e.g., app/products/[category]/[id]/page.tsx), you can generate params for multiple levels.

Segment Level Responsibility
Child Only A generateStaticParams in a child segment can access params from its parent.
Top-down A generateStaticParams in a parent segment can provide params for all its children.

Handling Unmatched Paths: dynamicParams

By default, if a user visits a path that was not generated by generateStaticParams, Next.js will attempt to render it on-demand (Dynamic Rendering). You can control this behavior using the dynamicParams constant:

TypeScript
// (Default) Paths not generated at build time are rendered on-demand.
export const dynamicParams = true

// Paths not generated at build time will return a 404.
export const dynamicParams = false

Comparison: Static vs. Dynamic

Feature Using generateStaticParams Without generateStaticParams
Rendering Static (SSG) Dynamic (SSR)
TTFB Near-instant (Cached) Slower (Wait for server)
Build Time Increases (must pre-render pages) Fast
Use Case Blog posts, product catalogs Search results, user dashboards

Best Practices

  • Return Objects: The function must return an array of objects where each key matches the name of the dynamic segment (e.g., if the folder is [id], return { id: '1' }).
  • Limit Path Count: If you have thousands of pages, generating them all at build time can make your builds very slow. Generate the most popular pages and let the rest be generated on-demand via dynamicParams = true.
  • String Values: Ensure the values returned in the objects are strings. Even if your ID is a number, it must be passed as a string (e.g., { id: '42' }).
  • Revalidation: Use export const revalidate = 3600 to ensure your statically generated paths are updated periodically without a full rebuild.

NextRequest & NextResponse

The NextRequest and NextResponse objects are Next.js-specific extensions of the standard Web Request and Response APIs. They provide additional helpers for server-side tasks like cookie management, header manipulation, and URL parsing.

NextRequest

NextRequest is used to inspect the incoming request. It is commonly used in Middleware and Route Handlers.

Extension Description
nextUrl An enhanced URL object with Next.js properties like pathname, query, and locale.
cookies A helper to read, check, or iterate through request cookies.
ip (Vercel/Edge only) Returns the IP address of the requester.
geo (Vercel/Edge only) Returns geographic metadata (city, country, region).

Example: Using nextUrl in Middleware


import { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Use nextUrl to easily check the path
  const region = request.geo?.region || 'US'
  if (request.nextUrl.pathname === '/shop') {
    // Logic based on region
  }
}

NextResponse

NextResponse is used to generate the server's response. It allows you to produce redirects, rewrites, and custom JSON responses.

Method Description Use Case
NextResponse.json() Creates a response with a JSON body and the correct Content-Type. Standard API responses.
NextResponse.redirect() Sends a redirect response to a new URL (307/308). Auth guards or moving users.
NextResponse.rewrite() Displays a different URL's content without changing the browser URL. A/B testing or masking routes.
NextResponse.next() Continues the middleware chain. Passing the request forward.

Example: Setting Cookies and Headers


import { NextResponse } from 'next/server'

export function GET() {
  const response = NextResponse.json({ message: 'Success' })

  // Set a custom header
  response.headers.set('x-custom-id', '123')

  // Set a cookie
  response.cookies.set('theme', 'dark', { httpOnly: true })

  return response
}

Comparison: Standard API vs. Next.js Extension

Feature Standard Request/Response NextRequest/NextResponse
Cookie Access Manual parsing of Cookie string. cookies.get() / cookies.set()
URL Parsing new URL(request.url) required. nextUrl available out of the box.
Rewriting Not natively supported. NextResponse.rewrite() supported.
Environment Browser/Node.js compatible. Optimized for Edge and Node.js runtimes.

Best Practices

  • Use .next() in Middleware: When you don't need to redirect or block a request in Middleware, always return NextResponse.next() to allow the request to proceed.
  • Prefer .json() for APIs: Use the built-in .json() method in Route Handlers. It automatically sets the correct Content-Type: application/json header and handles object serialization.
  • Immutability: Remember that NextRequest is read-only. To "modify" a request (like adding a header), you must use a rewrite or set headers on the response that the client will eventually receive.
  • Type Safety: In TypeScript, always import these types from next/server to ensure full autocomplete for Next-specific properties.

Configuration (next.config.js) Last updated: Feb. 28, 2026, 7:31 p.m.

The next.config.js file is the primary configuration point for a Next.js project. It is a regular JavaScript module that is used by the Next.js server and build phases. Here, you can define settings for image domains, custom Webpack configurations (if absolutely necessary), and environment-specific behaviors. It serves as the bridge between your application code and the underlying Node.js runtime environment.

A significant portion of the configuration involves managing the Build Output. You can enable experimental features to test upcoming Next.js capabilities or optimize the compiler settings to strip console logs and minify code more aggressively. As your project moves toward production, this file becomes crucial for ensuring your app behaves correctly in different environments, from local development to global edge deployments.

basePath

The basePath configuration allows you to deploy a Next.js application under a sub-path of a domain. This is essential when your application is not hosted at the root (e.g., example.com/) but rather in a specific sub-folder (e.g., example.com/docs).


Key Characteristics

When basePath is set, all paths in your application (links, images, and API routes) are automatically prefixed with that value.

Feature Description
Prefixing Automatically adds the sub-path to all internal links and assets.
Configuration Must be a string starting with a slash / and must not end with a slash.
Routing The application will only respond to requests starting with the basePath.

Basic Implementation

To enable a base path, add the basePath key to your next.config.js file:


// next.config.js
module.exports = {
  basePath: '/docs',
}

How It Affects Your Code

Next.js handles the prefixing for most core components, but there are a few nuances to keep in mind:

Component / Utility Behavior with basePath: '/docs'
<Link> href="/about" becomes /docs/about.
useRouter() router.push('/settings') navigates to /docs/settings.
Image Component src="/me.png" becomes /docs/me.png.
Public Folder Assets in public/ are served at /docs/....
CSS url() Not automatically prefixed. You must manually add the prefix.

Linking to the Root (Escaping basePath)

If you need to link to a page outside of your Next.js application (on the same domain but outside the basePath), you must use a standard <a> tag or set the legacyBehavior prop, as <Link> will always attempt to append the prefix.

Use Cases

  • Documentation Sites: Hosting a blog at /blog or documentation at /docs while the main site is a separate app.
  • Micro-frontends: Running multiple independent Next.js applications on a single domain, each isolated to its own path.
  • Legacy Migrations: Gradually moving a site to Next.js by hosting the new version under a specific subdirectory.

Best Practices

  • Use Relative Paths: Always use relative paths (starting with /) in your <Link> components; Next.js will handle the basePath injection for you.
  • Consistency: Ensure your deployment environment (like Nginx or a Load Balancer) is configured to route traffic for that specific sub-path to your Next.js server.
  • Environment Variables: If you deploy the same app to different sub-paths (e.g., /staging/docs vs /docs), use an environment variable in your config:

basePath: process.env.NEXT_PUBLIC_BASE_PATH || '',

images

The images configuration in next.config.js allows you to define how the Next.js Image Optimization API behaves. This is critical for security and performance, as it controls which external domains are allowed to serve images and how those images are processed.

Key Configuration Options

The images object contains several properties to fine-tune image handling.

Property Description
remotePatterns A list of allowed external domains and paths for image fetching (Recommended for security).
domains (Deprecated) A simpler list of allowed hostnames. Use remotePatterns instead.
formats Specifies the image formats the server should prioritize (e.g., image/avif, image/webp).
deviceSizes Breakpoints for generating optimized versions of images for different screens.
loader Allows using a third-party image provider (e.g., Cloudinary, Imgix) instead of the built-in optimizer.

remotePatterns (Security)

To prevent "Image Request Forgery" (where malicious actors use your server to optimize images from any site), Next.js requires you to whitelist external sources. remotePatterns allows for precise control using protocol, hostname, and port.


// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com',
        port: '',
        pathname: '/account/**', // Allows images only from account folders
      },
    ],
  },
}

Image Formats

By default, Next.js serves WebP images. You can enable AVIF support for even better compression (typically 20% smaller than WebP), though it takes longer to encode on the server.


module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'],
  },
}

Custom Loaders

If you don't want the Next.js server to handle image processing (to save on server resources or costs), you can use a custom loader. This changes the src of the <img> tag to point directly to your image CDN's URL.

Loader Type Behavior
default Uses the built-in Next.js optimization API (/_next/image).
cloudinary Generates URLs formatted for Cloudinary's API.
imgix Generates URLs formatted for Imgix's API.
custom Requires you to define a loader function in your next/image component.

Comparison: Local vs. Remote Images

Feature Local Images (import) Remote Images (src="url")
Dimensions Automatically inferred from the file. width and height must be provided.
Blur Placeholder Generated automatically at build time. Requires a manual blurDataURL.
Config Needed None. Requires remotePatterns entry.

Best Practices

  • Avoid domains: Use remotePatterns because it is more secure (it allows you to restrict by protocol and path).
  • Use AVIF Sparingly: While AVIF results in smaller files, it increases the CPU load on your server during the first request. Use it if your traffic justifies the extra compute cost for bandwidth savings.
  • Set Cache TTL: By default, optimized images are cached for 60 seconds (unless headers from the source say otherwise). Ensure your source image server has correct Cache-Control headers for optimal performance.
  • Device Sizes: Don't over-configure deviceSizes. The default values are designed to cover standard mobile, tablet, and desktop breakpoints.

output

The output configuration in next.config.js determines how Next.js builds and packages your application. It is primarily used to optimize the build for specific deployment environments, such as Docker containers or static hosting services.

Available Output Modes

Next.js supports three primary output modes, each serving a different architectural purpose.

Mode Configuration Use Case
Default (Omitted) Standard Node.js server deployment (e.g., Vercel, manual Node server).
Standalone 'standalone' Docker containers or environments with limited disk space.
Export 'export' Static Site Generation (SSG) for hosting on S3, NGINX, or GitHub Pages.

Standalone Output ('standalone')

When set to standalone, Next.js creates a minimal build folder that includes only the necessary files to run the production server, including a subset of node_modules.

  • Benefit: Reduces the Docker image size significantly (often by 80% or more).
  • Mechanism: It uses "Output File Tracing" to detect which files are actually required at runtime.
  • Result: It generates a folder at .next/standalone which can be run using node server.js without needing to install the full node_modules again.

// next.config.js
module.exports = {
  output: 'standalone',
}

Export Output ('export')

Setting output to export enables Static Exports. This tells Next.js to transform your routes into static HTML, CSS, and JavaScript files.

  • Requirement: You cannot use features that require a Node.js server (like dynamic headers(), cookies(), or non-static rewrites).
  • Result: Generates an out/ folder containing the entire static site.

// next.config.js
module.exports = {
  output: 'export',
}

Comparison: Standalone vs. Export

Feature standalone export
Server Needed Yes (Node.js) No (Static Server/CDN)
Dynamic Routes Supported (SSR) Only if pre-rendered (SSG)
Image Optimization Works (default) Requires a third-party loader
Middleware Supported Not supported

Best Practices

  • Docker Users: Always use output: 'standalone'. It makes your deployments faster and your images much leaner.
  • Static Hosting: Use output: 'export' only if you are hosting on a service that does not support Node.js (like an AWS S3 bucket or a basic PHP server).
  • Tracing: If your standalone build is missing files (like local fonts or specific config files), use the experimental.outputFileTracingIncludes option to manually include them.
  • Environment Variables: Remember that for export, environment variables are baked in at build time. For standalone, you can often inject them at runtime.

redirects

The redirects configuration in next.config.js allows you to redirect an incoming request path to a different destination path. These are performed at the server level before the request even reaches the Next.js routing engine, making them highly efficient for SEO and site restructuring.

Key Properties

The redirects key is an async function that returns an array of objects.

Property Type Description
source String The incoming request path pattern.
destination String The path you want to route the user to.
permanent Boolean true (308 status) or false (307 status).
basePath Boolean Whether to include the basePath in the redirect (defaults to true).

Basic Implementation

JavaScript


// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: '/about-us',
        destination: '/about',
        permanent: true, // 308 Permanent Redirect
      },
      {
        source: '/old-blog/:slug',
        destination: '/news/:slug',
        permanent: false, // 307 Temporary Redirect
      },
    ]
  },
}

Advanced Matching

Next.js uses path-to-regexp for matching, which allows powerful dynamic redirection.

Pattern Type Source Example Matches
Path Matching /post/:id /post/123 (redirects to destination with :id)
Catch-all /blog/:slug* /blog/2026/jan/post (matches all nested segments)
Regex Matching /user/:id(\d{1,}) Matches only if :id is numeric (e.g., /user/123)

Redirects with Headers, Cookies, and Query Params

You can trigger redirects only when specific conditions are met using the has or missing arrays.

module.exports = {
async redirects() {
return [
{
source: '/',
has: [{ type: 'cookie', key: 'authorized', value: 'true' }],
permanent: false,
destination: '/dashboard',
},
]
},
}

redirects() vs. Middleware vs. redirect()

Method Level Use Case
next.config.js Infrastructure Static, permanent redirects for SEO and renamed paths.
Middleware Request Dynamic logic (Auth, Geo-fencing, A/B testing).
redirect() Component Logic inside a Server Action or Page (e.g., "User not found").

Best Practices

  • Use 308 for SEO: When permanently moving content, use permanent: true. This tells search engines to transfer "link juice" to the new URL.
  • Avoid Infinite Loops: Ensure your source and destination don't point to each other, which would crash the browser's request.
  • Keep it Simple: While redirects() supports regex, keep patterns readable. For highly complex logic involving hundreds of dynamic redirects, consider using a database lookup within Middleware instead.
  • External Redirects: You can redirect to external URLs by providing a full URL in the destination field (e.g., https://external-site.com).

rewrites

The rewrites configuration in next.config.js allows you to map an incoming request path to a different destination path without changing the URL shown in the browser's address bar. This "masks" the underlying source, making it appear as though the content is hosted at the requested URL.

Key Characteristics:

Feature Description
URL Masking The browser URL remains exactly what the user typed.
SEO Friendly Search engines index the source URL, not the hidden destination.
Internal/External Can rewrite to internal routes or external URLs (Proxying).
Execution Order Run after headers and redirects, but before the filesystem.

Basic Implementation

Rewrites can be defined as a simple array or an object with specific execution phases (beforeFiles, afterFiles, fallback).


// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/about-us',
        destination: '/about', // Masking /about as /about-us
      },
      {
        source: '/blog/:slug',
        destination: 'https://external-blog-site.com/:slug', // Proxying to external site
      },
    ]
  },
}

Rewrite Execution Phases

To give you more control, rewrites can return an object containing three different arrays:

Phase Behavior Use Case
beforeFiles Checked before looking at any Next.js files or pages. Overriding a page that exists in the filesystem.
afterFiles Checked only if a matching file/page is not found. Default routing logic or dynamic pages.
fallback Checked last, after both files and dynamic routes fail. Proxying "not found" pages to another service.

Comparison: Rewrites vs. Redirects

Feature rewrites redirects
Browser URL Stays the same. Changes to destination.
HTTP Status 200 (Success). 307/308 (Redirect).
User Awareness User doesn't know content is elsewhere. User sees the new URL.
Best For Proxying APIs, A/B testing, masking. Renaming pages, SEO moves, Auth flows.

Common Use Cases

  • Incremental Adoptions: Rewriting /old-app/:path* to a legacy server while you migrate parts to Next.js one by one.
  • API Proxying: Avoiding CORS issues by rewriting /api/:path* to https://api.thirdparty.com/:path*.
  • Microservices: Routing different URL sub-paths to completely separate servers while keeping them under a single domain.

Best Practices

  • External Security: When rewriting to an external URL, ensure you trust the destination, as your server is acting as the intermediary.
  • Avoid Conflicts: Be careful with beforeFiles. If you rewrite / to /login, you will never be able to reach your actual homepage.
  • Use Path Matching: Use dynamic segments (like :slug) or catch-alls (like :path*) to keep your rewrite rules concise.
  • Middleware Alternatives: If you need to rewrite based on a user’s cookie or geolocation, use Middleware instead of next.config.js, as it allows for dynamic logic.

headers

The headers configuration in next.config.js allows you to set custom HTTP response headers for specific incoming request paths. This is the primary way to manage security policies, caching instructions, and custom metadata at the infrastructure level.


Key Properties

The headers key is an async function that returns an array of objects.

Property Type Description
source String The incoming request path pattern (supports regex and glob patterns).
headers Array An array of objects containing key and value pairs for the response.
basePath Boolean Whether to include the basePath in the match (defaults to true).

Basic Implementation


// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/about',
        headers: [
          {
            key: 'x-custom-header',
            value: 'my-custom-value',
          },
          {
            key: 'x-another-custom-header',
            value: 'my-other-value',
          },
        ],
      },
    ]
  },
}

Security Headers Example

One of the most common use cases for this configuration is implementing security best practices to protect your users and data.

Header Purpose
Content-Security-Policy Controls which resources (JS, CSS, Images) the browser is allowed to load.
X-Frame-Options Prevents clickjacking by forbidding your site from being rendered in an <iframe>.
X-Content-Type-Options Prevents the browser from "sniffing" the content type (forces nosniff).
Strict-Transport-Security (HSTS) Tells the browser to only access the site via HTTPS.

const securityHeaders = [
  { key: 'X-Frame-Options', value: 'DENY' },
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'Referrer-Policy', value: 'origin-when-cross-origin' },
]

module.exports = {
  async headers() {
    return [
      {
        source: '/:path*', // Apply to all routes
        headers: securityHeaders,
      },
    ]
  },
}

Conditional Header Matching

Similar to redirects and rewrites, you can apply headers only when specific conditions (like cookies or query parameters) are present using has or missing.

JavaScript


module.exports = {
  async headers() {
    return [
      {
        source: '/api/:path*',
        has: [{ type: 'header', key: 'x-api-version', value: 'v1' }],
        headers: [
          { key: 'x-api-status', value: 'deprecated' },
        ],
      },
    ]
  },
}

Comparison: next.config.js vs Middleware vs Route Handlers

Level Method Best For...
Global/Static next.config.js Security headers (CSP, HSTS) and static cache rules.
Request Logic Middleware Conditional headers based on auth, geo, or device type.
API/Route Route Handlers Specific content types or API-specific metadata.

Best Practices

  • Global Patterns: Use source: '/:path*' to apply security headers to every page in your application.
  • Caching: While Next.js handles much of the caching logic, static assets in the public folder are better managed via Cache-Control headers rather than the Image Optimizer.
  • Order Matters: Headers are processed in the order they appear in the configuration array.
  • Avoid Overwriting: Be careful not to override essential headers set by your hosting provider (such as Vercel), as they are required for proper platform functionality.

logging

The logging configuration in next.config.js allows you to control how Next.js displays console logs during development mode. Specifically, it provides a way to visualize Data Fetching activities, helping you see which requests are hitting the cache and which are being fetched from the network.

Key Characteristics

This feature is designed to improve the developer experience (DX) by making the "hidden" behavior of the Next.js Data Cache transparent in your terminal.

Feature Description
Visibility Displays URL, status code, and cache status (HIT/MISS/SKIP) for fetch requests.
Environment Only applicable during Development (next dev). It does not affect production logs.
Scope Tracks requests made within Server Components and Route Handlers.

Basic Implementation

To enable fetch logging, add the logging object to your next.config.js:


// next.config.js
module.exports = {
  logging: {
    fetches: {
      fullUrl: true, // Shows the complete URL instead of a truncated version
    },
  },
}

Understanding Log Output

When enabled, your terminal will show logs similar to this:


GET /api/products 200 in 150ms (cache: HIT)
Cache Status Meaning
HIT Data was retrieved from the Next.js Data Cache. No network request was made.
MISS Data was not in the cache. A network request was made and the result was cached.
SKIP The cache was bypassed (e.g., due to cache: 'no-store' or usage of dynamic functions like cookies()).
SET The data was written to the cache for the first time.

Why Use Fetch Logging?

  • Debugging Performance: Identify "waterfalls" where multiple fetches are happening sequentially instead of in parallel.
  • Cache Verification: Ensure that your revalidate settings are working as expected and that you aren’t accidentally bypassing the cache.
  • Network Reduction: See exactly how many outgoing requests your server is making to third-party APIs or databases.

Best Practices

  • fullUrl: true: Enable this if you use multiple similar API endpoints (e.g., /api/user/1 vs /api/user/2) to distinguish between them in logs.
  • Noise Management: If your application performs a high volume of fetches, your terminal can become cluttered. Enable logging only when actively debugging.
  • Production Logging: Remember that this configuration only affects development mode. For production, use dedicated logging/monitoring tools.

Pages Router Basics Last updated: Feb. 28, 2026, 7:31 p.m.

Only needed for older projects. This is distinct from the App Router.

The Pages Router is the original routing system for Next.js, where every file in the pages/ directory automatically becomes a route. This system relies heavily on the _app.js and _document.js files to manage global state and HTML structure. Unlike the App Router, the Pages Router does not support Server Components; every page is a traditional React component that hydrates on the client, often resulting in larger JavaScript bundles.

Data fetching in this model is handled through specialized functions like getStaticProps and getServerSideProps, which are exported from the page file. While this model is considered "legacy" compared to the App Router, it remains extremely robust and is still used in thousands of production applications. Understanding the Pages Router is essential for maintaining older projects or when migrating a codebase incrementally to the modern App Router architecture.

Pages & Layouts (Legacy)

In the Pages Router, routing is handled by the pages directory. Unlike the App Router’s folder-based system, the Pages Router uses a file-based system where each file automatically becomes a route.

File-Based Routing

The structure of the pages folder directly maps to the URL structure of your site.

File Path URL Type
pages/index.js / Home Page
pages/about.js /about Static Route
pages/posts/[id].js /posts/123 Dynamic Route
pages/settings/index.js /settings Nested Route

the _app.js and _document.js Files

The Pages Router does not have layout.js files by default. Instead, it uses two special files to handle global logic.

  • pages/_app.js: This is the "Root Component." It is used to initialize pages, persist layouts when navigating between pages, and inject global CSS.
  • pages/_document.js: This is used to augment the application's <html> and <body> tags. It only renders on the server and is used for SEO tags or web fonts.

Implementing Layouts (Legacy Pattern)

Since there is no automatic layout.js nesting, developers typically use one of two patterns to share UI (like Navbars/Footers).

Pattern 1: The Global Layout (_app.js)

Use this if every single page on your site shares the same layout.


// pages/_app.js
import Layout from '../components/layout'

export default function MyApp({ Component, pageProps }) {
  return (
    <Layout>
      <Component {...pageProps} />
    </Layout>
  )
}

Pattern 2: Per-Page Layouts (Recommended)

Use this if different sections of your site need different layouts (e.g., a Dashboard vs. a Landing Page).


// pages/about.js
import Layout from '../components/layout'

export default function About() {
  return <p>About Page Content</p>
}

About.getLayout = function getLayout(page) {
  return <Layout>{page}</Layout>
}

Feature Pages Router App Router
Routing Files in pages/ Folders in app/
Layouts Manual via _app.js Automatic via layout.js
Components All components are Client Components Server Components by default
Data Fetching getStaticProps / getServerSideProps fetch with async/await

Best Practices

  • Use _app.js for Globals: Use this file for global state providers (like Redux or QueryClient) and global styles.
  • Custom Document: Use _document.js only when you need to modify the structural HTML. Do not put application logic or styling logic here.
  • Client Side: Remember that in the Pages Router, almost everything is hydrated on the client side, so you must be mindful of your bundle size more strictly than in the App Router.

Data Fetching (getServerSideProps)

In the Pages Router, getServerSideProps is the primary method for fetching data at every request. When a user requests a page that exports this function, Next.js will pre-render the page on the server using the data returned by it.

Key Characteristics

Feature Description
Timing Runs at request time. Every time you refresh the page, it runs again.
Hydration The server sends the JSON result to the client so the page can hydrate correctly.
Security Safe for sensitive logic (database queries, API keys) as the code is stripped from the client bundle.
Performance Slower than getStaticProps because the server must wait for data on every hit.

Basic Implementation

JavaScript


// pages/profile.js

// This function runs on every request
export async function getServerSideProps(context) {
  const { params, req, res, query } = context;

  // 1. Fetch data from an external API or Database
  const resData = await fetch(`https://api.example.com/user/profile`);
  const data = await resData.json();

  // 2. Pass data to the page via props
  return {
    props: {
      user: data,
    },
  };
}

export default function Profile({ user }) {
  return <h1>Welcome, {user.name}</h1>;
}

The context Parameter

The context object provides essential information about the incoming HTTP request:

  • params: Contains the route parameters if using a dynamic route.
  • req: The HTTP IncomingMessage object (to read cookies or headers).
  • res: The HTTP response object (to set headers or status codes).
  • query: An object representing the query string.

When to use getServerSideProps

Use this method only if you must fetch data that changes frequently or is user-specific.

Scenario Use getServerSideProps? Alternative
User Dashboard Yes (Data is private and unique). Client-side fetching (SWR).
Live News Feed Yes (Content must be fresh). ISR (Incremental Static Regeneration).
Marketing Page No (Content is the same for all). getStaticProps.
Search Results Yes (Depends on query params). Client-side fetching.

Comparison: Legacy vs. Modern (App Router)

Feature getServerSideProps (Pages) fetch (App Router)
Definition Exported function at the bottom. Inline async component logic.
Granularity All or nothing (entire page). Component-level fetching.
Data Format Must be serializable (JSON). Can be any JS object (in SC).

Best Practices

  • Avoid API Routes: Do not use fetch() to call a Next.js API Route inside getServerSideProps. Instead, import the logic/function directly to save a network hop.
  • Keep it Lightweight: Since the user sees a blank screen while this function runs, ensure your database queries are indexed and fast.
  • Caching: You can use the res.setHeader() method inside the function to set Cache-Control headers, allowing the page to be cached by a CDN/Edge.
  • Redirects/404: You can return { notFound: true } or { redirect: { destination: '/login' } } directly from the function.

Data Fetching (getStaticProps)

In the Pages Router, getStaticProps is used to fetch data at build time. This allows Next.js to pre-render the page into static HTML and JSON files, which can then be served via a CDN for maximum performance.

Key Characteristics

getStaticProps is the backbone of Static Site Generation (SSG). It ensures the page is generated once and reused for every user request until the next build (or revalidation).

Feature Description
Timing Runs at build time (next build) in production.
Performance Extremely fast; the page is served as a static file from a CDN.
SEO Excellent, as the full HTML content is available to crawlers immediately.
Environment Runs only on the server; the code is never sent to the browser.

Basic Implementation

JavaScript


// pages/products.js

// This function runs during the build process
export async function getStaticProps() {

  // 1. Fetch data from a CMS, database, or API
  const res = await fetch('https://api.example.com/products');
  const products = await res.json();

  // 2. Return the data as props
  return {
    props: {
      products,
    },

    // Optional: Re-generate the page every 10 seconds (ISR)
    revalidate: 10,
  };
}

export default function Products({ products }) {
  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

Incremental Static Regeneration (ISR)

One of the most powerful features of getStaticProps is the revalidate property. This allows you to update static pages after you've built your site without needing a full rebuild.

  • How it works: When a request comes in, Next.js checks the cached page. If the revalidate time has passed, it triggers a background regeneration. Once the new page is ready, it replaces the old one in the cache.

Comparison: getStaticProps vs. getServerSideProps

Feature getStaticProps getServerSideProps
When it runs Build time (or via ISR). Every request.
Speed (TTFB) Instant (served from CDN). Slower (waits for server/API).
Data Freshness Can be stale (depends on revalidate). Always fresh.
Best For Blogs, Documentation, Marketing. Dashboards, Personalized feeds.

Best Practices

  • Direct Database Access: Since this function only runs on the server, you can write database queries (like Prisma or SQL) directly inside getStaticProps.
  • The JSON Payload: Next.js generates a .json file for every static page. When navigating via next/link, the client fetches this JSON instead of the full HTML to make transitions instant.
  • Return 404/Redirects: If your data fetch fails or an item is missing, you can return { notFound: true } to trigger a 404 page, or { redirect: { destination: '/' } }.
  • Don't Fetch Internal APIs: Never use fetch() to call your own /pages/api routes inside getStaticProps. Import the logic/service directly to avoid unnecessary network overhead.

Data Fetching (getInitialProps)

getInitialProps is the original data-fetching method in Next.js. While it is still supported for legacy reasons, it is largely considered deprecated in favor of getStaticProps and getServerSideProps.


Key Characteristics

Unlike the newer methods which run strictly on the server, getInitialProps has an "isomorphic" (or hybrid) behavior.

Feature Description
Hybrid Execution Runs on the server for the initial page load, but runs on the client during client-side navigation (via next/link).
Blocking It blocks the page from rendering until the data is fetched, which can cause "stuttering" during navigation.
Usage Must be added as a static method to the Page component.
Legacy Status Not recommended for modern projects because it prevents Automatic Static Optimization.

Basic Implementation

JavaScript


function Page({ stars }) {
  return <div>Next.js has {stars} stars</div>
}

Page.getInitialProps = async (ctx) => {

  // Runs on server for first load
  // Runs on client for link-based navigation
  const res = await fetch('https://api.github.com/repos/vercel/next.js')
  const json = await res.json()

  return { stars: json.stargazers_count }
}

export default Page

The context Object

The context object in getInitialProps is slightly different depending on where it executes:

  • pathname: Current route path.
  • query: URL query string as an object.
  • asPath: The actual path (including query) shown in the browser.
  • req: HTTP request object (only available on the server).
  • res: HTTP response object (only available on the server).
  • err: Error object if any error is encountered during rendering.

Why move away from getInitialProps?

Drawback Impact
No Static Optimization Pages using getInitialProps cannot be statically optimized to HTML at build time; they are always rendered on the server.
Bundle Size Since the function runs on both server and client, all libraries used inside it (like db clients or large parsing tools) are included in the client-side JavaScript bundle.
User Experience Navigation feels slower because the browser waits for the data fetch to finish before switching to the new page.

Comparison: Legacy vs. Standard Pages Router

Feature getInitialProps getStaticProps / getServerSideProps
Runtime Client + Server Server Only
Optimization Dynamic Only Static or Server-rendered
Data Security Exposed to client Hidden from client

Best Practices

  • Migrate if possible: If you are working on a project using getInitialProps, consider migrating to getStaticProps or getServerSideProps to reduce client bundle size and enable static optimization.
  • Check Environment: If you must use it, always check if ctx.req exists to avoid running server-only code (like database scripts) on the client.
  • Use for _app.js: Occasionally, getInitialProps is still used in a custom pages/_app.js to fetch global data, though this is often better handled with the App Router’s layout.js.

API Routes (Legacy API)

In the Pages Router, API Routes provide a solution to build your own API endpoints within a Next.js app. Any file inside the pages/api folder is mapped to /api/* and treated as an API endpoint instead of a frontend page.

Key Characteristics

API Routes run on the Server (Node.js environment) and do not increase your client-side bundle size.

Feature Description
Server-Side Only They have access to databases, environment variables, and file systems.
No UI They return data (usually JSON) rather than React components.
Dynamic Routing Supports dynamic filenames like pages/api/user/[id].js.
Middleware Support Can use Connect-compatible middleware (like cors or multer).

Basic Implementation

// pages/api/user.js

export default function handler(req, res) {
  if (req.method === 'POST') {
    // 1. Process a POST request
    const data = req.body;
    res.status(201).json({ message: 'User created', data });
  } else {
    // 2. Handle any other HTTP method
    res.status(200).json({ name: 'John Doe' });
  }
} 

Request and Response Helpers

Next.js provides built-in helpers to make handling common API tasks easier:

Helper Type Description
req.body Object The parsed body of the request (defaults to JSON).
req.query Object The query string parameters (and dynamic route params).
req.cookies Object The cookies sent by the request.
res.status() Function Sets the HTTP status code (e.g., 200, 404, 500).
res.json() Function Sends a JSON response and sets the proper content-type.
res.redirect() Function Redirects the request to a specified path or URL.

Dynamic API Routes

Just like pages, you can create dynamic API endpoints by using brackets in the filename.

  • File: pages/api/post/[id].js
  • Request: /api/post/123
  • Access: const { id } = req.query will return { id: 123 }.

Comparison: Pages API vs. App Route Handlers

Feature Pages API Routes (pages/api) App Route Handlers (app/api)
Export Style export default function export function GET/POST/etc
Objects Node.js req / res Web standard Request / Response
File Name filename.js route.ts
Complexity Simple, centralized logic. More granular and type-safe.

Best Practices

  • Do Not Fetch Internally: Never use fetch() to call an API Route from getStaticProps or getServerSideProps. Call the logic/database directly to avoid an extra network hop.
  • Method Checking: Always check req.method to ensure your endpoint only responds to the intended HTTP verbs (e.g., preventing a GET request on a DELETE endpoint).
  • Environment Variables: Use process.env for sensitive keys. Since these files only run on the server, the keys will not be exposed to the browser.
  • CORS: By default, API Routes are same-origin. If you need to access them from other domains, you must implement CORS headers or use the cors middleware.

Custom App (_app.js)

In the Pages Router, the _app.js file is a special component that Next.js uses to initialize pages. It wraps every page in your application, acting as the global entry point. This is where you handle logic that should persist across page changes.

Key Responsibilities

The Custom App allows you to control the page initialization process.

Responsibility Description
Global Layouts Add UI elements (like Navbars) that stay visible on every page.
Global CSS This is the only place where you can import global .css files.
State Persistence Keep state (like Redux or React Context) consistent during navigation.
Custom Error Handling Inject global error boundaries or tracking scripts.
Data Injection Inject additional data into pages using getInitialProps.

Basic Implementation

If you don’t create this file, Next.js uses a default version. To override it, create pages/_app.js.

JavaScript

// pages/_app.js
import "../styles/globals.css" // Global CSS must be imported here

export default function MyApp({ Component, pageProps }) {
  // Component: The active page being viewed
  // pageProps: The props returned from getStaticProps/getServerSideProps

  return (
    <div className="app-container">
      <nav>My Navigation</nav>
      <Component {...pageProps} />
      <footer>My Footer</footer>
    </div>
  )
}
  

Comparison: _app.js vs. App Router layout.js

While they serve similar purposes, their behavior differs significantly:

Feature _app.js (Pages) layout.js (App)
Hierarchy Single, global file for the entire app. Can be nested at any folder level.
Re-rendering Re-renders on every page change. Persists and does not re-render on navigation.
Data Fetching Uses getInitialProps. Uses async/await and Server Components.
Styles Centralized global imports. Localized or global imports.

Advanced Usage: Per-Page Layouts

If only some pages need a specific layout (e.g., a Sidebar only for Dashboard pages), you can use the "Layout Property" pattern:

JavaScript

// pages/_app.js
export default function MyApp({ Component, pageProps }) {
  // Use the layout defined at the page level, if available
  const getLayout = Component.getLayout || ((page) => page)

  return getLayout(<Component {...pageProps} />)
}
  

Best Practices

  • Minimize Logic: Keep _app.js lightweight. Heavy logic here can slow down every single page transition in your application.
  • Global Providers: Wrap your Component with Context Providers (e.g., ThemeProvider, AuthProvider) here to ensure the state is available app-wide.
  • Avoid getInitialProps: Adding getInitialProps to _app.js will disable Automatic Static Optimization for every page in your entire application. Only use it if you absolutely must fetch data globally on every request.
  • Type Safety: If using TypeScript, use the AppProps type from next/app.

Custom Document (_document.js)

In the Pages Router, the _document.js file is a special file used to augment your application's <html> and <body> tags. Unlike _app.js, which stays active on the client-side, _document.js is only rendered on the server during the initial build or request.

Key Responsibilities

The Custom Document is primarily used for structural adjustments to the HTML document that sits outside the React application tree.

Responsibility Description
HTML/Body Attributes Adding languages (lang="en") or custom classes to the <body>.
External Scripts Loading third-party scripts (e.g., Google Analytics) before the app hydrates.
Web Fonts Injecting <link> tags for Google Fonts or Adobe Fonts.
SEO & Meta Setting global meta tags that don’t change between pages.
CSS-in-JS Integrating libraries like Styled-components or Emotion that require server-side style injection.

Basic Implementation

If you don’t create this file, Next.js uses its own default. To customize it, you must export a class or functional component that utilizes the specific components from next/document.

JavaScript

// pages/_document.js
import { Html, Head, Main, NextScript } from "next/document"

export default function Document() {
  return (
    <Html lang="en">
      <Head>
        {/* Custom fonts or global meta tags go here */}
        <link rel="preconnect" href="https://fonts.googleapis.com" />
      </Head>

      <body className="custom-theme">
        <Main /> {/* Where the _app.js and Pages are rendered */}
        <NextScript /> {/* Where Next.js injects its JS bundles */}
      </body>
    </Html>
  )
}
  

Critical Constraints

Because _document.js only runs on the server, it has several unique limitations:

  • No Event Handlers: You cannot add onClick or other interactive listeners here.
  • No Client Hooks: You cannot use useState, or other React hooks.
  • No Global CSS: Unlike _app.js, you cannot import global CSS files here.
  • Main Component: The <Main /> component is not hydrated by the browser; any React code outside of it will not be interactive.

Comparison: _document.js vs. _app.js

Feature _document.js _app.js
Execution Server-side only. Server & Client.
Purpose HTML Structure (<html>, <body>). Page Logic & State.
Interactivity None. Full (Hooks, Events).
Usage Frequency Rarely modified. Frequently modified.

Best Practices

  • lang Attribute: Always set the lang attribute in the <Html> tag for accessibility (A11y) and SEO.
  • Use next/script: For third-party scripts, prefer using the next/script component inside _app.js rather than raw <script> tags in _document.js for better performance.
  • Keep it Clean: Only put code here that must be present before the React application starts (like theme-blocking scripts to prevent FOUC — Flash of Unstyled Content).
  • Meta Tags: While you can put meta tags here, it is often better to use next/head inside individual pages for more granular SEO control.

Architecture Last updated: Feb. 28, 2026, 7:32 p.m.

Only needed for older projects. This is distinct from the App Router.

Next.js architecture is built on a "Server-First" philosophy, utilizing a custom Rust-based compiler (SWC) to handle transpilation and minification at extreme speeds. The core of the modern architecture is the React Server Components (RSC) payload, a compact data format that describes the UI tree without the need for the browser to download the underlying React component code. This allows for a unique hybrid model where the server and client collaborate to render the final page.

The framework also integrates a sophisticated Edge Runtime, a lightweight Node.js-compatible environment that allows Middleware and certain routes to run globally close to the user. This reduces latency and enables features like geo-location-based content and instant authentication checks. By combining high-performance tooling with a strategic distribution of work between the server, the edge, and the browser, Next.js architecture provides a foundation for the "Next Generation" of web development.

Accessibility

Next.js is designed to make web accessibility (A11y) a default priority rather than an afterthought. It provides built-in components and linting tools that automatically handle many of the complex aspects of the Web Content Accessibility Guidelines (WCAG).


Built-in Accessibility Features

Next.js automates several "invisible" accessibility tasks that developers often forget, ensuring that screen readers and keyboard users have a seamless experience.

Feature Mechanism Benefit
Route Announcements next/router / App Router Automatically announces page changes to screen readers (ARIA live regions).
Focus Management Internal Router Resets focus to the top of the page or specific container upon navigation.
Image Alt Text next/image Enforces the use of alt attributes to describe visual content to non-sighted users.
Script Loading next/script Ensures third-party scripts don’t block the main thread, keeping the UI responsive.

The Role of next/link in Accessibility

Navigation is one of the most critical parts of A11y. The next/link component handles several low-level requirements:

  • Native Anchor Tags: It renders a standard <a> tag, ensuring that browser features (like "Open in new tab") and screen readers recognize it as a link.
  • Keyboard Navigation: Supports Tab for selection and Enter for activation out of the box.
  • Prefetching: Speeds up the transition, reducing the "cognitive load" or frustration caused by slow-loading interactive elements.

Integrated Tooling: ESLint Plugin

Next.js includes a dedicated ESLint plugin (eslint-plugin-jsx-a11y) that catches accessibility errors during development before they ever reach production.

Rule Example Common Error Corrective Action
alt-text <img src="dog.jpg"> Add alt="A golden retriever".
aria-props <div aria-wrong="true"> Use valid aria-* attributes.
click-events-have-key-events <div onClick={...}> Use a <button> or add keyboard handlers.
role-has-required-aria-props <div role="checkbox"> Add aria-checked.

Server Components and A11y

Because React Server Components (RSC) send less JavaScript to the client, the page becomes interactive faster. This is particularly beneficial for users on assistive technologies or low-powered devices, as it prevents the "uncanny valley" where a page looks ready but doesn't respond to keyboard input.


Best Practices for Developers

  • Use Semantic HTML: Don't use a <div> when you can use a <button>, <nav>, or <main>. Next.js works best when your structure is meaningful.
  • Head Management: Use the Metadata API to ensure every page has a unique, descriptive <title>. This is the first thing a screen reader announces.
  • Color Contrast: While Next.js handles the code, ensure your CSS meets the 4.5:1 contrast ratio for standard text.
  • Skip Links: Implement a "Skip to content" link at the top of your layout.js to allow keyboard users to bypass the navigation menu.
TypeScript

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <a href="#main-content" className="sr-only-focusable">
          Skip to main content
        </a>

        <Navbar />

        <main id="main-content">{children}</main>
      </body>
    </html>
  )
}
  

Fast Refresh

Fast Refresh is a Next.js feature that gives you near-instant feedback for edits made to your React components. It is the evolution of "Hot Module Replacement" (HMR), designed to be more resilient and reliable by preserving component state during updates.


How It Works

When you save a file, Next.js performs a "surgical" update to the browser. Instead of reloading the entire page, it only updates the specific code that changed.

Action Result Effect on State
Edit Component Logic Re-renders only that component. Preserved (e.g., text in an input state).
Edit Styles Injects updated CSS. Preserved.
Syntax Error Displays an error overlay. Preserved once the error is fixed.
Edit Non-React File Re-renders the entire tree. Reset.

Key Features

  • Error Resilience: If you introduce a syntax or runtime error, Fast Refresh will pause. Once you fix the error, the app will resume from its previous state without a full reload.
  • State Preservation: Hooks like useState and useRef keep their values as long as you aren't changing the structure of the hooks themselves (e.g., changing the order of hooks).
  • Instant CSS: Changes to CSS Modules or Global CSS are applied immediately without triggering a JavaScript re-render.

Fast Refresh vs. Hot Reloading

Feature Hot Reloading (Old) Fast Refresh (Modern)
State Often lost on every save. Preserved across most edits.
Errors Often required a manual refresh. Recovers automatically after fix.
Reliability Prone to "ghost" bugs. Highly consistent.

Technical Constraints

To ensure Fast Refresh works correctly, Next.js follows specific rules. If these are violated, it will fall back to a full page reload:

  1. Named Exports Only: Fast Refresh only works when a file only exports React components. If you export a constant and a component, it may trigger a full reload.
  2. Class Components: Fast Refresh primarily targets Functional Components. Class components may lose state more frequently.
  3. Hooks dependency: If you change the dependencies of a useEffect or useMemo, that specific hook will re-run, but the rest of the component state will remain.

Best Practices

  • Export Components Cleanly: Avoid exporting non-component data (like large config objects) from the same file as your React component. Move them to a separate constants.js or utils.js file.
  • Functional Components: Use Functional Components and Hooks to get the best out of the state preservation logic.
  • Observe the Overlay: When a runtime error occurs, read the Fast Refresh overlay. It often points to the exact line and provides a "Click to open in editor" link for faster debugging.

Next.js Compiler

The Next.js Compiler is a high-performance compilation infrastructure written in Rust using the SWC (Speedy Web Compiler) platform. It replaced the previous Babel-based pipeline to significantly speed up build times and developer refreshes.

Key Capabilities

The compiler handles several critical tasks that transform your modern React code into optimized JavaScript for the browser.

Task Description
Transpilation Converts TypeScript/JSX into standard JavaScript.
Minification Shrinks bundle sizes by removing whitespace and shortening variables (replaces Terser).
Code Splitting Breaks the application into smaller chunks to improve load times.
Styled Components Native support for SSR-compatible styles without extra Babel plugins.

Why Rust/SWC?

The move from JavaScript-based tooling (Babel) to Rust-based tooling (SWC) was driven by the need for performance as projects scaled.

  • Speed: Up to 17x faster than Babel for individual file transpilation.
  • Efficiency: Much faster "Fast Refresh" during development (up to 3x faster).
  • Consolidation: Replaces multiple tools (Babel, Terser, CSS minifiers) with a single, unified engine.

Notable Built-in Transforms

The compiler includes several "transforms" that previously required complex manual configuration:

  • Tree Shaking: Automatically removes unused code from your bundles.
  • Regex Optimization: Optimizes regular expressions at build time.
  • React Remove Properties: Allows removing specific JSX properties (like data-test-id) from production builds to keep code clean.
  • Modularize Imports: Automatically transforms large imports (like lodash) to only include the specific functions used, reducing bundle size.

Configuration

Most users never need to configure the compiler, as it works out of the box. However, you can opt into specific features via next.config.js:

JavaScript

// next.config.js
module.exports = {
  compiler: {
    // Remove console.log in production
    removeConsole: true,

    // Enable styled-components support
    styledComponents: true,

    // Enable Emotion support
    emotion: true,
  },
};

Comparison: Babel vs. Next.js Compiler

Feature Babel (Legacy) Next.js Compiler (Modern)
Language JavaScript Rust
Build Speed Standard Extremely Fast
Customization Via .babelrc (highly flexible) Via next.config.js (standardized)
Modernity Mature ecosystem Current industry standard

Best Practices

  • Avoid .babelrc: If you add a custom .babelrc or babel.config.js to your project, Next.js will disable the Rust compiler and fall back to Babel. Only do this if you have a specific Babel plugin that isn't yet supported by SWC.
  • Monitor Build Logs: The terminal will notify you if it detects a configuration that forces a fallback to Babel.
  • Leverage removeConsole: Use the compiler options to strip logs for production instead of manually deleting them or using third-party scripts.

Supported Browsers

Next.js is built to support a wide range of browsers by balancing modern features with necessary polyfills for older environments.

Targeted Browser Ranges

Category Description Examples
Modern Browsers Supports latest ECMAScript features (ES6+). Chrome 64+, Edge 79+, Safari 67+
Server Side Requires a Node.js environment. Node.js 18.17.0 or later
Legacy Support Browsers requiring polyfills for stability. Older versions of Safari and Chrome

How Next.js Handles Compatibility

Next.js uses a "Differential Loading" strategy and automated polyfilling to ensure your app runs smoothly across different devices.

  • Browserslist: Next.js uses Browserslist to determine which CSS and JavaScript features need to be transformed. You can customize this by adding a .browserslistrc file or a browserslist key in your package.json.
  • Built-in Polyfills: Next.js automatically includes polyfills for essential features like fetch(), URL, and Object.assign to ensure they work even if the browser doesn't natively support them.
  • SWC Compilation: The Rust-based compiler transforms modern syntax (like Optional Chaining or Nullish Coalescing) into versions older engines can understand.

Important Deprecations

As the web evolves, Next.js periodically moves away from outdated technologies to keep the framework fast and secure.

  • Internet Explorer 11: Next.js 13 and later dropped support for IE11. If you require IE11 support, you must stay on Next.js 12 or manually configure complex polyfills (not recommended).
  • Node.js Versioning: Next.js strictly follows Node.js LTS (Long Term Support) schedules. Always ensure your deployment environment matches the minimum required version (currently Node.js 18.17+).

Comparison: Default vs. Custom Targets

Feature Default Target Custom Target (via Browserslist)
Bundle Size Balanced for general use. Can be smaller if targeting only modern browsers.
Compatibility High (covers 95%+ of users). Defined by your specific user base.
Configuration Zero config needed. Requires maintenance of a .browserslistrc file.

Best Practices

  • Check Your Analytics: Before narrowing browser support, check your site's analytics to see what browsers your actual visitors are using.
  • Avoid Over-Polyfilling: Don’t manually add large polyfill libraries like babel-polyfill. Next.js automatically includes only what is required.
  • Test on Safari: Safari (especially on iOS) often has unique rendering quirks or delayed support for certain Web APIs. Always include it in testing.
  • Use next/image: Different browsers support different image formats (like WebP or AVIF). The Image component ensures the most optimized format.

DocsAllOver

Where knowledge is just a click away ! DocsAllOver is a one-stop-shop for all your software programming needs, from beginner tutorials to advanced documentation

Get In Touch

We'd love to hear from you! Get in touch and let's collaborate on something great

Copyright copyright © Docsallover - Your One Shop Stop For Documentation