Skip to main content

Overview

Next.js is a React framework that provides a complete architecture for building production-ready applications. It combines server-side rendering, static generation, API routes, and optimized bundling into a cohesive system built on React.
Next.js abstracts complex infrastructure decisions while remaining flexible enough to optimize for different use cases through its hybrid rendering model.

File-based routing

Next.js implements routing through the file system structure, eliminating the need for explicit route configuration.

How it works

The pages directory (or app directory in Next.js 13+) maps directly to routes in your application.
pages/
├── index.js /
├── about.js /about
├── blog/
   ├── index.js /blog
   ├── [slug].js             → /blog/:slug
   └── [year]/[month].js     → /blog/:year/:month
├── products/
   └── [...categories].js    → /products/* (catch-all)
└── api/
    └── users.js /api/users

Dynamic routes

Files with bracket notation create dynamic route segments.
pages/blog/[slug].js
import { useRouter } from 'next/router';

export default function BlogPost() {
  const router = useRouter();
  const { slug } = router.query;

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

// Matches: /blog/hello-world, /blog/nextjs-routing

Implementation details

1

File system scanning

During build time, Next.js scans the pages directory and automatically generates route manifests based on file names and directory structure.
2

Route matching

Incoming requests are matched against the route manifest using a priority system:
  1. Exact static paths (/about)
  2. Dynamic routes (/blog/[slug])
  3. Catch-all routes (/products/[...categories])
3

Component loading

Matched routes load their corresponding page component. Next.js automatically code-splits each route, loading only the JavaScript needed for that page.
Performance benefit: File-based routing enables automatic code splitting, ensuring users only download the code for routes they visit.

Static Site Generation (SSG)

SSG pre-renders pages at build time, generating static HTML that can be served instantly from a CDN.

getStaticProps

The getStaticProps function runs at build time to fetch data and pass it as props to your page component.
export default function BlogIndex({ posts }) {
  return (
    <div>
      <h1>Blog Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}

export async function getStaticProps() {
  // This runs at BUILD TIME only
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();

  return {
    props: {
      posts,
    },
  };
}

Build-time execution

Understanding when and where getStaticProps executes is crucial for proper implementation.
getStaticProps runs in a Node.js environment during the build process, never in the browser.
export async function getStaticProps() {
  // ✅ Safe: Direct database access
  const data = await db.query('SELECT * FROM posts');

  // ✅ Safe: File system operations
  const content = await fs.readFile('./content.md', 'utf-8');

  // ✅ Safe: Environment variables (not exposed to client)
  const apiKey = process.env.SECRET_API_KEY;

  return { props: { data } };
}
1

Data fetching

Next.js executes getStaticProps for each static page during next build.
2

Page rendering

The returned props are passed to the page component, which renders to static HTML.
3

HTML generation

The rendered HTML and JSON data are written to disk in the .next directory.
4

Deployment

Static HTML files are deployed to CDN, ready to serve instantly on request.
For dynamic routes, combine getStaticProps with getStaticPaths to specify which paths to pre-render.
pages/blog/[slug].js
export async function getStaticPaths() {
  // Determine which paths to pre-render
  const posts = await fetchAllPosts();

  const paths = posts.map(post => ({
    params: { slug: post.slug },
  }));

  return {
    paths,
    fallback: false, // 404 for non-pre-rendered paths
  };
}

export async function getStaticProps({ params }) {
  const post = await fetchPost(params.slug);

  return {
    props: { post },
  };
}

export default function BlogPost({ post }) {
  return <article>{post.content}</article>;
}

When to use SSG

Marketing pages

Landing pages, about pages, and marketing content that rarely changes benefit from instant CDN delivery.

Blog posts

Content-focused sites where posts are written once and served many times.

Documentation

Technical documentation that updates infrequently but needs to be fast and SEO-friendly.

E-commerce listings

Product pages with relatively stable content (combine with ISR for updates).

Incremental Static Regeneration (ISR)

ISR enables updating static pages after build time without rebuilding the entire site, combining the performance of SSG with the freshness of SSR.

Revalidation mechanism

Add a revalidate property to getStaticProps to specify how often Next.js should regenerate the page.
export async function getStaticProps({ params }) {
  const product = await fetchProduct(params.id);

  return {
    props: { product },
    revalidate: 60, // Regenerate page at most once every 60 seconds
  };
}

export default function ProductPage({ product }) {
  return (
    <div>
      <h1>{product.name}</h1>
      <p>Price: ${product.price}</p>
      <p>In stock: {product.inventory}</p>
    </div>
  );
}

Stale-while-revalidate strategy

ISR implements a stale-while-revalidate caching pattern that balances freshness and performance.
1

First request (cache hit)

User requests a page. Next.js serves the cached static HTML instantly from CDN. The page may be stale, but the user sees content immediately.
2

Background regeneration

If the page is older than the revalidation time, Next.js triggers a background regeneration.
  • Fetches fresh data by executing getStaticProps
  • Renders new HTML with updated data
  • Does NOT block the current request
3

Cache update

Once regeneration completes, Next.js replaces the cached page with the new version. The original user still received the stale page instantly.
4

Subsequent requests

Future users receive the freshly regenerated page until the next revalidation window.
Build time:    Page generated with product price: $99
t=0s:          User A requests page → Serves cached $99 (instant)
t=65s:         User B requests page → Serves cached $99 (instant)
               → Triggers background regeneration (price now $89)
t=70s:         Regeneration completes, cache updated to $89
t=80s:         User C requests page → Serves fresh $89 (instant)
Key insight: ISR guarantees users always get fast responses (serving stale content) while ensuring content stays reasonably fresh through background updates.

On-demand revalidation

Trigger revalidation programmatically when you know content has changed, without waiting for the time-based window.
export default async function handler(req, res) {
  // Verify request is authorized
  if (req.headers.authorization !== `Bearer ${process.env.REVALIDATE_TOKEN}`) {
    return res.status(401).json({ message: 'Unauthorized' });
  }

  try {
    // Revalidate specific path
    await res.revalidate('/products/123');
    return res.json({ revalidated: true });
  } catch (err) {
    return res.status(500).send('Error revalidating');
  }
}

API routes

API routes provide a serverless backend solution within your Next.js application, running as serverless functions.

Creating API endpoints

Files in pages/api become API endpoints instead of pages.
// GET /api/users
export default async function handler(req, res) {
  if (req.method === 'GET') {
    const users = await db.query('SELECT * FROM users');
    res.status(200).json({ users });
  } else if (req.method === 'POST') {
    const user = await db.insert('users', req.body);
    res.status(201).json({ user });
  } else {
    res.status(405).json({ error: 'Method not allowed' });
  }
}

Middleware and helpers

Implement reusable logic for authentication, validation, and error handling.
lib/auth.js
export function withAuth(handler) {
  return async (req, res) => {
    const token = req.headers.authorization?.split(' ')[1];

    if (!token) {
      return res.status(401).json({ error: 'No token provided' });
    }

    try {
      const user = await verifyToken(token);
      req.user = user;
      return handler(req, res);
    } catch (error) {
      return res.status(401).json({ error: 'Invalid token' });
    }
  };
}
pages/api/profile.js
import { withAuth } from '@/lib/auth';

async function handler(req, res) {
  // req.user is populated by withAuth
  const profile = await db.findUser(req.user.id);
  res.status(200).json({ profile });
}

export default withAuth(handler);

Use cases

Form submissions

Handle contact forms, newsletter signups, and user-generated content.

Webhooks

Receive and process webhooks from third-party services like Stripe or GitHub.

Proxy endpoints

Hide API keys by proxying requests to external services through your backend.

Data aggregation

Combine data from multiple sources before sending to the client.

Module bundling and code splitting

Next.js automatically optimizes your JavaScript bundles for optimal loading performance.

Automatic code splitting

Every page in the pages directory becomes its own JavaScript bundle.
.next/static/chunks/
├── pages/
│   ├── index-[hash].js           // Only loaded for /
│   ├── about-[hash].js           // Only loaded for /about
│   └── blog/[slug]-[hash].js     // Only loaded for /blog/[slug]
├── framework-[hash].js            // React framework code
└── main-[hash].js                 // Next.js runtime

Dynamic imports

Manually code-split components that aren’t needed immediately.
import dynamic from 'next/dynamic';

// Loaded only when rendered
const DynamicComponent = dynamic(() => import('../components/Heavy'));

// Loaded only when rendered, with loading state
const DynamicWithLoading = dynamic(
  () => import('../components/Heavy'),
  {
    loading: () => <p>Loading...</p>,
    ssr: false, // Disable SSR for this component
  }
);

export default function Page() {
  return (
    <div>
      <DynamicComponent />
    </div>
  );
}

Shared dependencies

Next.js automatically creates shared chunks for dependencies used across multiple pages.
1

Dependency analysis

During build, Next.js analyzes which dependencies are imported by each page.
2

Chunk generation

Dependencies used by multiple pages are extracted into shared chunks to avoid duplication.
3

Optimal loading

When navigating between pages, only new chunks are downloaded. Shared dependencies are cached.
Next.js uses webpack (or Turbopack in Next.js 13+) with carefully tuned configuration to balance chunk size, caching efficiency, and loading performance.

Bundle optimization techniques

Next.js removes unused exports from your dependencies, reducing bundle size.
// Only Button code is included in bundle
import { Button } from 'huge-library';
All JavaScript is minified in production builds using Terser, reducing file size by 40-60%.
Static files are automatically compressed using gzip and brotli for faster transfer.
The next/image component automatically optimizes images, serving modern formats (WebP, AVIF) and appropriate sizes.
import Image from 'next/image';

<Image
  src="/photo.jpg"
  width={800}
  height={600}
  alt="Photo"
  // Automatically optimized and lazy loaded
/>

Architecture summary

Next.js combines multiple rendering strategies and architectural patterns into a cohesive framework:

File-based routing

Zero-config routing with automatic code splitting per page.

SSG with getStaticProps

Build-time rendering for maximum performance and SEO.

ISR

Static generation with background updates for fresh content.

API routes

Serverless backend functions co-located with frontend code.

Automatic bundling

Optimized code splitting and chunk generation.

Hybrid rendering

Mix SSG, ISR, and SSR on a per-page basis.

Build docs developers (and LLMs) love