Next.js: The Complete Guide for 2026

Published February 12, 2026 · 30 min read

Next.js is the leading React framework for building production-grade web applications. It provides server-side rendering, static site generation, API routes, file-based routing, and a powerful developer experience out of the box. Whether you are building a marketing site, a SaaS dashboard, or an e-commerce platform, Next.js gives you the architecture to ship fast and scale confidently.

This guide covers everything from project setup to production deployment, with practical code examples you can use immediately.

1. What Is Next.js and Why Use It

Next.js is a React framework created by Vercel that adds server-side rendering, routing, and build tooling on top of React. While React itself is a UI library, Next.js provides the full application framework — routing, data fetching, optimization, and deployment.

Next.js vs Other Frameworks

Choose Next.js when you need SEO-friendly rendering, a full-stack framework with API routes, image and font optimization, and the flexibility to mix static and dynamic pages in one application.

2. Project Setup and Structure

Create a new Next.js project with the App Router:

npx create-next-app@latest my-app
cd my-app
npm run dev

The CLI will prompt you for TypeScript, Tailwind CSS, ESLint, and the src/ directory. The default App Router structure looks like this:

my-app/
├── app/
│   ├── layout.tsx          # Root layout (wraps all pages)
│   ├── page.tsx            # Home page (/)
│   ├── globals.css         # Global styles
│   ├── about/
│   │   └── page.tsx        # /about
│   ├── blog/
│   │   ├── page.tsx        # /blog
│   │   └── [slug]/
│   │       └── page.tsx    # /blog/my-post (dynamic)
│   └── api/
│       └── hello/
│           └── route.ts    # API: GET /api/hello
├── public/                 # Static files (images, fonts)
├── next.config.js          # Next.js configuration
├── package.json
└── tsconfig.json

Every folder inside app/ becomes a URL segment. A page.tsx file makes that segment publicly accessible. A layout.tsx wraps its children and persists across navigation.

3. Routing

File-Based Routing

Next.js uses the filesystem as the router. Each folder in app/ maps to a URL path, and page.tsx defines the UI for that route:

// app/dashboard/settings/page.tsx
// URL: /dashboard/settings

export default function SettingsPage() {
  return <h1>Settings</h1>;
}

Dynamic Routes

Use square brackets for dynamic segments:

// app/blog/[slug]/page.tsx
// Matches: /blog/hello-world, /blog/nextjs-guide, etc.

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);

  return <article><h1>{post.title}</h1></article>;
}

// Catch-all route: app/docs/[...slug]/page.tsx
// Matches: /docs/a, /docs/a/b, /docs/a/b/c
// params.slug = ['a', 'b', 'c']

Route Groups

Parentheses create logical groups without affecting the URL:

app/
├── (marketing)/
│   ├── layout.tsx      # Marketing layout
│   ├── page.tsx        # / (home)
│   └── about/
│       └── page.tsx    # /about
├── (dashboard)/
│   ├── layout.tsx      # Dashboard layout (different nav)
│   ├── dashboard/
│   │   └── page.tsx    # /dashboard
│   └── settings/
│       └── page.tsx    # /settings

Parallel Routes

Parallel routes render multiple pages simultaneously in the same layout using named slots:

// app/layout.tsx
export default function Layout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <>
      {children}
      {analytics}
      {team}
    </>
  );
}

// app/@analytics/page.tsx — renders in the analytics slot
// app/@team/page.tsx — renders in the team slot

4. Server Components vs Client Components

In the App Router, all components are Server Components by default. They render on the server, send HTML to the client, and ship zero JavaScript for that component.

// Server Component (default) — no directive needed
// This component's code never reaches the browser
export default async function ProductList() {
  const products = await db.product.findMany();

  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name} — ${p.price}</li>
      ))}
    </ul>
  );
}

Add 'use client' when you need browser interactivity:

'use client';

import { useState } from 'react';

export default function AddToCart({ productId }: { productId: string }) {
  const [quantity, setQuantity] = useState(1);

  return (
    <div>
      <button onClick={() => setQuantity((q) => Math.max(1, q - 1))}>-</button>
      <span>{quantity}</span>
      <button onClick={() => setQuantity((q) => q + 1)}>+</button>
      <button onClick={() => addToCart(productId, quantity)}>
        Add to Cart
      </button>
    </div>
  );
}

Key rule: Keep Client Components as small as possible and push them to leaf nodes. A Server Component can import and render a Client Component, but a Client Component cannot import a Server Component (it can receive one as children though).

5. Data Fetching

Fetching in Server Components

Server Components can use async/await directly. Next.js extends fetch with caching and revalidation:

// Static data (cached indefinitely by default)
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    cache: 'force-cache', // default behavior
  });
  return res.json();
}

// Dynamic data (never cached)
async function getCurrentUser() {
  const res = await fetch('https://api.example.com/me', {
    cache: 'no-store',
  });
  return res.json();
}

// Time-based revalidation (refresh every 60 seconds)
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 },
  });
  return res.json();
}

Server Actions

Server Actions let you define server-side functions that can be called directly from Client Components — no API route needed:

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  await db.post.create({ data: { title, content } });
  revalidatePath('/blog');
}

// app/blog/new/page.tsx (Client Component using the action)
'use client';

import { createPost } from '../actions';

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <button type="submit">Publish</button>
    </form>
  );
}

6. Layouts, Templates, and Metadata

Root Layout

Every Next.js app requires a root layout. It wraps all pages and must include <html> and <body> tags:

// app/layout.tsx
import { Inter } from 'next/font/google';
import './globals.css';

const inter = Inter({ subsets: ['latin'] });

export const metadata = {
  title: { default: 'My App', template: '%s | My App' },
  description: 'A Next.js application',
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <nav>{/* Global navigation */}</nav>
        {children}
        <footer>{/* Global footer */}</footer>
      </body>
    </html>
  );
}

Nested Layouts

Layouts nest automatically. A layout in app/dashboard/layout.tsx wraps all pages under /dashboard:

// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="dashboard">
      <aside>Sidebar</aside>
      <main>{children}</main>
    </div>
  );
}

Metadata API

Export a metadata object or generateMetadata function from any page or layout:

// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPost(slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: { title: post.title, images: [post.coverImage] },
  };
}

7. API Routes (Route Handlers)

In the App Router, API routes are defined using route.ts files that export HTTP method functions:

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const page = parseInt(searchParams.get('page') || '1');

  const posts = await db.post.findMany({
    skip: (page - 1) * 10,
    take: 10,
  });

  return NextResponse.json({ posts, page });
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const post = await db.post.create({ data: body });
  return NextResponse.json(post, { status: 201 });
}

// app/api/posts/[id]/route.ts
export async function DELETE(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  await db.post.delete({ where: { id } });
  return new NextResponse(null, { status: 204 });
}

8. Middleware

Middleware runs before a request is completed. It can rewrite, redirect, modify headers, or block requests. Create a middleware.ts file at the project root:

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

export function middleware(request: NextRequest) {
  // Authentication check
  const token = request.cookies.get('session')?.value;

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

  // Add security headers
  const response = NextResponse.next();
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');

  // i18n: redirect based on Accept-Language
  if (request.nextUrl.pathname === '/') {
    const lang = request.headers.get('accept-language')?.split(',')[0];
    if (lang?.startsWith('fr')) {
      return NextResponse.redirect(new URL('/fr', request.url));
    }
  }

  return response;
}

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

9. Styling

CSS Modules

CSS Modules are supported out of the box. They scope styles to the component automatically:

/* app/components/Button.module.css */
.button { padding: 0.75rem 1.5rem; border-radius: 6px; font-weight: 600; }
.primary { background: #3b82f6; color: white; }
.outline { background: transparent; border: 2px solid #3b82f6; color: #3b82f6; }

// app/components/Button.tsx
import styles from './Button.module.css';

export function Button({ variant = 'primary', children }) {
  return (
    <button className={`${styles.button} ${styles[variant]}`}>
      {children}
    </button>
  );
}

Tailwind CSS

Tailwind is the most popular styling choice for Next.js. It works seamlessly with Server Components since it produces plain CSS:

// Server Component with Tailwind — no 'use client' needed
export default function Card({ title, description }) {
  return (
    <div className="rounded-lg border border-gray-200 p-6
                    hover:border-blue-500 transition-colors">
      <h3 className="text-lg font-semibold mb-2">{title}</h3>
      <p className="text-gray-600">{description}</p>
    </div>
  );
}

10. Image and Font Optimization

next/image

The Image component automatically optimizes images with lazy loading, responsive sizing, and modern formats (WebP, AVIF):

import Image from 'next/image';

export default function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero banner"
      width={1200}
      height={600}
      priority           // Load immediately (above the fold)
      placeholder="blur" // Show blur while loading
      blurDataURL="data:image/jpeg;base64,..."
    />
  );
}

// Remote images require configuration in next.config.js
// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.example.com' },
    ],
  },
};

next/font

Next.js automatically self-hosts fonts with zero layout shift:

import { Inter, Fira_Code } from 'next/font/google';
import localFont from 'next/font/local';

const inter = Inter({ subsets: ['latin'], display: 'swap' });
const firaCode = Fira_Code({ subsets: ['latin'], variable: '--font-mono' });
const customFont = localFont({ src: './fonts/CustomFont.woff2' });

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

11. Authentication Patterns

Auth.js (formerly NextAuth.js) is the standard authentication library for Next.js:

// auth.ts
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Google from 'next-auth/providers/google';
import Credentials from 'next-auth/providers/credentials';

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    GitHub({ clientId: process.env.GITHUB_ID, clientSecret: process.env.GITHUB_SECRET }),
    Google({ clientId: process.env.GOOGLE_ID, clientSecret: process.env.GOOGLE_SECRET }),
    Credentials({
      credentials: { email: {}, password: {} },
      authorize: async (credentials) => {
        const user = await verifyUser(credentials.email, credentials.password);
        return user ?? null;
      },
    }),
  ],
});

// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth';
export const { GET, POST } = handlers;

// Protect routes in Server Components
import { auth } from '@/auth';

export default async function DashboardPage() {
  const session = await auth();
  if (!session) redirect('/login');

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

12. Deployment

Vercel (Recommended)

Push to GitHub and import into Vercel. Automatic preview deployments, edge functions, and image optimization are included.

Self-Hosted with Docker

# Dockerfile
FROM node:20-alpine AS base

FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

EXPOSE 3000
CMD ["node", "server.js"]

Enable standalone output in your config:

// next.config.js
module.exports = {
  output: 'standalone',
};

Static Export

For purely static sites without server-side features:

// next.config.js
module.exports = {
  output: 'export',
  // Optional: trailing slashes for static hosting
  trailingSlash: true,
};

Run npm run build and deploy the out/ directory to any static host (Nginx, S3, Cloudflare Pages).

13. Performance: ISR, Streaming, and Suspense

Incremental Static Regeneration (ISR)

ISR lets you update static pages without rebuilding the entire site:

// app/products/[id]/page.tsx
export const revalidate = 3600; // Regenerate every hour

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const product = await getProduct(id);
  return <ProductView product={product} />;
}

// On-demand revalidation via Server Action
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';

export async function updateProduct(id: string, data: ProductData) {
  await db.product.update({ where: { id }, data });
  revalidatePath(`/products/${id}`);
  revalidateTag('products'); // Invalidate all fetches tagged 'products'
}

Streaming with Suspense

Stream slow components without blocking the entire page:

// app/dashboard/page.tsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      {/* Header renders immediately */}
      <Suspense fallback={<div>Loading stats...</div>}>
        <SlowStats />  {/* Streams in when ready */}
      </Suspense>
      <Suspense fallback={<div>Loading chart...</div>}>
        <SlowChart />  {/* Streams independently */}
      </Suspense>
    </div>
  );
}

// loading.tsx — automatic Suspense boundary for the route segment
// app/dashboard/loading.tsx
export default function Loading() {
  return <div className="skeleton">Loading dashboard...</div>;
}

14. Testing

Jest and React Testing Library

// __tests__/components/Button.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '@/components/Button';

test('renders button with text', () => {
  render(<Button>Click me</Button>);
  expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});

test('calls onClick handler', async () => {
  const handleClick = jest.fn();
  render(<Button onClick={handleClick}>Submit</Button>);

  await userEvent.click(screen.getByRole('button'));
  expect(handleClick).toHaveBeenCalledTimes(1);
});

Playwright for End-to-End Tests

// e2e/navigation.spec.ts
import { test, expect } from '@playwright/test';

test('navigates to blog and loads a post', async ({ page }) => {
  await page.goto('/');
  await page.click('text=Blog');
  await expect(page).toHaveURL('/blog');

  await page.click('article:first-child a');
  await expect(page.locator('h1')).toBeVisible();
});

test('login flow redirects to dashboard', async ({ page }) => {
  await page.goto('/dashboard');
  await expect(page).toHaveURL('/login'); // Middleware redirect

  await page.fill('input[name="email"]', 'user@example.com');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');

  await expect(page).toHaveURL('/dashboard');
});

15. Common Mistakes and Best Practices

Mistake 1: Making Everything a Client Component

// WRONG: Unnecessary 'use client' on a component that doesn't need it
'use client';

export default function Footer() {
  return <footer>Copyright 2026</footer>; // No state, no effects
}

// CORRECT: Leave it as a Server Component (default)
export default function Footer() {
  return <footer>Copyright 2026</footer>;
}

Mistake 2: Fetching Data in Client Components When Server Components Work

// WRONG: Fetching in a Client Component with useEffect
'use client';
import { useState, useEffect } from 'react';

export default function Posts() {
  const [posts, setPosts] = useState([]);
  useEffect(() => {
    fetch('/api/posts').then(r => r.json()).then(setPosts);
  }, []);
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}

// CORRECT: Fetch directly in a Server Component
export default async function Posts() {
  const posts = await db.post.findMany();
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}

Mistake 3: Not Using loading.tsx and error.tsx

Next.js provides special files for loading and error states at the route level. Use them instead of manual Suspense boundaries and error handling in every page:

// app/blog/loading.tsx
export default function BlogLoading() {
  return <div className="animate-pulse">Loading posts...</div>;
}

// app/blog/error.tsx
'use client'; // Error components must be Client Components

export default function BlogError({ error, reset }) {
  return (
    <div>
      <h2>Something went wrong</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

Mistake 4: Ignoring Caching Defaults

Next.js caches aggressively by default. If your data is not updating, check your fetch cache settings:

// This is cached forever by default
const data = await fetch('https://api.example.com/data');

// For data that changes frequently, opt out of caching
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store',
});

// Or set the entire route as dynamic
export const dynamic = 'force-dynamic';

Best Practices Summary

Frequently Asked Questions

What is the difference between the App Router and Pages Router in Next.js?

The App Router (introduced in Next.js 13.4) uses a new directory structure under app/ with React Server Components as the default, nested layouts, and a streaming architecture. The Pages Router uses the older pages/ directory with getServerSideProps, getStaticProps, and client-side rendering by default. The App Router is the recommended approach for new projects, offering better performance through automatic code splitting, parallel route loading, and built-in support for Suspense boundaries.

When should I use 'use client' in Next.js?

Add the 'use client' directive at the top of a component file when it needs browser-only features: useState, useEffect, event handlers (onClick, onChange), browser APIs (localStorage, window), or third-party libraries that use these features. Keep Client Components as small as possible and push them to the leaf nodes of your component tree. Server Components (the default) are more performant because they render on the server and send zero JavaScript to the client.

How does data fetching work in Next.js App Router?

In the App Router, you fetch data directly in Server Components using async/await with the native fetch API or any async function. Next.js extends fetch with automatic request deduplication and caching options. You can control caching with fetch options like cache: 'force-cache' (static) or cache: 'no-store' (dynamic). For mutations, use Server Actions defined with 'use server'. Time-based revalidation is supported via next.revalidate, and on-demand revalidation uses revalidatePath() or revalidateTag().

Can I deploy Next.js without Vercel?

Yes, Next.js can be deployed anywhere that runs Node.js. You can self-host with next start behind a reverse proxy like Nginx, containerize it with Docker, or use platforms like AWS, Google Cloud, Railway, or Fly.io. For static sites, use output: 'export' in next.config.js to generate static HTML that can be served from any CDN or static host. The only features that require Vercel-specific infrastructure are Edge Middleware on Vercel's edge network and some advanced image optimization configurations.

What is ISR (Incremental Static Regeneration) in Next.js?

ISR allows you to create or update static pages after the site is built, without rebuilding the entire application. You set a revalidation interval (e.g., revalidate = 60 for 60 seconds), and Next.js serves the cached page until the interval expires. On the next request after expiry, Next.js regenerates the page in the background while still serving the stale page, then swaps in the updated version. This gives you the performance of static sites with the freshness of server-rendered pages.

How do I handle authentication in Next.js?

The most common approach is Auth.js (formerly NextAuth.js), which provides built-in support for OAuth providers (Google, GitHub, etc.), credentials-based login, JWT and database sessions, and middleware-level route protection. For the App Router, you configure Auth.js with a route handler at app/api/auth/[...nextauth]/route.ts and use middleware.ts to protect routes. You can access the session in Server Components with auth() and in Client Components with useSession().

Related Resources

Keep learning: Next.js combines React, Node.js, and modern web architecture into one framework. Review our React Hooks Guide for frontend patterns, our TypeScript Guide for type safety, and use our HTML to JSX Converter when migrating templates.

Related Resources

React Hooks Complete Guide
Master every React hook with practical examples
TypeScript Complete Guide
Type safety fundamentals for Next.js development
HTML to JSX Converter
Convert HTML templates to valid JSX instantly
React Hooks Cheat Sheet
Quick reference for all React hooks
REST API Design Guide
Design robust APIs for your Next.js backend
Node.js Complete Guide
Understand the runtime powering Next.js