#What Next.js is and why teams use it
AddedReact is a library for describing UI: you give it components and state, it gives you DOM. It has no opinion on which URL shows which component, where your code runs, how data reaches a component, how files are bundled, or how the app is deployed. A plain React app has to answer every one of those questions with extra tools: a router, a bundler, a data library, an API server, a hosting setup.
Next.js is a React framework that answers those questions for you. It gives you file-system routing (a folder is a URL), server rendering (React Server Components, HTML on the first request, prerendering at build time), data fetching and caching on the server, bundling (Turbopack), API endpoints in the same project (Route Handlers), and one production server (next start) or other deployment targets. You still write React components; Next.js decides when and where each one runs.
A mental model that holds for the whole site: every component in a Next.js app runs in one of three places: at build time, on the server for a request, or in the browser. React alone only has the third. Most interview questions about Next.js ("is this page static?", "why is my console.log not in the browser console?", "why did this stop updating?") are really "which of the three places does this code run in, and when?".
| Job | Plain React (Vite + React Router) | Next.js |
|---|---|---|
| Routing | You declare routes in code | Folders and files under app/ are the routes |
| First HTML | An empty <div id="root">; the browser renders everything | Real HTML from the server or from the build |
| Data | Fetched in the browser after the JS loads (or in a loader) | Fetched on the server, in the component, before the HTML is sent |
| API | A separate server (Express, …) | route.ts files and Server Actions in the same project |
| Bundling | Vite | Turbopack (built in, default since 16) |
| Running in production | Static files on a CDN | A Node.js server (next start), or static export, or a platform adapter |
The example is a tiny shop that uses four of those jobs at once: a route from a file, data read on the server, an interactive button, and an API endpoint.
import { getProducts } from "@/lib/products";
import { AddToCart } from "@/components/add-to-cart";
export default async function Home() {
const products = await getProducts();
return (
<main>
<h1>Shop</h1>
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name}, £{p.price} <AddToCart name={p.name} />
</li>
))}
</ul>
</main>
);
}app/page.tsxis the route/only because of where it sits: apage.tsxdirectly inapp/. There is no router configuration anywhere.app/api/stock/route.tsbecomes/api/stockthe same way (see Project structure and special files).Homeis an async Server Component (the default in the App Router): itawaitsgetProducts()directly, with nouseEffect, no loading state and no API call from the browser. Its code never reaches the browser.- The data doesn't depend on the request, so
next buildrendered/once, at build time, and saved the HTML: that's the○ /row. Every visitor gets the same prerendered file. AddToCartstarts with"use client", so it's a Client Component: it's still rendered to HTML on the server (the button textAdd Desk lamp (0)is in the HTML), and its JavaScript is also sent to the browser so React can attachonClickthere.- The
GEThandler callsnew Date(), a value that must be fresh per request, so the build couldn't prerender it:ƒ /api/stockmeans it runs on every request. A handler that returned a constant would have been○. /_not-foundis the built-in 404 page Next.js always adds (the route table is under "The rest of the app", with the other three files). Together: one file per route, server-side data with no client fetching, interactivity only where you opt in, and the build tells you what it could do ahead of time.
The rest of the app
The data module, the Client Component and the API endpoint, then the route table next build printed for the whole app:
export type Product = { id: string; name: string; price: number };
const products: Product[] = [
{ id: "1", name: "Desk lamp", price: 39 },
{ id: "2", name: "Notebook", price: 6 },
];
// In a real app this is a database query or an API call.
export async function getProducts(): Promise<Product[]> {
return products;
}"use client";
import { useState } from "react";
export function AddToCart({ name }: { name: string }) {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount((c) => c + 1)}>
Add {name} ({count})
</button>
);
}export async function GET() {
return Response.json({ inStock: 12, checkedAt: new Date().toISOString() });
}Route (app)
┌ ○ /
├ ○ /_not-found
└ ƒ /api/stock
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demandI ran this app with next build && next start -p 4501 and fetched the page with curl. The HTML already contains the products and the buttons (the <!-- --> comments are React's text separators):
<main><h1>Shop</h1><ul><li>Desk lamp<!-- -->, £<!-- -->39<!-- --> <button>Add <!-- -->Desk lamp<!-- --> (<!-- -->0<!-- -->)</button></li><li>Notebook<!-- -->, £<!-- -->6<!-- --> <button>Add <!-- -->Notebook<!-- --> (<!-- -->0<!-- -->)</button></li></ul></main>The response was 7,055 bytes with Cache-Control: s-maxage=31536000 (a prerendered page). Besides the markup it carried 7 <script src> tags for JavaScript chunks and 7 inline <script>self.__next_f.push(…)</script> tags: the RSC payload, the serialized Server Component tree that React uses in the browser. Two lines from it:
19:I[5865,["$2","$18"],"AddToCart"]
["$","$L19",null,{"name":"Desk lamp"}]19:I[…,"AddToCart"] is a client reference: "the component called AddToCart, from these JS chunks". ["$","$L19",null,{"name":"Desk lamp"}] is the element that uses it, with the props the server passed. There's no reference to Home or getProducts: Server Components appear only as their output. How a request becomes a page follows this payload all the way through.
In headless Chrome the buttons started at Add Desk lamp (0) and showed Add Desk lamp (2) after two clicks, so the server HTML had been hydrated. Two calls to /api/stock one second apart returned different times, as ƒ predicts:
{"inStock":12,"checkedAt":"2026-10-25T18:16:20.563Z"}
{"inStock":12,"checkedAt":"2026-10-25T18:16:21.582Z"}For contrast, I built the same list as a Vite + React single-page app (Vite 8.3) in the same sandbox. Its whole dist/index.html is this, and the products exist only after a 219.73 kB script runs:
<!doctype html>
<html lang="en">
<head><meta charset="UTF-8" /><title>Shop</title> <script type="module" crossorigin src="/assets/index-BdT6AlK-.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>Teams pick Next.js for:
- Content that must be in the HTML: marketing pages, blogs, shops, docs. Search engines and link previews get real markup, and users see content before any JavaScript runs.
- Fast first loads on slow devices: Server Components send HTML and a small payload instead of the code that produced it, so libraries used only on the server (Markdown parsers, date libraries, database clients) add nothing to the bundle.
- One project for UI and backend-for-frontend: data access, mutations (Server Actions) and small APIs (Route Handlers) live next to the pages that use them.
- Static where possible, dynamic where needed, per component: with Cache Components a single page can be a prerendered shell with live parts streamed in (see Partial Prerendering).
- Conventions: a new engineer knows where routes, layouts, loading states and error boundaries live.
You probably don't need it for:
- An app that lives behind a login and doesn't care about SEO (an internal dashboard, an admin tool) where a Vite SPA with a separate API is simpler to host and reason about.
- Hosting with no server at all.
output: 'export'gives you static files, but Server Actions, request-time rendering, proxy and on-demand revalidation need a running server. - A widget embedded in someone else's page, a browser extension, or React Native.
- A team that doesn't want two runtimes in one codebase. The server/client boundary, the caching model and the version churn (big changes in 13, 15 and 16) are real costs.