Next.js: The Complete Guide for 2026
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
- Create React App: Client-side only, no SSR, no file-based routing, no API routes. CRA is effectively deprecated in favor of frameworks like Next.js.
- Vite + React: Excellent dev server and build tool, but purely client-side. You must add routing, SSR, and data fetching yourself.
- Remix: Strong server-first approach with nested routing and form handling. Next.js has a larger ecosystem and broader hosting options.
- Astro: Best for content-heavy sites with minimal interactivity. Next.js is better for highly interactive applications.
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
- Default to Server Components. Only add
'use client'when you genuinely need browser APIs or React hooks. - Co-locate data fetching with the component that uses it. Fetch where you render, not in a parent component that passes data down.
- Use route groups to organize code without polluting URLs.
- Leverage parallel routes and intercepting routes for modals and complex UI patterns.
- Add
loading.tsx,error.tsx, andnot-found.tsxto every route segment for a polished user experience. - Use Server Actions for mutations instead of creating API routes for simple form submissions.
- Set
revalidateintentionally on every data fetch — don't rely on defaults you haven't verified. - Use the
Imagecomponent for all images. It prevents layout shift and serves optimized formats automatically.
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
- React Hooks Complete Guide — master the hooks you will use in every Next.js Client Component
- TypeScript Complete Guide — Next.js is built for TypeScript; learn the type system thoroughly
- HTML to JSX Converter — convert HTML snippets to valid JSX for your Next.js components
- React Hooks Cheat Sheet — quick reference for useState, useEffect, useRef, and more
- REST API Design Guide — design the API routes that power your Next.js application
- Node.js Complete Guide — understand the runtime that Next.js is built on
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.