How Next.js Routes Work
Next.js uses a file-based routing system where every page in the pages directory automatically becomes a route based on its file name. Each page is a React component exported as a default export, and supported file extensions include .js, .jsx, .ts, and .tsx.
Routing and rendering are complementary processes. Routing navigates the user to different pages, while rendering puts those pages on the UI. Every time you request a route, you also render that page, though not every render results from a route change. Next.js pre-renders each page in advance alongside the minimal JavaScript needed for hydration. How this happens depends on the form of pre-rendering — Static Generation or Server-side Rendering — both of which are tied to data fetching techniques such as getStaticProps, getStaticPaths, getServerSideProps, or client-side tools like SWR and react-query.
Pages and Custom Pages
A typical Next.js application has top-level directories like pages, public, and styles. Each page inside pages is a React component, and pages may also be referred to as route handlers.
There are also special pages in the pages directory that do not participate in routing. These custom pages are prefixed with an underscore:
_app.js— a custom component Next.js uses to initialize pages._document.js— a custom component that augments the application's<html>and<body>tags, since Next.js pages skip the surrounding document markup.
Linking Between Pages with Link
Next.js exposes a Link component from the next/link API for client-side route transitions between pages. The component can be used inside any component, page or not, and in its basic form it translates to a hyperlink with an href attribute.
Route Patterns
The file-based routing system supports common route patterns, with each route separated based on its definition.
Index Routes
The default route is pages/index.js, which serves as the starting point at /. Index routes automatically act as the default route for each directory. A directory containing index.js and home.js, for instance, exposes two paths: / and /home. This also eliminates naming redundancies, especially with nested routes.
Nested Routes
To create nested routes that go deeper than one level, you need a nested folder structure. For example, to serve /printed-books from a base URL, you could create a pages/printed-books/index.js file, eliminating the redundant path and making the book listing available at the clean base path.
Dynamic Routes
Instead of creating separate route files for each book, you can use a dynamic segment with bracket syntax, such as [book-id].js, to handle all paths like /printed-books/:book-id. The bracket syntax is not limited to files; it can also be used with folders, so you could define a route at /printed-books/:book-id/author with a folder structure containing author.js inside a [book-id] folder.
The dynamic segment is exposed as a query parameter accessible via the query object of the useRouter() hook from next/router.
Catch-All Routes
Deeply nested dynamic routes like /printed-books/:category/:release-year/:book-id quickly become redundant. Catch-all routes solve this by using the same bracket syntax prefixed with three dots, as in [...slug].js. These routes catch all segments of a path. The slug segments are returned as an array of query parameters. To access the category and release year individually, you would either rely on the book's metadata or parse the slug array.
Catch-all routes are strict: if no slug matches, the route throws a 404 error unless you provide a fallback index route. To avoid creating index routes alongside catch-all routes, you can use optional catch-all routes instead.
Optional Catch-All Routes
Optional catch-all routes use double square brackets, as in [[...slug]].js. Here, the slug is optional; when not present, the path falls back to /printed-books and the page renders without any query params. As a rule, use catch-all routes alongside index routes, or optional catch-all routes alone — avoid using both catch-all and optional catch-all routes together.
Route Precedence
With multiple routing patterns, clashes can occur. When appropriate, Next.js raises errors — for example, having more than one dynamic route on the same level. Otherwise, it applies precedence based on route specificity:
- Predefined route handlers are checked first.
- Dynamic route handlers are checked second.
- Catch-all route handlers are checked third.
- If nothing matches, a 404 page is thrown.
As a result, for a path like /printed-books/inclusive-components, the predefined or dynamic route handler takes priority over a catch-all route, and the more specific route wins.
Programmatic Navigation and Route Configuration
Beyond the file-system router and dynamic route patterns, Next.js exposes two additional APIs for client-side navigation plus a configuration layer in next.config.js. The declarative next/link component handles most transitions, while the imperative next/router hook gives you manual control. Each has its own syntax and edge cases worth knowing.
Using next/link for Declarative Transitions
The Link component from next/link renders a standard HTML anchor tag in the browser. For example, <Link href="/">Smashing Magazine</Link> becomes <a href="/">Smashing Magazine</a>. The href prop is the only required attribute; the official docs list the rest.
Dynamic route segments previously required passing both href and as props to Link so Next.js could interpolate parameters. That dual-prop approach was tedious and error-prone. Since Next.js 10, you can use a single href prop with the dynamic path directly. The change is backward compatible; existing code using as still works, but you can simplify by dropping href and renaming as to href.
One caveat: if Link wraps a custom component that returns an anchor tag (for instance, with styled-components), you must pass the passHref prop. Without it, the child component won't receive the generated href. This is also necessary when Link has multiple children, since it expects a single child element.
The href prop also accepts a URL object rather than a plain string. With a query property, Next.js formats it automatically. For example, an object representing printed books will produce paths like:
/printed-books/ethical-design?name=Ethical+Design/printed-books/design-systems?name=Design+Systems
When the pathname contains a dynamic segment, that segment must also appear as a property in the query object so interpolation happens correctly in the path itself. Following that pattern yields clean URLs such as /printed-books/ethical-design without extra query strings.
In TypeScript, inspecting LinkProps shows href typed as Url, which is either a string or a UrlObject. The UrlObject interface exposes properties like pathname, query, hash, and others documented in the Node.js URL module. One practical use for the hash property is linking to a specific section on a page; for instance, a URL object with hash: 'faq' resolves to /printed-books/ethical-design#faq.
Imperative Routing With next/router
When declarative links aren't sufficient, the useRouter hook from next/router offers an imperative alternative. It returns the router object inside any function component. Because it's a React hook, class components must use the withRouter higher-order component instead.
The router object exposes URL-state properties like pathname, query, asPath, and basePath, plus locale information (locale, locales, defaultLocale) when internationalization is configured. It also provides navigation methods: push adds a new entry to the history stack, while replace swaps the current URL without adding a new entry.
Configuring Routes in next.config.js
Route behavior is also tunable through the next.config.js file, a standard Node.js module. Any change there requires a server restart to take effect.
Two route-related settings are worth noting. First, the base path moves your application under a sub-path. Setting basePath: '/dashboard' means your default route becomes /dashboard instead of /, and all internal links and rewrites adapt accordingly. This requires Next.js 9.5 or newer.
Second, trailing slashes are off by default. Enabling trailingSlash: true appends a slash to the end of every URL. Like the base path option, this feature is only available from Next.js 9.5 onward.
Routing is central to any Next.js application given the file-system router and the close relationship between routes and rendering. Understanding the declarative Link component, the imperative router object, and the configuration options in next.config.js covers most client-side navigation needs. For deeper reference, consult the official documentation on Pages, next/link, and next/router.



