What is Next.js?
Next.js is a React framework that turns a UI library into a full application platform. It adds file-based routing, server rendering, data fetching, API endpoints, image and font optimisation, and a server runtime. Instead of choosing and wiring a router, a data layer and a build setup, you get a coherent stack designed to work together.
It is maintained by Vercel and has become the default way to build production React applications. The current model is the App Router, built on React Server Components, which changes where your code runs and how data flows.
The App Router and file-based routing
Routes come from the file system. A folder is a URL segment, and a page.tsx file makes it a route.
app/
├── layout.tsx # root layout, wraps everything
├── page.tsx # /
├── about/page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/page.tsx # /blog/:slug
└── dashboard/
├── layout.tsx # layout for /dashboard/*
└── page.tsx # /dashboard
Dynamic segments use square brackets, and layouts nest so shared UI stays mounted during navigation. Special files such as loading.tsx, error.tsx and not-found.tsx handle the corresponding states without extra code.
// app/blog/[slug]/page.tsx
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}
Server and client components
This is the concept that defines modern Next.js. By default, components are server components: they render on the server, can read data and secrets directly, and add nothing to the browser bundle. Components that need interactivity opt in with "use client".
// app/counter.tsx
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
A server component can import and render a client component, but not the other way around. The rule of thumb: keep components on the server and push "use client" down to the smallest interactive leaf. That keeps the JavaScript you ship to the browser small.
Data fetching
In the App Router you fetch data in server components with the standard fetch function, extended with caching and revalidation options.
// app/posts/page.tsx
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 },
});
if (!res.ok) throw new Error("Failed to load posts");
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return <PostList posts={posts} />;
}
Because the code runs on the server, there is no loading spinner to wire up and no waterfall on the client. You can revalidate by time, on demand with revalidatePath or revalidateTag, or opt out of caching entirely. Fetch requests are also deduplicated within a render.
Route handlers and server actions
For API endpoints, route handlers export functions that receive a web Request and return a Response.
// app/api/posts/route.ts
export async function GET() {
const posts = await db.post.findMany();
return Response.json(posts);
}
export async function POST(request: Request) {
const body = await request.json();
const post = await db.post.create({ data: body });
return Response.json(post, { status: 201 });
}
For mutations from your own UI, server actions are usually simpler. Mark a function with "use server" and call it from a form or an event handler.
// app/posts/new/page.tsx
import { revalidatePath } from "next/cache";
async function createPost(formData: FormData) {
"use server";
await db.post.create({ data: { title: String(formData.get("title")) } });
revalidatePath("/posts");
}
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">Create</button>
</form>
);
}
Server actions handle validation, database writes and cache revalidation together, and the form works before JavaScript loads. That is progressive enhancement without extra work.
Rendering and caching
Next.js supports several rendering strategies in one app:
- Static pages are prerendered at build time.
- Server-rendered pages are generated per request.
- Streaming sends HTML in chunks so slow data does not block the shell.
- Client rendering handles interactive islands after hydration.
Caching has several layers, from the fetch cache to the full route cache. Understanding them is the main learning curve, and the defaults are chosen to be fast. When something feels stale, the answer is usually an explicit revalidation call rather than disabling caching everywhere.
Optimisation built in
Next.js ships components that handle common performance work:
next/imageresizes, lazy-loads and serves modern formats.next/fontself-hosts fonts and removes layout shift.next/scriptcontrols how third-party scripts load.- Turbopack speeds up development and production builds.
These defaults mean a well-built Next.js app is fast without a separate optimisation project.
Best practices
- Keep components on the server and use
"use client"only where needed. - Fetch data in server components rather than in effects.
- Use server actions for mutations and revalidate explicitly.
- Colocate routes, components and data access under
app/. - Add
loading.tsxanderror.tsxfor good UX at route boundaries. - Prefer the framework’s image and font components over manual handling.
- Understand caching before turning it off; reach for targeted revalidation.
Common mistakes
- Sprinkling
"use client"at the top of the tree and losing the server benefits. - Fetching in
useEffectwhen a server component would do. - Forgetting to revalidate after a mutation and showing stale data.
- Assuming the App Router and Pages Router APIs are interchangeable.
- Blocking a route on slow data instead of streaming with Suspense.
- Treating Next.js as only a front-end tool and rebuilding a separate backend.
Where to go next
Next.js is the most complete way to ship React today. Strengthen your React and TypeScript foundations, compare the model with Remix and Astro, and learn the Node.js runtime it deploys to. Then build a small full-stack app with a server component, a route handler and a server action.