next
Next.js is Vercel's full-stack React framework. It gives React apps the things React itself does not: file-based routing (the App Router), server-side rendering, static generation with incremental revalidation, React Server Components, server actions for mutations, API route handlers, and built-in image and font optimization. Since v16 the Rust-based Turbopack bundler is the default. In practice it is the mainstream way to ship a React site that needs SEO or server rendering.
The safe default for React apps that need server rendering, with the largest community and hosting story in the ecosystem. Budget real time for major-version migrations and think twice if your app is a dashboard that never needed SSR in the first place.
Use it if
- You are building a public-facing React site where SEO and first-paint speed matter (marketing, e-commerce, content) and you want SSR/SSG without wiring it yourself
- You want one repo where pages, API endpoints and data mutations (server actions) live together with React Server Components cutting client JS
- You deploy on Vercel and want ISR, image optimization and edge rendering to work with zero configuration
- Your team already knows React and you need conventions instead of assembling a router, bundler and SSR layer from parts
- You are building an internal SPA or dashboard behind a login: Vite plus react-router is simpler, builds faster, and none of the server rendering machinery earns its complexity there
- You cannot absorb framework churn: majors 13 through 16 changed the router model, made params async, flipped fetch caching defaults, and renamed middleware to proxy; upgrades are real migration work
- You self-host and expect everything to just work: features like ISR and image optimization are tuned for Vercel first, and replicating them on plain Node or containers takes effort and reading
- Your backend is substantial (queues, websockets, long-running jobs); Next's serverless-shaped request model fights you and a separate API service is cleaner
Setup reality
npx create-next-app@latest scaffolds a working TypeScript app in a minute, and Turbopack makes dev startup genuinely fast. The pain arrives later: the caching model (what is static, what revalidates, what is dynamic) has changed defaults across majors and still confuses experienced teams; params and searchParams are now Promises you must await; middleware.ts was renamed proxy.ts in v16; some webpack-era plugins and libraries with deep bundler assumptions do not work under Turbopack; and self-hosting means understanding output standalone, a Node server, and giving up or rebuilding parts of image optimization.
Patterns
Fetch data in a Server Component pageserver-page
// app/posts/page.tsx (Server Component by default)
export default async function PostsPage() {
const res = await fetch("https://api.example.com/posts");
const posts = await res.json();
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}Server Components can be async and never ship this code to the browser; fetch here is NOT cached by default since v15.
Dynamic segment with async paramsdynamic-route
// app/posts/[slug]/page.tsx
export default async function Post({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <h1>{slug}</h1>;
}params and searchParams are Promises since v15; forgetting the await is the most common upgrade break.
JSON API endpointroute-handler
// app/api/hello/route.ts
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ ok: true });
}
export async function POST(req: Request) {
const body = await req.json();
return NextResponse.json({ received: body }, { status: 201 });
}Handlers use Web-standard Request/Response; a route.ts cannot live in the same folder as a page.tsx.
Mutate data with a server actionserver-action
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createTodo(formData: FormData) {
await db.todo.create({ data: { title: formData.get("title") as string } });
revalidatePath("/todos");
}
// in a component: <form action={createTodo}>...</form>Actions run only on the server but are exposed as POST endpoints; validate input like any public API.
Interactive component with stateclient-component
"use client";
import { useState } from "react";
export function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}The "use client" directive marks the boundary; everything it imports gets bundled for the browser, so keep these leaves small.
Pre-render dynamic pages at build timestatic-generation
// app/posts/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch("https://api.example.com/posts").then((r) => r.json());
return posts.map((p) => ({ slug: p.slug }));
}Unlisted slugs render on demand unless you export dynamicParams = false.
Cache a fetch and revalidate on a timer or tagrevalidation
const res = await fetch("https://api.example.com/prices", {
next: { revalidate: 60, tags: ["prices"] },
});
// elsewhere, after a mutation:
import { revalidateTag } from "next/cache";
revalidateTag("prices");Opt in to caching explicitly; time-based and tag-based revalidation compose, and tags are the tool for on-demand invalidation.
Per-page metadatametadata-seo
// static
export const metadata = { title: "Pricing", description: "Plans" };
// dynamic
export async function generateMetadata({ params }) {
const { slug } = await params;
const post = await getPost(slug);
return { title: post.title, openGraph: { images: [post.image] } };
}Only Server Components can export metadata; client pages need a server wrapper.
Optimized imagesimage-optimization
import Image from "next/image";
<Image src="/hero.png" alt="Hero" width={1200} height={630} priority />Remote images require allowing the host in images.remotePatterns in next.config; width/height (or fill) are mandatory to prevent layout shift.
Redirect and guard requests (proxy.ts)proxy-middleware
// proxy.ts (root, replaces middleware.ts in v16)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function proxy(req: NextRequest) {
if (!req.cookies.get("session") && req.nextUrl.pathname.startsWith("/app")) {
return NextResponse.redirect(new URL("/login", req.url));
}
return NextResponse.next();
}
export const config = { matcher: ["/app/:path*"] };v16 renamed middleware to proxy to signal it is for routing concerns, not auth logic; do real session checks in the page or layout too.
Shared layout with nested routeslayouts
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<section>
<nav>...</nav>
{children}
</section>
);
}Layouts persist across navigation and do not re-render; state you want reset per page belongs in the page, not the layout.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| react-router | npm | SPA or app behind auth where you do not need SSR; also its framework mode covers SSR with less magic than Next |
| astro | npm | Content-heavy sites where most pages are static; ships near-zero JS by default and lets you drop in React islands |
| nuxt | npm | Your team prefers Vue; it is the closest equivalent feature-for-feature |