Exploring Next.js advanced routing and beyond
Source: Dev.to
Routing in Next.js
Routing is the backbone of any modern web application. Get it right, and users navigate fluidly through your content. Get it wrong, and you’re fighting sluggish transitions, tangled layouts, and brittle URL structures.
Next.js offers one of the most flexible routing systems in the React ecosystem — but taking full advantage of it requires understanding the patterns that go beyond a basic pages/index.js.
What We’ll Cover
- Dynamic routing – single files that handle a whole class of URLs
- Nested layouts – composable UI shells with the app directory
- Advanced navigation patterns –
next/link, shallow routing, programmatic navigation - Middleware & API routes – tying everything together
Dynamic Routing (Pages Directory)
Note on router versions – The examples below use the pages directory and
next/router.
If you’re using the app directory (Next.js 13+), replaceuseRouterfromnext/routerwithuseParamsfromnext/navigation.
Single‑segment dynamic route
// pages/blog/[slug].js
import { useRouter } from 'next/router';
const BlogPost = () => {
const { slug } = useRouter().query;
return
## Blog Post: {slug}
;
};
export default BlogPost;
Matches any URL like /blog/my-first-post and passes the matched segment as slug.
Nested dynamic segments
// pages/blog/[category]/[slug].js
import { useRouter } from 'next/router';
const BlogCategoryPost = () => {
const { category, slug } = useRouter().query;
return (
## Category: {category}
## Post: {slug}
);
};
export default BlogCategoryPost;
Supports URLs such as /blog/tech/nextjs-advanced-routing.
Catch‑all routes (arbitrary depth)
// pages/docs/[...slug].js
import { useRouter } from 'next/router';
const DocsPage = () => {
const { slug } = useRouter().query; // slug is an array
// Example: ['guide', 'installation', 'windows']
return
## Docs: {slug?.join(' / ')}
;
};
export default DocsPage;
- Use
[[...slug]].js(double brackets) to also match the root path/docs.
Client‑Side Navigation with next/link
import Link from 'next/link';
const BlogList = ({ posts }) => (
{posts.map((post) => (
{post.title}
))}
);
next/link automatically prefetches linked pages, making subsequent loads feel instant.
Shallow routing (URL change without full navigation)
import { useRouter } from 'next/router';
router.push('/blog?page=2', undefined, { shallow: true });
Useful for filtering, pagination, or any UI state that shouldn’t trigger a full page transition.
API Routes – Dynamic Segments Work the Same Way
// pages/api/blog/[slug].js
export default function handler(req, res) {
const { slug } = req.query;
res.status(200).json({ message: `Data for blog post: ${slug}` });
}
Accessible at /api/blog/my-first-post.
Layout Composition with the App Directory (Next.js 13+)
The app directory makes layout composition a first‑class feature. Layout files live alongside the routes they govern and wrap their subtree automatically.
Root layout
// app/layout.js
export default function RootLayout({ children }) {
return (
My App Header
{children}
My App Footer
);
}
Section‑specific layout
// app/blog/layout.js
export default function BlogLayout({ children }) {
return (
Blog Sidebar
{children}
);
}
When a user visits /blog/my-post, Next.js renders:
- Root layout → header & footer
- Blog layout → sidebar & main content
- Page component → the post itself
Only the parts that actually change are re‑rendered, reducing unnecessary work and improving performance.
Sharing State Across Layout Boundaries
Simple React Context (client component)
// components/ThemeProvider.js
'use client';
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext();
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
return (
{children}
);
};
export const useTheme = () => useContext(ThemeContext);
Important – The
'use client'directive is required for any component that uses hooks or browser APIs inside the app directory.
When Context Becomes a Bottleneck
Frequent updates can cause re‑renders deep in the tree. In those cases consider a more granular state library such as Zustand or Redux.
Page Metadata without “
// app/blog/layout.js
export const metadata = {
title: 'Blog — My App',
description: 'Read the latest posts on our blog.',
};
Next.js merges metadata from nested layouts and pages automatically; child values override parents.
Programmatic Navigation (useRouter)
import { useRouter } from 'next/router';
const NavigateButton = () => {
const router = useRouter();
return (
router.push('/dashboard')}>
Go to Dashboard
);
};
router.replace– replaces the current history entry (useful for redirects after a form submission).- Scroll behavior – By default Next.js scrolls to the top on each navigation. To restore a user’s previous scroll position, you can implement custom scroll handling or use the built‑in
scrolloption inrouter.push.
TL;DR Checklist
- ✅ Use
[slug].jsfor single‑segment dynamic routes. - ✅ Nest brackets (
[category]/[slug]) for hierarchical URLs. - ✅ Use catch‑all (
[...slug]/[[...slug]]) for deep nesting. - ✅ Prefer
next/linkfor client‑side navigation; enable shallow routing when you only need to update the URL. - ✅ Mirror dynamic segments in API routes for consistency.
- ✅ Leverage the app directory’s layout files for automatic UI composition.
- ✅ Keep shared UI state in React Context (or a dedicated state library for larger apps).
- ✅ Declare page metadata via the
metadataexport. - ✅ Use
useRouter(oruseParams/useRouterfromnext/navigationin the app directory) for programmatic navigation.
With these patterns in place, your Next.js app will have a robust, maintainable routing foundation that scales alongside your features. Happy coding!
Preserve Scroll Position on Navigation
Enable scroll restoration in next.config.js:
// next.config.js
module.exports = {
experimental: {
scrollRestoration: true,
},
};
This is especially valuable in content‑heavy applications—news feeds, search results, long‑form article lists—where losing scroll position forces users to re‑find their place.
Prefetch Routes Selectively
next/link automatically prefetches in‑viewport links.
For routes the user is likely to visit next but that aren’t yet on screen, you can trigger prefetching programmatically:
// pages/index.tsx (or .js)
import { useEffect } from 'react';
import Router from 'next/router';
const HomePage = () => {
useEffect(() => {
Router.prefetch('/dashboard');
}, []);
return
## Welcome
;
};
export default HomePage;
Tip:
Prefetching every possible route increases memory usage and can degrade overall performance. Focus on routes that appear immediately after the user’s current step—e.g., the next page in an onboarding flow or the most commonly clicked item in a navigation menu.
Middleware for Authentication & Feature Flags
Middleware runs before a request reaches its route handler, making it the right place to enforce authentication, role‑based access, or feature flags without touching individual page components.
// middleware.js
export function middleware(req) {
const token = req.cookies.get('auth-token');
if (!token) {
return Response.redirect(new URL('/login', req.url));
}
// Additional checks (roles, feature flags) can go here
}
export const config = {
matcher: ['/dashboard/:path*'], // limit middleware to specific paths
};
The matcher config ensures the middleware only runs on the specified paths, avoiding unnecessary execution on every request.
Recommended Project Structure
As your routing logic grows, keeping the project organized becomes critical. Below is a structure that accommodates all the patterns discussed:
nextjs-advanced-routing/
├── app/
│ ├── layout.js # Root layout
│ └── blog/
│ ├── layout.js # Blog‑specific layout
│ └── page.js
├── pages/
│ ├── blog/
│ │ ├── [slug].js # Dynamic route
│ │ └── [category]/
│ │ └── [slug].js # Nested dynamic route
│ └── api/
│ └── blog/
│ └── [slug].js # Dynamic API route
├── components/
│ ├── BlogList.js
│ └── ThemeProvider.js
├── middleware.js
├── next.config.js
└── package.json
Closing Thoughts
Next.js routing is powerful because it scales with your application. You can start with a handful of static pages and gradually introduce:
- Dynamic and catch‑all routes
- Nested layouts
- Scroll restoration
- Selective prefetching
- Middleware‑based access control
…without having to restructure what you’ve already built.
Mastering these patterns gives you a routing layer that’s both flexible enough to handle edge cases and structured enough to stay maintainable as your team and codebase grow.