Skip to main content

Next.js Guide

This guide takes you from zero to production with Next.js, focusing on the App Router (stable since Next.js 14). It also includes a Pages Router appendix for legacy apps. You’ll learn architecture, routing, data fetching, mutations, server actions, caching, streaming, APIs, middleware, deployment, testing, and more—with practical examples.

Who this is for:

  • React devs who want to build full‑stack apps with modern SSR/SSG, server components, and great DX.
  • Teams migrating from Pages Router, CRA, or other frameworks.

Prerequisites:

  • Strong React fundamentals (components, hooks, suspense).
  • Node.js LTS and a package manager (npm, pnpm, or yarn).
  • Basic web/REST knowledge and Git.

Version note:

  • This guide targets Next.js 14+ with the App Router in /app. Where behavior differs by version, notes are included.

1) Quick Start

  • Create a new app:
npx create-next-app@latest my-app
# or
pnpm create next-app my-app
  • Dev, build, start:
cd my-app
npm run dev # start dev server (hot reload, RSC)
npm run build # production build
npm run start # start production server
npm run lint # lint with next/core-web-vitals
  • Project structure (App Router):
my-app/
app/
layout.tsx
page.tsx
globals.css
(marketing)/
page.tsx
dashboard/
layout.tsx
page.tsx
settings/
page.tsx
loading.tsx
error.tsx
api/
hello/route.ts
public/
favicon.ico
next.config.js
package.json
tsconfig.json
.eslintrc.json

2) App Router Fundamentals

2.1 Server Components vs Client Components

  • In /app, components are Server Components by default (rendered on server, zero client JS).
  • Client Components opt in with "use client" at top of file. They can use stateful hooks, browser APIs, and event handlers.
  • Prefer Server Components for data fetching and heavy logic; use Client Components for interactivity/UI state.

Example:

// app/users/page.tsx (Server Component by default)
import { fetchUsers } from "@/lib/db";

export default async function UsersPage() {
const users = await fetchUsers(); // runs on server; can access DB directly
return (
<ul>
{users.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
// app/users/Filter.tsx (Client Component)
"use client";
import { useState } from "react";

export default function Filter({
onChange,
}: {
onChange: (q: string) => void;
}) {
const [q, setQ] = useState("");
return (
<input
value={q}
placeholder="Filter..."
onChange={(e) => {
setQ(e.target.value);
onChange(e.target.value);
}}
/>
);
}

2.2 File Conventions (App Router)

  • app/layout.tsx: Root layout, wraps the entire app (HTML, body). Can define metadata.
  • app/page.tsx: Route segment’s default page.
  • app/loading.tsx: Suspense fallback for the segment (streaming/loading state).
  • app/error.tsx: Error boundary for the segment.
  • app/not-found.tsx: Handles not found for the segment.
  • app/template.tsx: Similar to layout, but re-renders on navigation.
  • app/(group)/: Route groups for organizing without affecting the URL.
  • app/[param]/: Dynamic segments.
  • app/[...slug]/: Catch-all.
  • app/[[...slug]]/: Optional catch-all.
  • app/api/**/route.ts: Route Handlers for HTTP endpoints.

2.3 Layouts and Pages

// app/layout.tsx
import "./globals.css";
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "My App",
description: "A Next.js app",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
// app/page.tsx
export default function HomePage() {
return <h1>Welcome</h1>;
}

2.4 Loading and Error UI

  • loading.tsx is auto-used as Suspense fallback while children fetch data/stream.
  • error.tsx is a client component boundary for errors. It receives a reset() function.
// app/dashboard/loading.tsx
export default function Loading() {
return <p>Loading dashboard...</p>;
}
// app/dashboard/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<pre>{error.message}</pre>
<button onClick={() => reset()}>Try again</button>
</div>
);
}

2.5 Not Found

// app/blog/[slug]/not-found.tsx
export default function NotFound() {
return <p>Post not found</p>;
}

Throw notFound() from next/navigation in a Server Component/handler to trigger it.


3) Routing

3.1 Basic, Nested, and Dynamic Routes

  • app/about/page.tsx → /about
  • app/blog/[slug]/page.tsx → /blog/my-post
  • app/shop/[...slug]/page.tsx → /shop/a/b/c
// app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import { getPostBySlug } from "@/lib/posts";

export default async function PostPage({
params,
}: {
params: { slug: string };
}) {
const post = await getPostBySlug(params.slug);
if (!post) notFound();
return <article dangerouslySetInnerHTML={{ __html: post.html }} />;
}

3.2 Route Groups

  • Organize routes without affecting URL.
app/
(marketing)/home/page.tsx -> /home
(dashboard)/dashboard/page.tsx -> /dashboard

3.3 Parallel Routes

  • Render multiple independent UI trees simultaneously with named slots.
app/@analytics/page.tsx
app/@feed/page.tsx
app/page.tsx // can include <Slot name="@analytics" /> with children prop
// app/layout.tsx
export default function RootLayout({
children,
analytics,
feed,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
feed: React.ReactNode;
}) {
return (
<html>
<body>
<aside>{analytics}</aside>
<main>{children}</main>
<aside>{feed}</aside>
</body>
</html>
);
}

3.4 Intercepting Routes

  • Temporarily render a different route tree in place (e.g., show a modal on top of current page).
  • Use special segment prefixes:
    • (.)segment → from same level
    • (..)segment → from parent
    • (..)(..)segment → two levels up, etc.

Example: Show a photo modal over a list page without leaving the list context.

3.5 Navigation APIs

  • Link: Prefetches on viewport by default (can disable via prefetch={false}).
  • useRouter, usePathname, useSearchParams in Client Components.
  • In Server Components, you can read params/searchParams from props, but navigation is client-only.
// app/components/Nav.tsx
import Link from "next/link";

export default function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/blog" prefetch={false}>
Blog
</Link>
</nav>
);
}

4) Data Fetching, Caching, and Revalidation

4.1 Fetch in Server Components

  • fetch is enhanced on the server with caching, request deduplication, and revalidation.
  • Defaults to static caching when possible; mark dynamic when needed.
// Static (default) with ISR every 60 seconds:
await fetch("https://api.example.com/posts", { next: { revalidate: 60 } });

// Dynamic (no cache):
await fetch("https://api.example.com/user", { cache: "no-store" });

// Tag-based cache and revalidate by tag:
await fetch("https://api.example.com/posts", { next: { tags: ["posts"] } });

4.2 Route Segment Config

  • export const revalidate = 60; // segment-level ISR
  • export const dynamic = 'force-static' | 'force-dynamic' | 'error' | 'auto';
  • export const fetchCache = 'default-cache' | 'only-cache' | 'force-cache' | 'default-no-store' | 'only-no-store' | 'force-no-store';
  • export const runtime = 'nodejs' | 'edge';
// app/blog/[slug]/page.tsx
export const revalidate = 120; // re-gen every 2 minutes

4.3 Revalidate on Demand

Use in Server Actions or Route Handlers:

import { revalidatePath, revalidateTag } from "next/cache";

// After a mutation:
revalidatePath("/blog"); // path-based
revalidateTag("posts"); // tag-based

4.4 generateStaticParams

  • Pre-generate static routes for dynamic segments (SSG).
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const slugs = await getAllSlugs();
return slugs.map((slug) => ({ slug }));
}

4.5 Streaming with Suspense

  • Combine loading.tsx with streaming data in Server Components.
// app/dashboard/page.tsx
import { Suspense } from "react";
import SlowWidget from "./SlowWidget";

export default function Dashboard() {
return (
<>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading widget...</p>}>
<SlowWidget />
</Suspense>
</>
);
}

5) Server Actions (Mutations Without API Plumbing)

  • Write server functions alongside UI using "use server". They run on the server and can be called from forms (progressive enhancement) or programmatically.
  • Great for form submissions, DB mutations, and cache revalidation without creating API routes.

Basic form example:

// app/todos/actions.ts
"use server";
import { createTodo } from "@/lib/db";
import { revalidatePath } from "next/cache";

export async function addTodo(formData: FormData) {
const title = String(formData.get("title") || "").trim();
if (!title) throw new Error("Title is required");
await createTodo({ title });
revalidatePath("/todos");
}
// app/todos/page.tsx (Server Component)
import { addTodo } from "./actions";

export default function TodosPage() {
return (
<form action={addTodo}>
<input name="title" placeholder="New todo" />
<button type="submit">Add</button>
</form>
);
}

Status and client feedback:

// app/todos/Form.tsx
"use client";
import { useFormStatus } from "react-dom";

export default function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Saving..." : "Save"}</button>;
}

Programmatic calls:

  • Use server actions with useOptimistic/useTransition in Client Components for optimistic UI.

Security notes:

  • Server Actions run on server; avoid trusting client data.
  • Validate and sanitize input.
  • Consider CSRF for non-GET if you expose them outside of progressive enhancement context.

6) Metadata, SEO, and Head

  • export const metadata for static values.
  • export async function generateMetadata({ params, searchParams }) for dynamic values.
// app/blog/[slug]/page.tsx
import type { Metadata } from "next";
import { getPostBySlug } from "@/lib/posts";

export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPostBySlug(params.slug);
if (!post) return { title: "Not found" };
return {
title: post.title,
description: post.excerpt,
openGraph: { title: post.title, description: post.excerpt },
};
}

7) Route Handlers (API Endpoints)

  • Place in app/api/**/route.ts.
  • Export HTTP methods (GET, POST, etc.).
  • Access cookies and headers via next/headers.
// app/api/hello/route.ts
import { NextResponse } from "next/server";

export async function GET() {
return NextResponse.json({ message: "Hello" });
}

export async function POST(req: Request) {
const body = await req.json();
return NextResponse.json({ ok: true, received: body });
}

Cookies/Headers:

import { cookies, headers } from "next/headers";

export async function GET() {
const cookieStore = cookies();
const token = cookieStore.get("token")?.value;
const h = headers();
const ua = h.get("user-agent");
return Response.json({ token, ua });
}

Edge runtime:

export const runtime = "edge"; // opt into Edge for this route

8) Middleware

  • File: middleware.ts at project root.
  • Runs before a request; can rewrite, redirect, or add headers.
  • Lightweight—use for auth gating, locale detection, A/B, etc.
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(req: NextRequest) {
const isLoggedIn = req.cookies.get("session")?.value;
if (!isLoggedIn && req.nextUrl.pathname.startsWith("/dashboard")) {
const url = new URL("/login", req.url);
return NextResponse.redirect(url);
}
return NextResponse.next();
}

export const config = {
matcher: ["/dashboard/:path*"],
};

9) Styling

9.1 Global CSS and CSS Modules

  • app/globals.css for global styles (import in root layout).
  • Use CSS Modules for component-scoped styles: Component.module.css
import styles from "./Card.module.css";
export function Card({ children }) {
return <div className={styles.card}>{children}</div>;
}

9.2 Tailwind CSS

  • During create-next-app, you can select Tailwind.
  • Manual:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
  • Configure content paths and import in globals.css.

9.3 next/font (Google & Local)

import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });

export default function Layout({ children }) {
return <body className={inter.className}>{children}</body>;
}

9.4 Image Optimization

import Image from "next/image";

<Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority />;
  • For external domains, configure next.config.js images.domains or remotePatterns.

10) Environment Variables

  • .env.local for local, .env.production for prod.
  • Access at build-time or runtime on server.
  • For client exposure, prefix with NEXTPUBLIC.

Examples:

# .env.local
DATABASE_URL="postgres://..."
NEXT_PUBLIC_ANALYTICS_ID="abc123"

Use:

const dbUrl = process.env.DATABASE_URL; // server only
const id = process.env.NEXT_PUBLIC_ANALYTICS_ID; // client OK

11) next.config.js Essentials

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
remotePatterns: [{ protocol: "https", hostname: "images.example.com" }],
},
async redirects() {
return [{ source: "/old", destination: "/new", permanent: true }];
},
async rewrites() {
return [{ source: "/blog/:slug", destination: "/api/proxy?slug=:slug" }];
},
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "SAMEORIGIN" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
],
},
];
},
// output: 'standalone', // good for Docker
// experimental: { ... }, // check docs for latest flags
};
module.exports = nextConfig;

12) Authentication

12.1 Auth.js (NextAuth)

  • Mature solution for OAuth, credentials, sessions, and adapters.

Quick setup:

npm install next-auth
app/api/auth/[...nextauth]/route.ts   // route handler

Example:

// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";

const handler = NextAuth({
providers: [
GitHub({
clientId: process.env.GH_ID!,
clientSecret: process.env.GH_SECRET!,
}),
],
callbacks: {
async session({ session, token }) {
session.user.id = token.sub!;
return session;
},
},
});
export { handler as GET, handler as POST };

Client usage:

"use client";
import { signIn, signOut, useSession } from "next-auth/react";

export function AuthButtons() {
const { data: session } = useSession();
return session ? (
<button onClick={() => signOut()}>Sign out</button>
) : (
<button onClick={() => signIn("github")}>Sign in</button>
);
}

Protecting routes:

  • Use middleware for route gating or check session in Server Components using getServerSession.

12.2 Custom Auth

  • Use cookies/JWT in Route Handlers + Middleware.
  • Store hashed passwords in DB; perform checks in Server Actions/handlers.

13) Databases and ORMs

13.1 Prisma + Postgres

npm install prisma @prisma/client
npx prisma init

schema.prisma:

datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Post {
id String @id @default(cuid())
title String
content String?
createdAt DateTime @default(now())
}

Use in Server Components/Actions:

// lib/db.ts
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export const db = prisma;
// app/posts/actions.ts
"use server";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";

export async function createPost(fd: FormData) {
const title = String(fd.get("title") || "");
await db.post.create({ data: { title } });
revalidatePath("/posts");
}

Connection pooling:

  • Use a global prisma instance in dev, or Prisma’s recommended pattern for Next.js.
  • For serverless/Edge, use a compatible DB provider or driver.

14) Forms and Mutations: Actions vs API

  • Server Actions: simplest path for forms and UI mutations; automatic progressive enhancement.
  • API Routes (Route Handlers): explicit HTTP endpoints; good for external consumers, webhooks, or non-form calls.
  • RPC: You can implement server functions and call via Actions or via fetch to Route Handlers.

Validation:

  • Use zod/yup to validate input server-side.
  • Return structured errors and display via useFormState.

15) Performance, Bundling, and Caching

  • Minimize Client Components; keep most logic/data on server for zero-JS on client.
  • Use dynamic import for heavy client widgets:
const Chart = dynamic(() => import("./Chart"), {
ssr: false,
loading: () => <p>Loading...</p>,
});
  • Link prefetch: default prefetch on visible links; disable when necessary.

  • Image optimization: next/image reduces bandwidth and improves LCP.

  • Cache wisely:

    • Static by default in RSC when possible.
    • next: { revalidate } for ISR.
    • cache: 'no-store' for per-request freshness.
    • Tag content and revalidateTag on mutation.
  • Avoid unnecessary cookies/headers in data fetch paths—presence of dynamic request data can force dynamic rendering.


16) Internationalization (i18n)

  • App Router supports i18n via locales in next.config.js and routing.
// next.config.js
module.exports = {
i18n: {
locales: ["en", "fr"],
defaultLocale: "en",
},
};
  • Structure routes like app/[locale]/page.tsx and handle locale param.
  • Use Middleware to rewrite to default locale on root.

17) MDX

  • Use @next/mdx to add MDX pages/components.
npm install @next/mdx @mdx-js/react

next.config.js:

const withMDX = require("@next/mdx")();
module.exports = withMDX({
pageExtensions: ["ts", "tsx", "md", "mdx"],
});

Use MDX in app routes (with RSC constraints—wrap client-only MDX content in "use client" islands).


18) Testing

  • Unit/Component: Jest + React Testing Library.
  • E2E: Playwright or Cypress.

Install:

npm install -D jest @testing-library/react @testing-library/jest-dom ts-jest

Example test:

// __tests__/home.test.tsx
import { render, screen } from "@testing-library/react";
import HomePage from "@/app/page";

test("renders heading", () => {
render(<HomePage />);
expect(screen.getByText(/Welcome/i)).toBeInTheDocument();
});

Playwright:

npm init playwright@latest
npm run test:e2e

19) Linting and Formatting

  • ESLint with next/core-web-vitals:
{
"extends": ["next/core-web-vitals"]
}
  • Prettier for formatting:
npm install -D prettier eslint-config-prettier

20) Deployment

  • Zero-config for Next.js.
  • Edge Functions, Image Optimization, and ISR supported out-of-the-box.

20.2 Self-hosted

  • Build and start:
npm run build
npm run start
  • Standalone output for Docker:
// next.config.js
module.exports = { output: "standalone" };

Dockerfile example:

FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]

21) Observability

  • next build output with React Server Components info.
  • Use logging in server code; integrate APM (Datadog, Sentry) by wrapping Route Handlers and Actions.
  • Web Vitals reporting (custom _app in Pages Router or instrumentation in App Router via instrumentation.ts).

22) Common Recipes

22.1 Blog with SSG and ISR

// app/blog/[slug]/page.tsx
export const revalidate = 300;
export async function generateStaticParams() {
const slugs = await getAllSlugs();
return slugs.map((slug) => ({ slug }));
}
export default async function Page({ params }) {
const post = await getPostBySlug(params.slug);
return <article dangerouslySetInnerHTML={{ __html: post.html }} />;
}

22.2 Search with URL Params

// app/products/page.tsx
export default async function ProductsPage({
searchParams,
}: {
searchParams: { q?: string };
}) {
const q = searchParams.q || "";
const products = await searchProducts(q);
return (
<>
<form>
<input name="q" defaultValue={q} />
<button type="submit">Search</button>
</form>
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</>
);
}

22.3 Protect Dashboard with Middleware + getServerSession

// middleware.ts (matcher: ['/dashboard/:path*'])
// see Middleware section above

// app/dashboard/layout.tsx
import { getServerSession } from "next-auth";
export default async function Layout({ children }) {
const session = await getServerSession();
return <>{children}</>;
}

22.4 File Uploads

  • Use Route Handlers with formData() or a signed upload to object storage (S3, R2).
export async function POST(req: Request) {
const form = await req.formData();
const file = form.get("file") as File;
// process or forward to storage
return Response.json({ size: file.size });
}

23) Pages Router Appendix (Legacy)

  • pages/index.tsx → /
  • pages/about.tsx → /about
  • pages/blog/[slug].tsx → /blog/my-post
  • Data fetching:
    • getStaticProps (SSG)
    • getServerSideProps (SSR)
    • getStaticPaths (for dynamic SSG)
  • API Routes: pages/api/**

Example:

// pages/blog/[slug].tsx
export async function getStaticPaths() {
const slugs = await getAllSlugs();
return {
paths: slugs.map((slug) => ({ params: { slug } })),
fallback: "blocking",
};
}
export async function getStaticProps({ params }) {
const post = await getPostBySlug(params.slug);
if (!post) return { notFound: true };
return { props: { post }, revalidate: 300 };
}
export default function Page({ post }) {
return <article dangerouslySetInnerHTML={{ __html: post.html }} />;
}

Migration:

  • You can have both /pages and /app during incremental migration.
  • Prefer building new surfaces in /app to leverage Server Components and Actions.

24) Troubleshooting and Gotchas

  • Unexpected dynamic rendering: Reading cookies/headers or using request-specific APIs can force dynamic. Remove where not needed.
  • Client component bloat: Move non-interactive parts to server.
  • Server Actions not running: Ensure "use server" and that action is imported from a server file. Actions can only be called from forms or by reference in client via special wiring—don’t call them like regular async functions from client without proper setup.
  • Edge vs Node APIs: Some Node APIs are not available on Edge. Choose runtime accordingly.
  • Image domains: Configure images.domains or remotePatterns for external sources.
  • Environment variables missing: .env*. Only NEXT*PUBLIC** keys are available to client.

25) Learning Path Checklist

  • Build a small app with:

    • App Router layouts/pages, loading/error/not-found
    • Server Components for data fetching
    • One Client Component with interactivity
    • A Route Handler for GET/POST
    • A form that uses a Server Action and revalidatePath
    • Image optimization and next/font
    • Deployed to Vercel
  • Add:

    • Dynamic routes + generateStaticParams
    • Tag-based caching + revalidateTag
    • Middleware-based auth gating
    • Edge Route Handler (for a fast, global endpoint)
    • E2E tests with Playwright


You now have a full mental model and practical toolkit for building production Next.js apps with the App Router. If you want, I can scaffold a sample repo demonstrating these patterns (actions, ISR, middleware, Edge, Prisma) tailored to your stack.