Next.js routing overview
Next.js routing is file-based, which means the folders and files in your project become the URLs of your site, and you rarely write a separate routing configuration at all. If you are used to declaring routes in code, this feels unusual at first, then quickly becomes one of the calmer parts of working in the framework. Create a folder, drop a page file inside it, and that route exists. The structure of your project reads like a map of your site.
This guide is a practical walk through how routing works in the current version of Next.js, which centers on the App Router and the app directory. We cover file conventions, layouts, nested and dynamic routes, route groups, linking and navigation, loading and error states, the split between server and client components, and how metadata fits in. The examples are small on purpose, because the concepts are easier to hold when the code is short. You can always read the official reference at nextjs.org alongside this.
A quick note on scope before we start. There are two routers in the Next.js world: the older Pages Router and the newer App Router. Both still work, but the App Router is where the framework is heading, so this guide focuses there and explains the difference clearly. If you are choosing between Next.js and other approaches in the first place, our Next.js vs React guide gives you that wider decision, and this piece assumes you have already landed on Next.js and want to understand how its routing actually works.
Pages Router vs App Router
Next.js has had two routing systems, and knowing which one you are looking at saves a lot of confusion when you read tutorials online. The older system is the Pages Router, which uses a pages directory. The newer system is the App Router, which uses an app directory and is built around React Server Components. Both can technically run in the same project, but new work generally uses the App Router, and that is what this guide teaches.
The Pages Router was simpler in some ways: a file in pages became a route, data was fetched with special functions the framework called for you, and everything rendered as a fairly traditional page. It served the ecosystem well for years and is still supported. If you inherit an older Next.js project, you may well find it built this way, and there is nothing wrong with maintaining it.
The App Router adds more capability at the cost of a few more concepts. Routes are defined by folders that contain special files, layouts can nest and persist across navigation, components render on the server by default, and loading and error states are expressed as files rather than hand-rolled. It is a richer model, and once the conventions click it is pleasant to work in. The main thing to hold onto is that the two routers express the same idea, file-based routing, with different conventions and different capabilities.
| Aspect | Pages Router | App Router |
|---|---|---|
| Directory | pages/ | app/ |
| Route file | Any file in pages | page file in a folder |
| Shared UI | Custom app wrapper | Nested layout files |
| Default rendering | Client components | Server components |
| Loading states | You build them | loading file convention |
| Status | Supported, older | Current direction |
For the rest of this guide, assume the App Router unless stated otherwise. If your project uses the Pages Router, most of the routing ideas still translate, but the file names and some conventions differ.
The app directory and file conventions
The App Router works through a small set of special file names inside the app directory. Rather than configuring routes, you place these files in folders, and Next.js gives each one a specific job. Learning this handful of names is most of learning the router.
The core files are worth knowing by heart. A page file makes a route publicly reachable and renders its main content. A layout file wraps a page and any nested pages with shared UI, like a header and footer, and it persists across navigation within its section. A loading file provides an instant loading state while a route is preparing. An error file catches errors in its section and shows a fallback. A not-found file renders when something is missing. There are a few more, but these are the ones you will reach for constantly.
Here is a small tree that shows how these fit together for a site with a home page, an about page, and a blog with individual posts:
app/
layout.tsx // wraps every page
page.tsx // the / route
about/
page.tsx // the /about route
blog/
layout.tsx // wraps blog pages
loading.tsx // shown while blog loads
page.tsx // the /blog route
[slug]/
page.tsx // the /blog/:slug route
Notice that only folders with a page file become routes. A folder can exist purely to hold a layout or to group things without adding a URL segment, which we will use later. This is the whole mental model: folders describe structure, and a small set of special files give each folder its behavior. Once that clicks, most routing questions answer themselves.
File-based routing basics
Let us build up from the simplest case. To create a route, you make a folder for the path you want and put a page file inside it that exports a React component. That component is what renders at the route. Here is the home page, which lives at the top of the app directory and serves the root URL:
// app/page.tsx
export default function HomePage() {
return <h1>Welcome</h1>;
}
To add an about page at /about, you create an about folder with its own page file. There is no route table to update and nothing to register. The folder name is the URL segment, and the page file makes it real:
// app/about/page.tsx
export default function AboutPage() {
return <h1>About us</h1>;
}
Nesting works exactly as you would expect. A folder inside a folder adds another segment to the URL. So app/blog/page.tsx serves /blog, and if you wanted a page at /blog/archive you would add app/blog/archive/page.tsx. The depth of your folders is the depth of your URLs. This is what people mean when they say Next.js routing is intuitive: the file system is the source of truth, and there is very little indirection between where a file lives and where its content appears.
Because the structure is a convention rather than one team's invention, a developer who knows Next.js can open your project and understand its routes at a glance. That predictability is a quiet benefit for any business planning to maintain a site over time, and it is one of the reasons teams pick a framework over assembling routing by hand. If you want the broader picture of what a framework gives you beyond routing, our explainer on what a web framework is sets the context.
Layouts and nested layouts
Most sites share elements across many pages: a header, a footer, a navigation bar. In the App Router you express shared UI with a layout file, and its defining feature is that it wraps the page and any nested pages while persisting across navigation within its section. That persistence matters, because it means the shared parts do not re-render and flicker every time a visitor moves between pages inside the same area.
Every App Router project needs a root layout at the top of the app directory, because it defines the outer HTML structure of every page. It receives the current page as a children value and renders it inside the shared shell:
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header>My Site</header>
{children}
<footer>Contact us</footer>
</body>
</html>
);
}
Layouts nest. You can add a layout file inside a subfolder to wrap just that section, and it renders inside the parent layout rather than replacing it. So a blog layout adds a sidebar around every blog page while the root layout still supplies the site header and footer around everything. This nesting is one of the strengths of the App Router, because it lets each section own its own shared UI without repeating markup:
// app/blog/layout.tsx
export default function BlogLayout({ children }) {
return (
<div class="blog">
<aside>Categories</aside>
<main>{children}</main>
</div>
);
}
Think of layouts as a stack that wraps your page from the outside in. The root layout is the outermost wrapper, then each nested layout adds another ring, and the page sits in the center. When a visitor navigates within a section, only the innermost content that changed re-renders, while the shared layouts stay put. That is both faster and calmer for the visitor.
Dynamic routes with [slug]
Real sites have pages that follow a pattern rather than a fixed address: one page per blog post, one per product, one per user. You do not create a folder for each of these by hand. Instead you create a single dynamic route using square brackets in the folder name, and Next.js matches any value in that position and passes it to your page.
To make a page for every blog post at /blog/anything, you create a folder named [slug] inside blog. The word inside the brackets is the name of the value you receive. Your page can then read that value and use it to fetch and show the right post:
// app/blog/[slug]/page.tsx
export default function Post({ params }) {
return <article>Reading post: {params.slug}</article>;
}
Visit /blog/hello-world and params.slug is the string hello-world. Visit /blog/routing-tips and it is routing-tips. One folder handles every post. In practice you use that value to look up the matching content from a database or content system, then render it. For a set of known posts, Next.js can also pre-generate each page at build time so they are served as ready-made static HTML, which is fast and search friendly.
There are a couple of related patterns worth knowing. A folder named with a name in double brackets, meaning a catch-all, matches multiple path segments at once, which is handy for documentation trees or nested categories. And a dynamic segment can sit at any depth, so you can have patterns like a product route under a category route. The core idea stays the same throughout: brackets in a folder name mean "match a value here and hand it to the page." Once you internalize that, dynamic routing stops feeling like magic and starts feeling like plumbing you control.
Route groups and organization
As a project grows, you often want to organize folders without those folders adding segments to the URL. Maybe you want to group all your marketing pages together and all your dashboard pages together, each with its own layout, but you do not want /marketing or /dashboard showing up in the address. Route groups solve exactly this.
A route group is a folder whose name is wrapped in parentheses. Next.js uses it to organize files and to apply a shared layout, but it does not include the folder name in the URL. So a folder named with parentheses around marketing can hold your home and about pages under one layout, while those pages still live at the root of the site rather than under a marketing path:
app/
(marketing)/
layout.tsx // layout for marketing pages
page.tsx // still the / route
about/
page.tsx // still /about
(shop)/
layout.tsx // a different layout
products/
page.tsx // /products
This is purely for your benefit as the person maintaining the project. The visitor sees clean URLs, while you get to group related routes and give each group its own layout, its own loading state, and its own error handling. On a larger site this keeps the app directory readable instead of turning into one long list of folders. Route groups are a small feature, but they are the kind of thing that keeps a growing codebase pleasant to work in rather than tangled.
Use them when you have clusters of pages that share a look or a purpose but should not share a URL prefix. Do not over-apply them; a small site rarely needs any. Like most of the App Router, they are there when the project grows into needing them, and quietly absent when it does not.
Linking and navigation
Moving between pages in Next.js is done with the built-in Link component rather than a plain anchor tag, and the reason is speed. When you use Link, Next.js can move to the new page on the client without a full page reload, and it can prepare the destination ahead of time so navigation feels instant. You import it from the framework and use it much like a normal link:
import Link from "next/link";
export default function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/blog">Blog</Link>
<Link href="/about">About</Link>
</nav>
);
}
Under the hood, when a Link becomes visible, Next.js can quietly fetch what the destination needs so that a tap or click resolves right away. This is a big part of why a well-built Next.js site feels quick to move around: much of the next page is already prepared before you ask for it. You still use a normal anchor tag for external links to other websites, since those leave your app entirely.
For navigation triggered in code rather than by a click, such as sending a user to a new page after they submit a form, Next.js provides a router you call from a client component. You get it from a framework hook and call a method to push the new address. This covers the cases where a visible link does not fit, like redirecting after an action completes. The key point for good performance is to prefer Link for anything a user clicks, so the framework can do its preparation, and reserve programmatic navigation for genuine in-code transitions.
Fast navigation is closely tied to how a site scores on the metrics search engines watch, so it is worth getting right. Our guide on Core Web Vitals explains what those measurements actually are, and our piece on how to improve website speed covers the wider set of techniques that keep a site quick.
Loading and error UI
Two of the nicer conveniences in the App Router are built-in loading and error states, expressed as files. Instead of hand-writing the logic to show a spinner while data loads or a message when something breaks, you add a specially named file to a section and Next.js wires it up for you.
A loading file in a folder is shown automatically while that route is preparing, for example while server-side data is being fetched. The visitor sees your loading UI instantly, then the real content replaces it when it is ready. This gives you a smooth, immediate response to navigation without extra plumbing:
// app/blog/loading.tsx
export default function Loading() {
return <p>Loading posts...</p>;
}
An error file catches errors that happen while rendering its section and shows a fallback instead of a broken page. It runs as a client component and can offer a way to try again, so a single failing section does not take down the whole site. You scope error handling by placing these files at the level you want them, so a problem in the blog does not blank out the rest of the app:
// app/blog/error.tsx
"use client";
export default function Error({ reset }) {
return (
<div>
<p>Something went wrong loading the blog.</p>
<button onClick={reset}>Try again</button>
</div>
);
}
There is also a not-found file for the case where a page or a piece of content does not exist, so a visitor who lands on a missing post gets a proper message rather than an empty screen. Together these conventions mean the states that every real site needs, loading, error, and missing, are first-class parts of the router instead of things each team reinvents. That consistency is part of what makes a Next.js codebase quick to pick up.
Server and client components
The App Router is built on React Server Components, and while a full treatment is beyond a routing guide, you need the high-level picture because it shapes how you write pages. By default, components in the app directory are server components. They render on the server, can fetch data directly, and send no JavaScript for themselves to the browser. That keeps pages light and is a big reason App Router sites can be fast.
Some things can only happen in the browser: state that changes as the user interacts, event handlers, and browser-only features. For those you opt into a client component by adding a directive at the very top of the file. That marks the component and its logic to run in the browser, where interactivity lives:
"use client";
import { useState } from "react";
export default function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>Clicked {n}</button>;
}
The practical pattern is to keep most of your page as server components, which fetch data and render content, and to push interactivity down into small client components at the edges. A page might be a server component that loads a post and renders it, with a little client component for a like button or a comment form. This keeps the amount of JavaScript sent to the browser small while still giving you rich interactivity exactly where you need it.
For routing specifically, the thing to remember is that layouts and pages are server components by default, which is why they can fetch data so directly, and that you reach for "use client" only where interaction requires it. You can read more about the model at react.dev. Getting this split right is one of the differences between a Next.js site that feels quick and one that ships more JavaScript than it needs.
Metadata and SEO
Routing and search visibility are closely linked in Next.js, because the App Router gives you a clean, per-route way to set the page title, description, and other metadata that search engines and social platforms read. You do not manage the document head by hand. Instead you export a metadata value from a layout or a page, and Next.js renders the right tags for you.
The simplest form is a static metadata object exported from a page. It sets the title and description for that route, and because Next.js can render this on the server, the tags are present in the initial HTML that search engines receive:
// app/about/page.tsx
export const metadata = {
title: "About Us",
description: "Who we are and what we build.",
};
export default function AboutPage() {
return <h1>About us</h1>;
}
For dynamic routes, where the title depends on the specific post or product, you export a function that generates metadata from the route's value. That lets each blog post carry its own title and description based on the content it loads, which matters a great deal for how individual pages appear in search results. Next.js also has file conventions for sitemaps, icons, and social share images, so the whole set of search and sharing details is handled within the routing system rather than bolted on.
Because metadata renders on the server alongside the page, search engines receive complete, described pages immediately, which is one of the core reasons Next.js is strong for SEO. If you want to understand how this fits into choosing Next.js in the first place, our Next.js vs React comparison covers the rendering and SEO trade-offs, and our Vite vs Next.js guide compares it against a browser-only build tool. You can also read the framework's own metadata reference on nextjs.org.
Data fetching in routes
Routing and data fetching meet in the page and layout files, because in the App Router a server component can fetch its own data directly before it renders. There is no separate data function the framework calls for you as there was in the older router. You simply request the data inside the component, and Next.js renders the finished result on the server:
// app/blog/page.tsx
export default async function BlogPage() {
const res = await fetch("https://example.com/api/posts");
const posts = await res.json();
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
Because this runs on the server, the visitor receives a page that already contains the posts, with no loading flash and no extra round trip from their browser. Next.js also lets you control how data is cached and how often it is refreshed, so a page can be built once and reused, rebuilt on a schedule, or rendered fresh on every request, depending on how live the data needs to be. This is the same rendering flexibility that makes the framework strong for public pages, applied right at the route level.
The loading file we saw earlier pairs naturally with this: while a server component fetches its data, the loading UI shows instantly, then the finished page replaces it. When you need data in an interactive client component instead, you fetch it in the browser as you normally would in React. The general guidance is to fetch on the server in your pages and layouts wherever you can, so that pages arrive complete, and to reserve browser fetching for interactive pieces that genuinely need it. For background on how the browser handles requests in general, the MDN Fetch documentation is a solid reference.
Common routing mistakes
A few predictable errors trip people up when they start with the App Router. Knowing them ahead of time saves a lot of confused debugging.
Forgetting the page file
A folder does not become a route until it contains a page file. Beginners often create a folder and a component and wonder why the URL returns nothing. The folder describes the path, but the page file is what makes the route reachable. Layouts, loading files, and other conventions do not create routes on their own.
Overusing client components
Adding the client directive to the top of nearly every file throws away much of the benefit of the App Router, because it ships more JavaScript and gives up server rendering for those components. Keep pages and layouts as server components, and mark only the small interactive pieces as client components. This split is the single most common thing teams get wrong at first.
Using anchor tags instead of Link for internal navigation
Plain anchor tags for internal links trigger full page reloads and skip the preparation Next.js does for fast navigation. Use the Link component for internal routes so movement stays quick, and reserve plain anchors for links that leave your site.
Mismatching folder names and links
Because routes come from folder names, a typo in a folder or a link quietly produces a missing page. When a link leads nowhere, check that the folder path and the href match exactly, including dynamic segments. This is usually the culprit behind an unexpected not-found page.
Ignoring metadata
Skipping the metadata export leaves pages with generic or missing titles and descriptions, which weakens how they appear in search results. Since the App Router makes per-route metadata straightforward, there is little reason to leave it out, and doing it well is part of getting the search benefit Next.js offers.
Final thoughts
Next.js routing comes down to a simple idea expressed through a small set of file conventions: folders describe your URLs, a page file makes a route real, a layout file shares UI across a section, and brackets in a folder name capture dynamic values. Around that core, the App Router adds route groups for organization, built-in loading and error states, a fast Link component for navigation, and per-route metadata for search. Server components render your pages on the server by default, and you opt into client interactivity only where you need it.
Once these conventions click, routing becomes one of the calmer parts of building with Next.js, because the file system tells you exactly where everything lives. That predictability is good for speed, good for search, and good for any team that has to maintain the site over time. If you are weighing Next.js against other approaches before you commit, our Next.js vs React and single-page application guides give you that wider view.
If you would rather have an experienced team build your Next.js site with routing, rendering, and SEO handled properly from the first commit, that is exactly what we do. You can see the range of what we take on across our services, and when you are ready you can request a free quote or get in touch to talk through your specific project. We are glad to give you a straight recommendation, even when the simpler path is the right one.