Next.js Roadmap

Next.js is a full-stack framework for building web applications with React. It handles routing, rendering on the server, data fetching, caching, image and font optimisation and deployment, so teams can ship fast sites without wiring those pieces by hand. Front-end, full-stack and product engineering roles ask for it often.

This roadmap starts with why a framework exists and ends with production topics such as caching, observability and containers. It follows the App Router, the current model, and explains the Pages Router where you will meet it in older code. Each topic has a short explanation and an exercise. Core topics are expected in interviews, recommended ones are common at work, and optional ones can wait.

By the end you can build, test and deploy a Next.js application and explain choices such as server versus client components and static versus dynamic rendering. Practise in coding practice and track your progress in progress page.

Level
Beginner to advanced
Time
10-14 weeks at about 8 hours a week, less if you already know React well
Scope
61 topics in 10 stages
Before you start
React Roadmap fundamentals: components, props, state and hooks; Modern JavaScript Roadmap including promises and async functions; Basic HTML Roadmap and CSS Roadmap; Helpful: TypeScript Roadmap basics

What you will be able to do

  • Create a Next.js project and structure routes, layouts and loading and error states with the App Router.
  • Choose between static, server and client rendering and explain the trade-offs.
  • Split an application into server and client components and pass data between them safely.
  • Fetch data without waterfalls and change data with server functions and route handlers.
  • Control caching and revalidation, and explain each caching layer.
  • Protect routes with middleware or proxy and keep secrets on the server.
  • Test the application with unit and end-to-end tests.
  • Deploy to a managed platform or a container and monitor it in production.

The roadmap at a glance

  1. Stage 1Why Next.js and First SetupBeginner · 1 week · 6 topics
  2. Stage 2RoutingBeginner · 2 weeks · 10 topics
  3. Stage 3Rendering StrategiesIntermediate · 2 weeks · 8 topics
  4. Stage 4Data Fetching and MutationsIntermediate · 2 weeks · 7 topics
  5. Stage 5Caching and RevalidationAdvanced · 2 weeks · 4 topics
  6. Stage 6API Endpoints, Middleware and RuntimesIntermediate · 1-2 weeks · 5 topics
  7. Stage 7Styling and MediaIntermediate · 2 weeks · 5 topics
  8. Stage 8Metadata, Content and DeliveryIntermediate · 1 week · 5 topics
  9. Stage 9Quality and TestingIntermediate · 1-2 weeks · 3 topics
  10. Stage 10Production, Deployment and ObservabilityAdvanced · 2 weeks · 8 topics

Stage 1 Why Next.js and First Setup

Beginner1 week6 topics

Goal. Explain why a framework sits on top of React, and create, run and navigate a new Next.js project.

Introduction Core

Next.js is a full-stack web framework built on React. It adds routing, rendering on the server, data fetching, caching, image and font handling, and a build system, so you can ship a production site without assembling those parts yourself. Companies use it for marketing sites, shops, dashboards and content platforms.

Modern Next.js is built around the App Router, where components run on the server by default and you opt in to the browser only where you need interactivity. Most tutorials from before 2023 describe the older Pages Router, so check which one an article uses before copying code.

Practice. Skim the file layout of a new project and write one sentence about what each top-level folder is for.

Why Frontend Frameworks? Core

A plain page is HTML, CSS and some scripts. Once an interface has shared state, many views and data from several sources, hand-written DOM updates become hard to keep correct. A frontend framework gives you components, a model for updating the screen when data changes, and conventions that a whole team can follow.

The trade-off is more JavaScript sent to the user and a build step to maintain. Choose a framework when the interface is genuinely interactive. For a few static pages, plain HTML is still a fine answer.

Why React?. React lets you describe the interface as a function of data: given this state, the screen looks like this. When state changes, React works out the smallest set of updates. Components are small, reusable and testable, and the ecosystem of libraries, tooling and developers is very large.

Practice. List three features of a site you use that would be painful to build with manual DOM code.

Why Next.js? Core

Plain React in the browser leaves you to choose a router, a bundler, a data layer and a rendering strategy. Next.js makes those choices and makes them work together. You get file-based routing, rendering on the server for faster first paint and better search visibility, built-in optimisation for images and fonts, and simple deployment.

The cost is coupling to the framework and its fast-moving conventions. Caching rules in particular have changed between major versions, so always read the documentation for the version you run.

Practice. Write down two projects where Next.js is a good fit and one where a plain single-page app or static site would be simpler.

React Frameworks Recommended

Next.js is one of several frameworks built on React. Remix, now merged into React Router, centres on nested routes, loaders and actions that map closely to web standards. Others such as Gatsby (static sites) and Astro (content sites, often mixing in React components) target narrower use cases. Expo and React Native are for mobile.

Interviewers sometimes ask you to compare them. A good answer names the unit of design: Next.js has the largest ecosystem and mixes server and client components, Remix leans on routes and forms, and Astro ships very little JavaScript by default.

Practice. Read the introduction of one alternative framework and note one thing it does differently from Next.js.

JavaScript Basics Core

You need working knowledge of modern JavaScript: const and let, arrow functions, destructuring, spread, modules with import and export, promises and async and await. Server components are async functions, and almost every Next.js feature involves a promise somewhere.

Understand the difference between code that runs on the server and code that runs in the browser. Browser-only objects such as window do not exist on the server, and secrets must never reach client code.

Node.js. Next.js development and builds run on Node.js, and by default your server code runs there too. Install a current long-term-support version, and learn the basics of npm or pnpm, package.json scripts and how environment variables work in a process.

TypeScript. Next.js has first-class TypeScript support. A new project can start with TypeScript, route files such as page.tsx and layout.tsx are typed, and the framework can generate types for route params. Types catch wrong props, missing fields and bad API responses before you ship.

Practice. Write an async function that fetches JSON, handles a failed response, and returns a typed result.

create-next-app Core

create-next-app scaffolds a project and asks about TypeScript, ESLint, Tailwind CSS, the src directory, the App Router and the import alias. Accept the defaults while learning.

npx create-next-app@latest my-app
cd my-app
npm run dev

The dev server supports fast refresh, so edits appear without a full reload. next build creates an optimised production build and next start serves it. Always test with a production build before judging performance, because development mode is slower and behaves differently for caching.

Setting Things Up. Setting things up means understanding what the generated project contains. The app folder holds routes. public holds static files. next.config holds framework settings. package.json lists dependencies and scripts, and tsconfig.json configures TypeScript, including the import alias such as @/.

Practice. Create a project, change the home page text, then run a production build and start it.

Stage 2 Routing

Beginner2 weeks10 topics

Goal. Build nested, dynamic and grouped routes with layouts, loading states and error boundaries.

Types of Routers Core

Next.js has two routers. The App Router uses the app directory, React Server Components, nested layouts and streaming. The Pages Router uses the pages directory with functions such as getServerSideProps and getStaticProps. The two can coexist in one project during a migration, and a route must exist in only one of them.

New work should use the App Router. Know the Pages Router well enough to read and maintain older code.

Pages Router. The Pages Router maps files in pages to URLs. A page exports a React component, and data comes from getStaticProps (build time), getServerSideProps (each request) or client-side fetching. API routes live in pages/api, and _app and _document customise the wrapper and the HTML shell.

Practice. Find which router an existing project uses by looking at its folders.

App Router Core

In the App Router, folders define URL segments and special files define what each segment does. A folder becomes a route when it contains page.tsx. Components in this router are server components unless a file starts with "use client".

app/
  layout.tsx          root layout
  page.tsx            /
  blog/
    page.tsx          /blog
    [slug]/page.tsx   /blog/my-post

The root layout must render the html and body tags. Because layouts and pages are server components, they can read databases and call APIs directly.

Practice. Create a blog route with a list page and a dynamic page for a single post.

Routing Terminology in Next.js Core

A segment is one part of a path. A route is the full path made of segments. A layout wraps a segment and its children and keeps state across navigation. A page is the unique UI of a route. The leaf is the last segment, and the root is the top.

Other terms to know: dynamic segment, catch-all segment, route group, private folder, slot, and the route tree that Next.js builds from all of these. Precise vocabulary makes both documentation and interviews easier.

Practice. Draw the route tree for a shop with products, a cart and an account area.

Next.js Routing Basics Core

Navigation uses the Link component, which prefetches routes in the viewport and updates the page without a full reload. For programmatic navigation in client components use the useRouter hook, and on the server use redirect.

import Link from "next/link";

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

Hooks such as usePathname and useSearchParams read the current location in client components. Use Link for any normal navigation, since a plain anchor tag causes a full page load.

Redirects in Next.js. There are several ways to redirect. Call redirect() in a server component or action, or permanentRedirect() for a permanent one. Declare static rules in next.config with redirects for old URLs. Return a redirect response from middleware or a route handler for logic that depends on the request.

Practice. Build a navigation bar that highlights the current route using usePathname.

Structuring Routes Core

Only page and route files make a folder publicly reachable, so you can colocate components, tests and helpers inside route folders safely. A folder that starts with an underscore is a private folder and is excluded from routing. Parentheses create a route group, which organises files or shares a layout without changing the URL.

app/
  (marketing)/about/page.tsx    /about
  (shop)/cart/page.tsx          /cart
  _components/Header.tsx        not a route

Pick one convention for the team and apply it consistently.

Practice. Use two route groups to give marketing pages and app pages different layouts.

Layouts and Templates Core

A layout wraps its children and persists across navigation between pages beneath it. Its state is kept and it does not re-render when you move between sibling pages, which suits navigation bars and sidebars. Layouts receive a children prop and can be nested.

A template looks similar but creates a fresh instance on every navigation, so state resets and effects run again. Use templates for page-enter animations or per-page logging. A layout cannot read the current path or search parameters directly, so read them in a client component inside it.

Practice. Build a dashboard layout with a sidebar, and a template that logs each page view.

Routing Patterns and Route Matcher Core

Common patterns include dynamic segments written [id], catch-all segments [...slug], optional catch-all [[...slug]], and route groups. A page receives the matched values through params.

// app/blog/[slug]/page.tsx
export default async function Post(
  { params }: { params: Promise<{ slug: string }> }
) {
  const { slug } = await params;
  return <h1>{slug}</h1>;
}

Use generateStaticParams to tell Next.js which values to pre-render at build time. Validate params, because they come from the user.

Route Matcher. A route matcher decides which routes a piece of code applies to. The clearest example is the matcher config of middleware, which takes path patterns with parameters and wildcards, so the code runs only where intended.

Practice. Create a docs section with a catch-all route that renders a breadcrumb from the segments.

Parallel and Intercepting Routes Recommended

Parallel routes render more than one page in the same layout at once, using slots defined by folders that start with @. The layout receives each slot as a prop alongside children. This suits dashboards, where panels load and fail independently, and conditional views such as showing a login slot for guests.

Each slot can have its own loading and error files. A default.tsx file defines what to render when a slot has no match on a full page reload, and missing it is a common cause of 404 errors.

Intercepting Routes. Intercepting routes load another route inside the current layout, for example showing a photo in a modal while keeping the feed behind it. The URL still changes, so the modal is shareable. If the user reloads or opens the link directly, they see the full page instead.

Practice. Build a dashboard with two slots, one of which deliberately throws, and watch the other still render.

Loading and Streaming Core

A loading.tsx file wraps the page in a React Suspense boundary and shows its content while the page fetches data. With streaming, the server sends the shell of the page immediately and fills in slower parts as they resolve, so users see something sooner.

import { Suspense } from "react";

<Suspense fallback={<p>Loading reviews...</p>}>
  <Reviews />
</Suspense>

Place Suspense boundaries around the slow parts, not the whole page, and design fallbacks of a similar size to avoid layout shift.

Practice. Add a fallback around one slow component and measure the difference in time to first content.

Error States Core

An error.tsx file defines an error boundary for a segment. It must be a client component and receives the error and a reset function to retry. not-found.tsx renders when you call notFound() or a route does not exist. global-error.tsx catches errors in the root layout.

Errors thrown in a server component are hidden from the browser in production, with only a digest to correlate with server logs. Show friendly messages, log the details on the server, and never expose stack traces.

Practice. Add an error boundary with a retry button and a custom not-found page.

Stage 3 Rendering Strategies

Intermediate2 weeks8 topics

Goal. Choose between static, server and client rendering for each page and compose server and client components correctly.

Rendering Strategies Core

Rendering is the work of turning components into HTML and a working page. Next.js supports several strategies and lets you mix them per route: pre-rendering at build time, rendering on each request, rendering in the browser, and streaming a mix of these.

The decision depends on how fresh the data must be, whether the page is personalised, how much traffic it gets and whether search engines need to read it. A product page can be static and revalidated, a dashboard can render per request, and a rich editor can be mostly client-side.

Practice. For five pages of a shop, pick a rendering strategy for each and justify it in one line.

Rendering Pages in Next.js Core

In the App Router, a route is rendered on the server into a special payload plus HTML. The HTML shows content straight away. The payload lets React rebuild the component tree in the browser, then hydration attaches event handlers to the existing HTML so it becomes interactive.

Server components never hydrate, because they send no component code to the browser. Only client components and their imports are in the JavaScript bundle. This is the main reason the model reduces bundle size.

Practice. Disable JavaScript in your browser and observe which parts of a page still render.

Client-Side Rendering (CSR) Core

With client-side rendering, the server sends a nearly empty HTML shell and JavaScript builds the page in the browser, usually fetching data after load. It suits highly interactive, private areas where search visibility does not matter.

The weaknesses are a blank first paint, more JavaScript to download and run, and data requests that start only after the code loads, which creates request waterfalls on slow connections.

Client-Side Rendering in Next.js. In Next.js, a file with "use client" at the top marks a boundary: that component and everything it imports run in the browser. Client components are still pre-rendered to HTML on the server for the first load, then hydrated.

Practice. Build a page that fetches in a client component and compare its first paint with a server-rendered version.

Server-Side Rendering (SSR) Core

Server-side rendering builds the HTML for each request, so content is fresh and can depend on cookies, headers or the user. The browser gets meaningful HTML at once, which helps first paint and search engines.

A route becomes dynamic when it reads request-specific data such as cookies(), headers() or search parameters, or when it fetches with caching disabled. The cost is server work on every request and a slower time to first byte than a cached static page, so cache what you can.

Practice. Make a page show the signed-in user from a cookie and confirm it renders per request.

Static Site Generation (SSG) Core

With static generation, pages are rendered once at build time and served as files from a CDN. They are the fastest and cheapest option and scale easily. In the App Router, a route with no dynamic inputs is static by default.

export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((p) => ({ slug: p.slug }));
}

When data changes, use time-based or on-demand revalidation to refresh pages without a full rebuild. A site with tens of thousands of pages may pre-render the popular ones and generate the rest on first request.

Practice. Pre-render a set of blog posts at build time and check the build output marks them as static.

SPA vs SSR Recommended

A single-page app loads one shell and then handles navigation in the browser. Server rendering produces HTML per request. Next.js can behave like both: the first load is rendered on the server, and later navigations are handled client-side with only the new segments fetched.

Compare them on first paint, search visibility, server cost, offline behaviour and complexity. A pure SPA is simpler to host. Server rendering wins when content matters to search or first-load speed matters to users.

Single-Page Application (SPA) Rendering. You can build an SPA with Next.js by rendering a client component at the entry point and exporting the app as static files. All routing then happens in the browser, and data comes from APIs you call from client code.

Practice. Write a table comparing an SPA and an SSR page on five criteria.

Rendering Composition Core

Composition rules decide how server and client components nest. A server component can import and render client components. A client component cannot import a server component, but it can receive one as children or another prop, so you can wrap server content in a client wrapper.

// Server component
export default function Page() {
  return (
    <Modal>
      <ServerContent />   {/* still rendered on the server */}
    </Modal>
  );
}

Props crossing from server to client must be serialisable: no functions, class instances or dates that you expect to keep as objects. Add the server-only package to modules that must never be bundled for the browser.

Practice. Wrap a server-rendered list inside a client accordion by passing it as children.

Static Export Recommended

Setting output: "export" in the configuration makes next build write plain HTML, CSS and JavaScript into an out folder, which you can host anywhere. It supports static routes, client components and client-side fetching.

Features that need a running server do not work: request-time rendering, cookies and headers in server code, route handlers that read the request, middleware, and the default image optimiser. Use it for documentation sites, landing pages or an SPA.

Practice. Export a small site, serve the output folder with any static server, and list which features you had to drop.

Stage 4 Data Fetching and Mutations

Intermediate2 weeks7 topics

Goal. Fetch data on the server and the client without waterfalls, and change data with server functions.

Data Fetching Patterns in Next.js Core

In the App Router the simplest pattern is to make a server component async and fetch inside it. There is no useEffect and no loading state to manage by hand, and credentials stay on the server.

export default async function Products() {
  const res = await fetch(process.env.API_URL + "/products");
  const items = await res.json();
  return <ul>{items.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}

You can also query a database or ORM directly from a server component. Fetch where the data is used and let Suspense handle slow parts, instead of passing data down through many layers.

Practice. Display rows from a database or JSON file in a server component with a typed result.

Fetching Location Recommended

Where you fetch affects speed. Fetching on the server near your database avoids a browser round trip and keeps secrets safe. Fetching in the browser suits data that depends on user interaction, such as live search or polling.

Region matters too: running server code in the same region as the database cuts latency a lot, and a page that makes many sequential calls to a far-away region becomes slow. Decide on the location of compute with that in mind.

Server-Side Data Fetching for Locations. Pages that depend on a visitor's place, such as showing nearby stores, need the location on the server. Platforms often provide country, region or city in request headers, which you can read with headers() and use to choose content or currency.

Practice. Move one client-side fetch to the server and compare the number of network requests.

Client-Side Data Fetching Core

Client components fetch data after render using useEffect or, better, a data library such as SWR or TanStack Query. These handle caching, deduplication, retries, refetch on focus and optimistic updates, so you write less code and get fewer bugs.

"use client";
import useSWR from "swr";

const fetcher = (url: string) => fetch(url).then((r) => r.json());

export function Profile() {
  const { data, error, isLoading } = useSWR("/api/me", fetcher);
  if (error) return <p>Failed</p>;
  return isLoading ? <p>Loading</p> : <p>{data.name}</p>;
}

Use it for personalised or frequently changing data that does not need to be in the first HTML.

Practice. Build a live search box that fetches suggestions from a route handler with SWR.

Parallel vs Sequential Core

Sequential fetching means each request waits for the previous one. It is correct when the second request needs the first result, for example loading a user and then their orders. Otherwise it creates a waterfall and slows the page.

const [user, posts] = await Promise.all([
  getUser(id),
  getPosts(id),
]);

Start independent requests together with Promise.all, or split them into separate components under their own Suspense boundaries so they run in parallel and stream independently.

Practice. Find a sequential pair of fetches in a page and make them parallel, measuring the improvement.

Preloading data Recommended

Preloading starts a fetch early so the result is ready when a component needs it. A pattern is to export a preload function that calls the data function without awaiting it, then call it from a parent before rendering children that will await the same data.

It works well with request memoisation, because the later call reuses the in-flight promise. Use it to remove waterfalls caused by layout and page rendering in sequence, and pair it with server-only so the helper stays on the server.

Practice. Add a preload call in a layout so a child page finds the data already in flight.

Server Functions Core

Server functions, commonly called server actions, are async functions that run on the server and can be called from client code or used as a form action. Mark them with "use server".

"use server";
import { revalidatePath } from "next/cache";

export async function addTodo(formData: FormData) {
  const title = String(formData.get("title") ?? "").trim();
  if (!title) return;
  await db.todo.create({ data: { title } });
  revalidatePath("/todos");
}

Treat every server function like a public endpoint: they are reachable by anyone, so check authentication, authorisation and validate input inside each one. Use them for mutations, not for reading data.

Practice. Create a form that adds an item through a server function and refreshes the list after saving.

Forms and Mutations Core

With a server function as the form action, a form works even before JavaScript loads. Hooks such as useActionState return validation messages from the server, and useFormStatus shows a pending state on the submit button. useOptimistic updates the screen before the server answers.

Validate on the server with a schema library such as Zod and return field errors rather than throwing. After a mutation, revalidate the affected data so users see the result.

Practice. Add server-side validation to a form and show field errors with useActionState.

Stage 5 Caching and Revalidation

Advanced2 weeks4 topics

Goal. Explain Next.js caching layers and control freshness with revalidation.

Memoization in Fetch Core

Request memoisation means identical fetch calls with the same URL and options during one render pass share a single network request. It lets several components ask for the same data without passing it through props, and without duplicate calls.

It lasts only for the duration of one server render and applies to GET requests. It does not apply to database queries or third-party clients that do not use fetch; for those, wrap the function in React's cache.

Practice. Call the same fetch from two components and confirm in your server log that only one request is made.

React Cache function Core

React's cache function memoises any async function for one request. It is the tool for ORM or database calls that fetch memoisation cannot cover.

import { cache } from "react";

export const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } });
});

Two calls with equal arguments during one render run the query once. The cache is cleared between requests, so it never serves stale data to another user. It is different from the data cache, which persists across requests.

Practice. Wrap a database query in cache and call it from a layout and a page.

Caching Data Core

Next.js has several caching layers: request memoisation, a persistent data cache, a full route cache for static output, and a client-side router cache. The defaults have changed across major versions. Older versions cached fetch results by default, while recent ones treat fetches as uncached unless you opt in. Always check the documentation for your version.

await fetch(url, { cache: "force-cache" });             // cache
await fetch(url, { next: { revalidate: 3600 } });   // cache for an hour
await fetch(url, { cache: "no-store" });            // never cache

Newer releases also introduce a "use cache" directive that caches components or functions explicitly. Decide the freshness of each piece of data on purpose.

Practice. List the data on one of your pages and write the maximum acceptable staleness next to each item.

Revalidating Cached Data Core

Revalidation refreshes cached data. Time-based revalidation sets a lifetime, after which the next request triggers a refresh in the background. On-demand revalidation refreshes after an event, using revalidatePath for a route or revalidateTag for everything tagged.

await fetch(url, { next: { tags: ["products"] } });

// in a server function or route handler
revalidateTag("products");

Tags are more precise than paths and let one mutation refresh many pages. A common design is a webhook from a CMS that calls a route handler, which revalidates a tag.

Revalidation Errors. When background revalidation fails, Next.js keeps serving the last good data and tries again on a later request. That is the stale-while-revalidate behaviour and it protects users from a flaky upstream.

Practice. Tag a product fetch and write a route handler that revalidates the tag when called with a secret.

Stage 6 API Endpoints, Middleware and Runtimes

Intermediate1-2 weeks5 topics

Goal. Build route handlers, run logic before requests, and choose the right runtime.

API Endpoints Core

In the App Router you write API endpoints as route handlers in a route.ts file, exporting functions named after HTTP methods. They use the standard Request and Response objects.

// app/api/items/route.ts
import { NextResponse } from "next/server";

export async function GET() {
  return NextResponse.json([{ id: 1, name: "Pen" }]);
}

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

A route.ts cannot sit beside a page.tsx at the same path. Use handlers for webhooks, mobile clients and third-party callers; for your own forms, server functions are usually simpler.

Static vs Dynamic API Endpoints. A handler is static when its output does not depend on the request, so Next.js can compute it at build time. It is dynamic when it reads the request, cookies or headers, uses methods other than GET, or opts out through segment config.

Caching API Endpoints in Next.js. Route handlers using GET can be cached when they do not read request data. Whether a handler is static or dynamic depends on its use of the request, cookies, headers or dynamic configuration, and on the version of Next.js. You can set it with segment options such as dynamic and revalidate.

Practice. Write handlers for listing and creating items, returning correct status codes and validating the body.

Streaming in Next.js API Endpoints Recommended

A route handler can return a ReadableStream in the response body, so the client receives data as it is produced. This suits long responses, server-sent events and chat-style output from a language model.

export async function GET() {
  const stream = new ReadableStream({
    async start(controller) {
      for (let i = 0; i < 3; i++) {
        controller.enqueue(new TextEncoder().encode("tick " + i + "\n"));
        await new Promise((r) => setTimeout(r, 500));
      }
      controller.close();
    },
  });
  return new Response(stream);
}

Check that your hosting platform and any proxy do not buffer the response.

Redirects in Next.js API Endpoints. Handlers can redirect with NextResponse.redirect or the redirect helper. The first needs an absolute URL, which you build from request.url.

Practice. Stream a counter from a handler and read it incrementally in the browser.

Middleware Use Cases Core

Middleware runs before a request reaches a route. Typical uses are redirecting signed-out users, rewriting paths for experiments, setting headers, choosing a locale, blocking bots and basic rate limiting. It sees the request and returns a response, a redirect or a rewrite.

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

export function middleware(req: NextRequest) {
  if (!req.cookies.get("session")) {
    return NextResponse.redirect(new URL("/login", req.url));
  }
}

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

Keep it fast and light. It is not the place for database queries or heavy work, and it must not be your only authorisation check, because data access code should verify permissions itself.

Proxy. In recent Next.js versions the middleware convention has been renamed to proxy. The file is proxy.ts and the exported function is proxy, with the same role: run code before a request completes. The change clarifies that it sits in front of the app like a network proxy, and it runs on the Node.js runtime by default.

Practice. Protect a dashboard section with middleware and also check the session in the data layer.

Setting Headers in Next.js Middleware Recommended

To set headers, create the response, call headers.set, and return it. Use it for security headers, request IDs and CORS rules. You can also forward modified request headers to your route using the request option of NextResponse.next.

const res = NextResponse.next();
res.headers.set("x-request-id", crypto.randomUUID());
res.headers.set("X-Frame-Options", "DENY");
return res;

For static security headers, the headers option in configuration is simpler and avoids running code at all.

Using Cookies in Next.js Middleware. The request exposes req.cookies to read, and the response has res.cookies.set to write. Use secure attributes when setting session or preference cookies: httpOnly, secure, sameSite and a sensible maxAge.

Practice. Add a content-security header and a request ID, then inspect them in the browser network tab.

Runtimes and Types Recommended

Next.js code can run on two runtimes. The Node.js runtime has the full set of Node APIs and the entire npm ecosystem. The Edge runtime is a small, Web-standards environment with fast start and a limited API. You choose per route with export const runtime.

Types of code you run include server components, route handlers, server functions, middleware or proxy, and client code in the browser. Each has different access to cookies, the filesystem and secrets.

Edge Runtime. The edge runtime starts quickly and runs close to users, which helps for light tasks such as redirects, personalisation by header and A/B routing. It has no file system, limited Node modules, and size limits on code.

Practice. Make a table of which runtime each kind of file uses by default in your version.

Stage 7 Styling and Media

Intermediate2 weeks5 topics

Goal. Style an application and optimise fonts, images and scripts.

Global CSS and CSS Modules Core

A file named Button.module.css exports class names that are made unique at build time, so styles apply only to the component that imports them. No naming scheme is needed.

import styles from "./Button.module.css";

export function Button() {
  return <button className={styles.primary}>Save</button>;
}

CSS Modules work in server components and have no runtime cost, which makes them a safe default.

Global CSS. Global CSS is imported once, normally in the root layout, and applies everywhere. Use it for resets, typography, CSS variables for colours and spacing, and rules for the html and body elements.

Practice. Style a card component with a CSS Module and confirm the generated class names.

Tailwind CSS Recommended

Tailwind is a utility-first CSS framework: you compose small classes in markup, such as flex gap-4 p-2. It is a popular choice in the project generator, produces small CSS files and works well with server components.

Strengths are speed and consistency from a shared design scale. Weaknesses are long class lists and the need to extract components for reuse. Learn the responsive prefixes, state variants and how to extend the theme with your design tokens.

Sass in Next.js. Install the sass package and Next.js compiles .scss and .sass files, including module files such as card.module.scss. You get nesting, mixins and variables. Use sassOptions in configuration to add include paths.

CSS in JS. CSS-in-JS libraries such as styled-components and Emotion generate styles at run time inside components. They need a client component and a registry for server rendering, and the runtime cost and complexity of streaming have pushed many teams toward CSS Modules or Tailwind in the App Router.

Practice. Build a responsive card grid using only Tailwind utilities.

Font Optimization Core

The next/font module loads fonts at build time and serves them from your own domain, which removes an external request, avoids layout shift by matching fallback metrics, and respects privacy.

import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"], display: "swap" });

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return <html className={inter.className}><body>{children}</body></html>;
}

Local font files work too through next/font/local. Limit the number of weights and subsets you load.

Practice. Load one font family and confirm there is no layout shift when it appears.

Images Core

The Image component resizes, converts to modern formats, lazy-loads and reserves space so the page does not jump. You supply width and height, or use fill with a sized parent, plus meaningful alt text.

import Image from "next/image";

<Image src="/hero.jpg" alt="Team at work" width={1200} height={600} priority />

Mark the main above-the-fold image with priority. For remote images, allow the host in configuration with remotePatterns. Use the sizes prop so the browser picks a suitably small file.

Videos in Next.js. Small clips can use the native video element with muted, loop and playsInline for background effects. Larger videos should be served from a video host or object storage with streaming formats rather than from your app server.

Static Assets. Files in the public folder are served from the site root, so public/logo.svg is reachable at /logo.svg. Use it for favicons, robots files, fixed images and downloads.

Practice. Replace plain image tags on a page and compare the downloaded sizes.

Third-Party Scripts Recommended

The Script component loads external scripts with a chosen strategy: afterInteractive (default), lazyOnload or beforeInteractive. It makes sure each script loads once, even across navigations.

import Script from "next/script";

<Script src={process.env.NEXT_PUBLIC_WIDGET_URL} strategy="lazyOnload" />

Third-party scripts are a common source of slow pages. Load only what you need, defer it, and measure its impact.

Third Party Libraries. Some libraries use browser APIs or React state and fail when imported into a server component, with an error that says a hook or context is only available in a client component. Wrap such a library in your own file marked "use client" and import that wrapper.

Practice. Load an analytics script with the lazy strategy and compare page speed.

Stage 8 Metadata, Content and Delivery

Intermediate1 week5 topics

Goal. Prepare content for search, other languages and fast delivery.

Metadata Core

The Metadata API sets the title, description, Open Graph and other tags. Export a static metadata object or an async generateMetadata function that can use route params and fetched data.

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await getPost(slug);
  return { title: post.title, description: post.summary };
}

Also provide a sitemap, robots file, canonical URLs and social images. Good metadata improves search results and link previews.

Practice. Generate a unique title and description for each blog post and check the page source.

Markdown and MDX Recommended

Markdown is a simple format for content. MDX adds the ability to import and use React components inside Markdown, which is useful for documentation and blogs. Next.js supports MDX through @next/mdx or by loading files with a library such as next-mdx-remote.

You can map elements such as headings and links to your own components for consistent styling. Treat user-supplied MDX as untrusted, since it can run code.

Practice. Render a Markdown file as a page with your own link and heading components.

Internationalization Recommended

Internationalisation (often written i18n) means supporting several languages and regions. The App Router does not ship a full i18n library; the common approach is a [locale] dynamic segment, middleware that detects the language and redirects, and a library such as next-intl to load messages.

Handle plurals, dates and number formats with the Intl APIs, add hreflang alternates for search engines, and plan for right-to-left languages in your CSS.

Practice. Add English and one other language with locale detection and a language switcher.

Lazy Loading Recommended

Lazy loading delays code until it is needed. Use next/dynamic or React lazy with Suspense for heavy client components, such as editors, maps and charts. Images below the fold are lazy-loaded by default.

import dynamic from "next/dynamic";

const Chart = dynamic(() => import("./Chart"), { ssr: false });

The ssr: false option is for components that need the browser. Do not lazy-load content visible at first paint, since it delays what users came to see.

Package Bundling. Next.js bundles your code with Turbopack in recent versions, with webpack still available. It splits code per route, so users download only what a page needs, and it removes unused exports where it can.

Practice. Lazy-load a heavy component and confirm it appears as a separate chunk in the network tab.

Package Bundling Recommended

Next.js bundles your code with Turbopack in recent versions, with webpack still available. It splits code per route, so users download only what a page needs, and it removes unused exports where it can.

Use the bundle analyser plugin to find large dependencies. Typical fixes: import named functions instead of whole libraries, move heavy work to server components, and replace large packages with lighter ones. Packages in transpilePackages are compiled with your code when they ship untranspiled sources.

Practice. Analyse a production bundle and cut one large dependency from the first-load JavaScript.

Stage 9 Quality and Testing

Intermediate1-2 weeks3 topics

Goal. Lint, format and test a Next.js application at unit and end-to-end level.

ESLint Core

ESLint finds likely bugs and enforces rules as you write. The Next.js configuration adds rules for React hooks, images and links, for example warning when you use a plain anchor for internal navigation or an img tag where Image fits.

Run it in your editor and in continuous integration, and fail the build on errors. Configuration formats have moved to the flat config style, so follow the current documentation when upgrading.

Prettier. Prettier formats code automatically, ending debates about style. Install it, add a small configuration, and use the Prettier plugin for Tailwind to sort classes. Keep ESLint and Prettier from conflicting by disabling ESLint formatting rules.

Practice. Introduce a deliberate hooks mistake and read the lint message.

Jest Recommended

Jest is a test runner with a built-in assertion library and mocks. Next.js provides a configuration helper that wires up SWC, CSS imports and environment variables. Pair it with React Testing Library to test client components by what the user sees.

import { render, screen } from "@testing-library/react";
import { Counter } from "./Counter";

test("shows initial count", () => {
  render(<Counter />);
  expect(screen.getByRole("button")).toHaveTextContent("0");
});

Async server components are not supported by unit test runners, so test their logic as plain functions and cover the whole page with end-to-end tests.

Vitest. Vitest is a fast test runner with a Jest-compatible API and native support for modern module formats and TypeScript. It reuses Vite tooling, starts quickly and has a good watch mode.

Practice. Write a test for a client component that checks a click updates the text.

Playwright Recommended

Playwright drives Chromium, Firefox and WebKit from one API and supports parallel runs, tracing, screenshots and network mocking. It is a common default for end-to-end testing of Next.js apps.

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

test("home page has heading", async ({ page }) => {
  await page.goto("/");
  await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
});

Prefer locators by role and label, which also checks accessibility, rather than CSS selectors.

Cypress. Cypress runs end-to-end tests in a real browser with an interactive runner that shows each step. You write tests that visit pages, click, type and check results, and it can also test components in isolation.

Practice. Write a Playwright test for a form submit and run it in two browsers.

Stage 10 Production, Deployment and Observability

Advanced2 weeks8 topics

Goal. Configure, deploy, monitor and operate a Next.js application.

Environment Variables Core

Next.js loads variables from .env files into process.env on the server. Only variables whose names start with NEXT_PUBLIC_ are inlined into browser code, at build time. Everything else stays on the server.

Because public values are baked in during the build, changing one needs a rebuild. Keep different values for development, test and production, never commit secret files, and validate the required variables at start-up so a missing one fails loudly.

Practice. Add one public and one private variable and confirm which one the browser can see.

Handling Sensitive Data Core

Anything sent to the browser is public, including props passed from server to client components. Keep secrets, tokens and database access in server-only modules, and import server-only to make the build fail if client code pulls them in.

Return only the fields a page needs, using data transfer objects rather than whole database rows. Check authorisation inside every server function and data access function, store sessions in httpOnly cookies, and avoid logging personal data.

Practice. Audit a page for props that include fields the browser should not see.

Preparing for Production Core

Before launch, run next build and read its output for route types and sizes. Check metadata, error pages, redirects, robots rules, security headers and image domains. Confirm caching behaviour on a production build, enable compression, and review third-party scripts.

Measure Core Web Vitals with real-user data where possible. Add error tracking, set up health checks, and run the end-to-end suite against the built app in continuous integration.

Practice. Write a pre-launch checklist and run it against a project of your own.

Deployment Options Core

You can deploy Next.js in several ways. A managed platform built for it, such as Vercel, supports every feature with little setup. Other platforms support it through adapters. You can run it as a Node.js server on a virtual machine, as a Docker container on a container service, or as a static export on object storage and a CDN.

Compare them on feature support, cost, control and operational work. Features such as image optimisation, incremental revalidation and streaming need a runtime and a shared cache when you run more than one instance.

Practice. Deploy the same app to a managed platform and as a container, and note what differs.

Node.js Server Recommended

Running next start on a server starts the production Node.js server and supports all features. Put it behind a reverse proxy such as Nginx or a load balancer for TLS and compression, and use a process manager to restart it on failure.

When you run several instances, they need a shared cache handler so revalidation reaches every instance, and deployments must keep static assets from the previous build available for a time so open pages do not break.

Memory Usage. Server memory use grows with the number of routes, the size of the in-memory cache and leaks in your own code, such as global arrays that grow per request. Watch memory in production and set limits for the container.

Practice. Run the production server on a small virtual machine behind a reverse proxy.

Docker Container Recommended

A container makes the runtime reproducible. Set output: "standalone" to produce a minimal server with only the files and dependencies needed, then use a multi-stage Dockerfile: install, build, and copy the standalone output into a slim runtime image.

FROM node:22-alpine AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
USER node
CMD ["node", "server.js"]

Run as a non-root user and pass runtime configuration as environment variables. See the Docker Roadmap roadmap for more.

Practice. Build a container image, run it locally and check its size.

How to set up a custom server in Next.js Optional

Normally Next.js runs its own server. A custom server lets you start Next.js from your own Node.js code, for example to attach it to an existing Express app or handle non-HTTP protocols like WebSocket upgrades.

It disables some automatic optimisations and does not work on platforms that expect the standard server. Prefer middleware, route handlers or rewrites first, and use a custom server only when nothing else meets the need.

Adapter. An adapter turns the output of a Next.js build into what a hosting platform needs, such as serverless functions, an edge layer and cache storage. Each platform provides its own, and the framework is working toward a stable adapter interface so that features behave the same everywhere.

Practice. Wrap Next.js in a small custom server that adds one request log line.

Analytics Recommended

Measure what real users experience. Next.js can report Core Web Vitals through the useReportWebVitals hook, which you send to your analytics tool. Add page-view tracking in a client component that reacts to path changes, since navigation does not reload the page.

Respect privacy rules: ask for consent where required, avoid personal data and prefer lightweight tools.

OpenTelemetry. OpenTelemetry is a standard for traces, metrics and logs. Next.js emits spans for rendering, fetches and route handlers when you register an instrumentation setup, so you can see where time goes in a request and follow it into your database and other services.

instrumentation. An instrumentation.ts file at the root exports a register function that runs once when the server starts. Use it to initialise monitoring, tracing or other setup before the app handles requests.

Practice. Send web vitals values to a simple endpoint and chart them.

Projects to build

Build these in order. Each one should end up in a public repository with a short README that explains your decisions.

  1. Personal blog with MarkdownBeginner

    Build a blog whose posts are Markdown or MDX files. Generate pages statically, add metadata for each post, a sitemap and a custom not-found page.

    App Router · generateStaticParams · Metadata · Markdown and MDX

  2. Product catalogue with searchBeginner

    Build a catalogue with list and detail pages, a category filter driven by search parameters and optimised images. Add loading skeletons using streaming.

    Dynamic routes · Images · Suspense and streaming · Search parameters

  3. Task manager with server functionsIntermediate

    Create an authenticated task app with a database, forms that use server functions with server-side validation, and optimistic updates. Revalidate the list after each change.

    Server functions · Forms · Revalidation · Authentication

  4. Dashboard with parallel routesIntermediate

    Build a dashboard whose panels load independently using parallel routes and Suspense, with an error boundary per panel and a modal opened through an intercepting route.

    Parallel routes · Intercepting routes · Error states · Streaming

  5. Multilingual shop with webhooksAdvanced

    Build a shop in two languages with locale routing, tag-based caching, and a route handler that receives a webhook from a content system and revalidates the right tags. Add Playwright tests.

    Internationalisation · Caching and revalidation · Route handlers · Playwright

  6. Containerised app with tracingAdvanced

    Package a Next.js application as a standalone Docker image, deploy it, add health checks and OpenTelemetry traces, and write a short note on how you would scale it to several instances.

    Docker · Deployment · OpenTelemetry · Operations

Interview questions

Try to answer each question out loud before you open the answer.

What is the difference between the App Router and the Pages Router?Junior

The Pages Router maps files in pages to routes and uses functions such as getServerSideProps for data. The App Router uses the app directory, nested layouts, server components by default, streaming and special files such as loading and error. Data is fetched in async components. I would use the App Router for new projects and keep the Pages Router only for existing code until it is migrated.

What is a server component and how does it differ from a client component?Junior

A server component renders on the server and sends no component code to the browser, so it can read a database or secrets directly and keeps the bundle small. It cannot use state, effects or browser APIs. A client component, marked with "use client", is also pre-rendered but hydrates in the browser and can be interactive. I keep most of the tree on the server and push client components to the leaves.

Explain SSR, SSG and CSR and when you would use each.Junior

SSG renders at build time and serves files from a CDN, ideal for content that changes rarely. SSR renders on each request, which suits personalised or frequently changing pages. CSR builds the page in the browser, fine for private, highly interactive screens where search visibility does not matter. In Next.js I mix them per route, and revalidation lets static pages stay fresh without a rebuild.

How does hydration work and what causes hydration errors?Mid

The server sends HTML so content appears quickly. React then runs in the browser and attaches event handlers to that existing markup. A hydration error appears when the first client render differs from the server HTML. Typical causes are using Date.now() or Math.random() during render, reading window, invalid HTML nesting, and browser extensions. Fix them by moving such values into effects or marking the component as client-only.

How do layouts differ from templates?Junior

A layout wraps child routes and persists across navigation, so its state and DOM stay. It does not re-render when moving between pages beneath it. A template wraps children too but creates a new instance on each navigation, so state resets and effects run again. I use layouts for shells such as navigation and templates for per-page animations or logging.

How do you fetch data in the App Router and avoid waterfalls?Mid

I make server components async and fetch where the data is used. Independent requests start together with Promise.all or sit in sibling components under separate Suspense boundaries so they stream in parallel. Sequential fetching is only for real dependencies. I can preload data in a parent so a child finds it in flight, and I memoise repeated calls with fetch memoisation or React cache.

What are server functions and what security concerns do they have?Mid

They are async functions marked "use server" that run on the server and can be used as form actions or called from client code. Each one becomes a public endpoint, so I check authentication and authorisation inside it and validate every input with a schema. I never trust arguments from the client, return safe error messages, and use them for mutations rather than data reads.

Describe the caching layers in Next.js.Senior

Request memoisation dedupes identical fetches during one render. The data cache stores fetch results across requests and can be revalidated by time or by tag. The full route cache stores rendered output of static routes. The client router cache keeps visited segments in the browser. Defaults differ by version, so I set freshness explicitly for each piece of data and test on a production build.

How do you revalidate cached content after an update?Mid

For time-based refresh I set a revalidate interval on the fetch or route. For immediate updates I tag fetches and call revalidateTag, or call revalidatePath, from a server function or a route handler triggered by a webhook. Tags are better for content used on many pages. Failed revalidations keep serving the previous data, so I monitor staleness.

When would you use a route handler instead of a server function?Mid

Route handlers suit endpoints called by anything other than my own React forms: webhooks, mobile apps, third parties, streaming and file downloads, where I need control over methods, headers and status codes. Server functions suit mutations triggered from my own UI, because they are typed, work with forms and integrate with revalidation. Data reads in pages should go directly through server components, not through a handler.

What is middleware or proxy good for, and what should you avoid in it?Mid

It runs before a request is handled, so it is good for redirects, rewrites, locale detection, headers and cheap checks such as session presence. I avoid heavy work, slow database calls and treating it as the only authorisation layer, since data access code must verify permissions itself. In recent versions the file convention was renamed to proxy.

What is the difference between the Node.js and Edge runtimes?Mid

The Node.js runtime has the full Node API and npm ecosystem, so database drivers and most libraries work. The Edge runtime is a small Web-standards environment that starts fast and runs close to users but has limited APIs and size. I use edge for light logic like redirects, and Node close to the database for heavier pages.

How does Next.js optimise images and fonts?Junior

The Image component resizes images, serves modern formats, lazy-loads them and reserves space to prevent layout shift. The priority prop preloads the main image. next/font downloads fonts at build time, hosts them on your domain and adjusts fallback metrics. Together they improve Largest Contentful Paint and Cumulative Layout Shift.

How would you handle authentication and keep secrets safe?Senior

I store the session in an httpOnly, secure cookie, check it in a data access layer used by pages and server functions, and use middleware only for quick redirects. Secrets live in server-only modules and environment variables without the public prefix. I return only the fields the UI needs, because props passed to client components are visible in the browser.

How do you test a Next.js application?Mid

I unit-test pure functions and client components with Vitest or Jest and Testing Library. Async server components are hard to unit test, so I keep their logic in plain functions and cover pages with end-to-end tests in Playwright against a production build. I also run ESLint, type checks and an accessibility check in continuous integration.

How would you deploy and scale a Next.js app yourself?Senior

I would build a standalone output into a slim container, run it as a non-root user and put several instances behind a load balancer. Because instances do not share cache by default, I configure a shared cache handler so revalidation works everywhere, serve static assets from a CDN, keep old assets available during deploys, and add health checks, metrics and traces.

A page is slow. How do you investigate?Senior

I reproduce it on a production build and check the route type in the build output. Then I look at the network waterfall for sequential fetches, large images, heavy third-party scripts and big client bundles using the analyser. On the server I use traces to find slow queries and region mismatch with the database. Fixes include parallel fetching, caching, Suspense boundaries, lazy loading and moving code to server components.

Common mistakes

  • Marking whole pages as client components for convenience; keep them on the server and push "use client" to small interactive leaves.
  • Copying Pages Router tutorials into the App Router; check which router and which version an example is for.
  • Fetching in sequence when requests are independent; start them together or split them under Suspense.
  • Assuming caching defaults are the same across versions; set freshness explicitly and test on a production build.
  • Treating server functions as private; they are public endpoints, so authenticate, authorise and validate inside each.
  • Using middleware or proxy as the only access check; verify permissions in the data layer too.
  • Passing secrets or whole database rows as props to client components; send only the fields needed.
  • Using plain anchors and image tags; use Link and Image to get prefetching and optimisation.
  • Judging performance on the dev server; measure a production build.
  • Forgetting default.tsx in parallel route slots, which leads to unexpected 404 errors on reload.

Ready-for-interview checklist

  • I can explain the App Router file conventions and build nested, dynamic and grouped routes.
  • I can explain the difference between server and client components and the composition rules.
  • I can choose between static, dynamic and client rendering for a page and justify it.
  • I can explain hydration and common causes of hydration errors.
  • I can fetch data in parallel and stream slow parts with Suspense.
  • I can write a server function with validation and authorisation checks.
  • I can describe the caching layers and revalidate by tag or path.
  • I can write a route handler and middleware or proxy with a matcher.
  • I can use the Image, Font, Script and Metadata features correctly.
  • I can keep secrets on the server and configure environment variables.
  • I can write unit and end-to-end tests for a Next.js app.
  • I can deploy the app as a container or on a managed platform and explain the trade-offs.

Related roadmaps

FAQ

How long does it take to learn Next.js?

If you know React well, you can build a working App Router project in two to three weeks. Reaching interview level, with caching, data fetching, testing and deployment, usually takes ten to fourteen weeks at around eight hours a week.

Should I learn React before Next.js?

Yes. Next.js assumes you understand components, props, state and hooks. Spend time with the React Roadmap roadmap first, then add routing, rendering and data fetching from here.

Is the Pages Router still worth learning?

Know it well enough to read and migrate older code, because many production apps use it. New projects should start with the App Router, which is where new features arrive.

Next.js or plain React with a bundler?

For a private, highly interactive app with no search needs, plain React is fine. For content sites, shops and anything that benefits from server rendering, built-in routing and optimised assets, Next.js saves a lot of work.

Do I need to host on Vercel?

No. You can run Next.js as a Node.js server, in a container, or as a static export. A managed platform is simplest, but check feature support and caching behaviour on any other host.

Practise while you learn.Solve interview problems in the browser, with hints and a timer.
CodingSQLSystem design
Next Post Previous Post
No Comment
Add Comment
comment url