cheat sheet

next

Package-level reference for Next.js — App Router, server components, server actions, middleware, caching, and the 14 → 15 → 16 migration path.

next

What it is

next is Vercel's full-stack React framework: routing, server rendering, client hydration, API routes, middleware, image optimisation, font loading, and bundling all in one package. The App Router (default since Next 13) treats every file under app/ as a route, makes server components the default, and folds data fetching into async components directly.

Reach for Next.js when you want React + SSR + routing in a single supported toolkit, and when deployment to Vercel (or a Node-compatible host) is acceptable. Reach for Remix / React Router 7 if your data model is request-shaped, Astro if the site is mostly static, or Vite + React if you only need an SPA.

Install

Next prefers a scaffolded project.

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

Output: interactive prompts (TypeScript, ESLint, Tailwind, App Router, src/ dir, import alias); scaffolds and runs npm install.

For an existing project, install directly:

bash
npm install next react react-dom

Output: added next + the React peers to dependencies

bash
pnpm add next react react-dom

Output: added 3 packages

bash
yarn add next react react-dom

Output: added next, react, react-dom

bash
bun add next react react-dom

Output: installed next, react, react-dom

Versioning & Node support

Next ships a major roughly yearly. As of late 2025, the current line is next@15.x / next@16.x (track nextjs.org/blog for the exact current major).

  • Node 18.17+ minimum; Node 20 LTS recommended.
  • Pairs with react@18.3+ (15.x) and react@19+ (16.x); always pin peers in lockstep.
  • Dual ESM/CJS internally; user code can be ESM or CJS.
  • TypeScript types bundled.
  • Major bumps move quickly — read the release notes before upgrading a production app.

Package metadata

  • Maintainer: Vercel
  • Project home: github.com/vercel/next.js
  • Docs: nextjs.org/docs
  • npm: npmjs.com/package/next
  • License: MIT
  • First released: 2016
  • Downloads: consistently top-20 on npm — millions of weekly downloads.

Peer dependencies & extras

Next declares react and react-dom as peer deps. Common extras:

  • @next/font (now built into next/font) — font self-hosting
  • next-auth / @auth/core — authentication
  • @next/bundle-analyzer — visualise bundle size
  • @next/mdx — MDX support
  • eslint-config-next — Next-tuned ESLint
  • tailwindcss — paired by default in create-next-app
  • @vercel/analytics, @vercel/speed-insights — first-party telemetry
  • drizzle-orm / prisma — common DB layers
  • zod / valibot — input validation for server actions and route handlers

Alternatives

PackageTrade-off
remix / react-router v7+Same React + SSR space. Request/response-shaped data API. Less Vercel-flavoured.
astroStatic-first with islands. Better for content sites.
gatsbyOlder static-first React framework. Largely surpassed by Next App Router and Astro.
redwoodFull-stack opinionated stack (GraphQL by default). Smaller community.
solid-startSolidJS equivalent. Smaller ecosystem, faster runtime.
nuxtVue equivalent.
sveltekitSvelte equivalent.
Vite + React + custom serverMaximum flexibility, maximum operations cost.

Real-world recipes

App Router page + async data fetching

app/page.tsx is the home route. Server components can await directly.

tsx
async function getPosts() {
  const res = await fetch("https://api.example.com/posts", { next: { revalidate: 60 } });
  return res.json() as Promise<{ id: string; title: string }[]>;
}

export default async function HomePage() {
  const posts = await getPosts();
  return (
    <ul>
      {posts.map((p) => <li key={p.id}>{p.title}</li>)}
    </ul>
  );
}

Output: page renders on the server with data already in the HTML; revalidates every 60 seconds via the built-in fetch cache.

Server action with form

Server actions are server-only functions invoked from a <form>.

tsx
// app/posts/new/page.tsx
import { revalidatePath } from "next/cache";

async function createPost(formData: FormData) {
  "use server";
  const title = String(formData.get("title"));
  await db.posts.create({ title });
  revalidatePath("/posts");
}

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">Create</button>
    </form>
  );
}

Output: submits POST to a generated endpoint; on success the /posts page is revalidated and re-fetched.

Middleware for auth gating

middleware.ts runs at the edge before any route handler.

typescript
import { NextRequest, NextResponse } from "next/server";

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

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

Output: unauthenticated requests to /dashboard/* redirect to /login before the route runs.

Route handler (API route)

app/api/posts/route.ts for REST-style endpoints.

typescript
import { NextRequest, NextResponse } from "next/server";

export async function GET() {
  const posts = await db.posts.findMany();
  return NextResponse.json(posts);
}

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

Output: GET /api/posts returns all posts; POST /api/posts creates one.

Dynamic route + generateStaticParams

tsx
// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await db.posts.findMany({ select: { slug: true } });
  return posts.map((p) => ({ slug: p.slug }));
}

export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await db.posts.findUnique({ where: { slug } });
  return <article><h1>{post?.title}</h1></article>;
}

Output: pre-renders each slug at build time; falls back to on-demand rendering for new posts.

Image optimisation

tsx
import Image from "next/image";

export function Hero() {
  return <Image src="/hero.webp" alt="Hero" width={1200} height={600} priority />;
}

Output: Next serves an optimised, responsive image with srcset; priority preloads it.

Streaming with Suspense and loading.tsx

tsx
// app/posts/loading.tsx
export default function Loading() {
  return <p>Loading posts…</p>;
}

// app/posts/page.tsx (slow data)
export default async function PostsPage() {
  const posts = await getPosts();
  return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}

Output: the loading UI streams immediately; the actual page replaces it once data resolves.

Production deployment

Next's deployment story splits along host capabilities:

  • Vercel. First-party host. Zero config — git push deploys. Edge functions, ISR, image optimisation, and analytics integrated.
  • Cloudflare Pages / Workers. Use @cloudflare/next-on-pages or the OpenNext adapter. App Router works; some Node-only APIs need shims.
  • AWS (SST / OpenNext). OpenNext builds a Lambda + CloudFront stack with full App Router and ISR support.
  • Self-hosted Node. next start after next build. Configure a process manager (PM2, systemd) and a reverse proxy (Nginx, Caddy).
  • Standalone output. output: 'standalone' in next.config.js emits a minimal Node bundle for Docker images.
js
// next.config.js
const nextConfig = {
  output: "standalone",
  images: { remotePatterns: [{ protocol: "https", hostname: "cdn.example.com" }] },
};
export default nextConfig;

Output: next build writes .next/standalone/ containing a server.js entry; copy that and .next/static/ into a Docker image.

Performance tuning

Next exposes several caching surfaces; understanding them is the difference between snappy and sluggish.

  • fetch cache. fetch(url, { next: { revalidate: 60 } }) — Next caches the response for 60 seconds. cache: 'no-store' disables.
  • Route segment config. export const revalidate = 3600 at the top of a page.tsx sets a default. export const dynamic = 'force-dynamic' opts the whole segment out of caching.
  • Tag-based revalidation. Tag a fetch with next: { tags: ['posts'] }, then call revalidateTag('posts') from a server action to invalidate.
  • Partial Prerendering (PPR). Mix static and dynamic in one route — the static shell ships immediately, dynamic holes stream in.
  • <Image> and <Script>. Use the built-ins, not raw <img> / <script>. They handle preload, priority hints, and lazy loading.
  • Server components by default. Push "use client" to leaves. Client JS bundle shrinks proportionally.
  • next/font for self-hosted fonts. Eliminates render-blocking external font requests and CLS.
  • @next/bundle-analyzer. Visualise what's bloating client bundles before guessing.

Version migration guide

Next moves quickly. The headline shifts across recent majors:

FromToKey changes
next@13next@14App Router stable; server actions stable ("use server"); Turbopack dev mode; smaller bundles.
next@14next@15params and searchParams are Promises (must await). Fetch defaults moved from cache-by-default to no-cache-by-default. React 19 RC by default. cookies() / headers() are now async.
next@15next@16React 19 GA; turbopack production builds; further server actions hardening; tightened CSP defaults.

14 → 15 migration: async params and searchParams.

Before (14):

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

After (15):

tsx
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  return <h1>{slug}</h1>;
}

Output: compiles; params is now an awaited promise.

14 → 15 migration: async cookies() and headers().

Before (14):

typescript
import { cookies } from "next/headers";
const token = cookies().get("session")?.value;

After (15):

typescript
import { cookies } from "next/headers";
const token = (await cookies()).get("session")?.value;

Output: cookies() returns a promise — await it before chaining.

Checklist:

  1. Run the official codemod: npx @next/codemod@latest upgrade latest.
  2. Manually inspect every params / searchParams access; await them.
  3. Audit fetch calls — 15+ does NOT cache by default. Add next: { revalidate: N } where you want caching.
  4. Update React + React DOM versions per the Next release notes.
  5. Run next build locally before deploying — type errors and config breaks surface here.

Security considerations

  • Server actions are public endpoints. Marking a function "use server" does NOT authenticate it. Validate the session inside every action.
  • x-middleware-subrequest bypass (CVE-2025-29927). A crafted header could skip middleware in older 13/14 versions. Upgrade to a patched release (15.x and recent 14.x patches).
  • revalidate and revalidateTag are unauthenticated by default. Any code path that exposes them to user input lets attackers flush your cache. Gate behind authenticated routes.
  • Open redirect via searchParams.next. A pattern like redirect(searchParams.next) is a classic open-redirect bug. Validate against an allowlist of internal paths.
  • Server component data leak. Anything returned by a server component lands in the client HTML. Filter sensitive fields before returning.
  • CSP. Configure Content-Security-Policy in middleware.ts or next.config.js. Next 16 ships stricter defaults; older versions need manual setup.
  • Image optimisation as SSRF. next/image will fetch arbitrary remote URLs if remotePatterns is too loose. Restrict to known CDNs.

Testing & CI integration

Unit test with Vitest

typescript
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  test: { environment: "jsdom", setupFiles: ["./vitest.setup.ts"] },
});
typescript
// components/Button.test.tsx
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { Button } from "./Button";

describe("Button", () => {
  it("renders", () => {
    render(<Button>Click</Button>);
    expect(screen.getByRole("button")).toHaveTextContent("Click");
  });
});

Output: unit tests run in jsdom; works for client components.

Playwright for E2E

typescript
import { test, expect } from "@playwright/test";

test("home page", async ({ page }) => {
  await page.goto("/");
  await expect(page).toHaveTitle(/My App/);
});

Output: spins up next start against a built app and asserts in a real browser.

CI pipeline

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: "npm" }
      - run: npm ci
      - run: npm run lint
      - run: npm test -- --run
      - run: npm run build

Ecosystem integrations

PackageRole
next-auth / @auth/coreAuthentication adapters
drizzle-orm / prismaDatabase layer
@tanstack/react-queryClient cache for non-server-component data
zod / valibotInput validation for server actions
@vercel/analyticsWeb Vitals reporting
next-intli18n
next-sitemapsitemap.xml + robots.txt generation
mdx-bundler / @next/mdxMDX rendering

Troubleshooting common errors

Error: Dynamic server usage: Page used cookies() etc. — a static page tried to read request data. Mark it export const dynamic = 'force-dynamic' or move the read into a child server component.

Hydration failed because the server rendered HTML didn't match — locale, dates, or random IDs. Pin Intl options; gate variability behind useEffect.

fetch failed in next build — your build tried to fetch a server that isn't running. Either skip the call at build time (process.env.NEXT_PHASE === 'phase-production-build') or expose a static fallback.

Module not found: Can't resolve 'fs' — using a Node-only API in a client component. Move to a server component or route handler.

Error: Route Handlers cannot use cookies() outside of a server component or action — Next 15+ enforces async cookies/headers. Make the handler async and await cookies().

Image with src "..." has invalid src propremotePatterns doesn't cover the host. Add it to next.config.js.

Server actions are not enabled — older configs require experimental.serverActions: true in next.config.js; in current majors they are on by default.

When NOT to use this

  • Pure static site. Astro / Eleventy / Hugo produce smaller, simpler builds for content-heavy sites.
  • Pure SPA. Vite + React + React Router is leaner than Next for SPA-only workloads.
  • Non-Vercel infra with strict constraints. Next runs on Cloudflare, AWS, and self-hosted, but each adapter has rough edges. Confirm your target platform supports the features you need (ISR, middleware, edge runtime).
  • Existing Express / Fastify backend you cannot replace. Mounting Next as middleware works but loses some of its conveniences.
  • Bun-only deployment. Next supports Bun for dev; production support varies. Hono / Elysia are Bun-native alternatives.

See also