DEV Community

Cover image for Exploring Next.js advanced routing and beyond
Agbo, Daniel Onuoha
Agbo, Daniel Onuoha

Posted on

Exploring Next.js advanced routing and beyond

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.

This article covers the techniques that matter most: dynamic routing, nested layouts, advanced navigation patterns, and the middleware layer that ties them together.

1. Dynamic Routing and Route Matching

Dynamic routing lets you create a single file that handles an entire class of URLs. A blog where each post has a unique slug is the classic example — rather than creating a separate file for every post, you define one route using square brackets.

Note on router versions: The examples below use the pages directory and next/router. If you're using the app directory (Next.js 13+), replace useRouter from next/router with useParams from next/navigation.

Creating Dynamic Routes

// pages/blog/[slug].js
import { useRouter } from 'next/router';

const BlogPost = () => {
  const { slug } = useRouter().query;
  return <h1>Blog Post: {slug}</h1>;
};

export default BlogPost;
Enter fullscreen mode Exit fullscreen mode

This maps any URL matching /blog/* — such as /blog/my-first-post — to the BlogPost component, passing the matched segment as slug via the query object.

Handling Multiple Dynamic Segments

For more structured content hierarchies, nest dynamic segments:

// pages/blog/[category]/[slug].js
const BlogCategoryPost = () => {
  const { category, slug } = useRouter().query;
  return (
    <div>
      <h1>Category: {category}</h1>
      <h2>Post: {slug}</h2>
    </div>
  );
};

export default BlogCategoryPost;
Enter fullscreen mode Exit fullscreen mode

This supports URLs like /blog/tech/nextjs-advanced-routing.

Catch-All Routes

When you need to match an arbitrary number of path segments — for example, a documentation site with deeply nested pages — use the spread syntax inside brackets:

// pages/docs/[...slug].js
const DocsPage = () => {
  const { slug } = useRouter().query;
  // slug is an array: ['guide', 'installation', 'windows']
  return <h1>Docs: {slug?.join(' / ')}</h1>;
};
Enter fullscreen mode Exit fullscreen mode

Use [[...slug]].js (double brackets) to also match the root path /docs.

Optimizing Route Transitions

next/link handles client-side navigation and automatically prefetches linked pages in the background, so subsequent loads feel instant:

import Link from 'next/link';

const BlogList = ({ posts }) => (
  <ul>
    {posts.map((post) => (
      <li key={post.slug}>
        <Link href={`/blog/${post.slug}`}>{post.title}</Link>
      </li>
    ))}
  </ul>
);
Enter fullscreen mode Exit fullscreen mode

For cases like filtering or pagination — where you want to update the URL without triggering a full navigation — use shallow routing:

router.push('/blog?page=2', undefined, { shallow: true });
Enter fullscreen mode Exit fullscreen mode

Dynamic API Routes

Dynamic segments work identically in API routes:

// 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}` });
}
Enter fullscreen mode Exit fullscreen mode

This endpoint is accessible at /api/blog/my-first-post and follows the same file-based convention as page routes.

2. Nested Layouts and Shared Components

The app directory, introduced in Next.js 13, makes layout composition a first-class feature. Instead of manually wrapping pages in providers and shell components, you colocate layout files with the routes they govern.

Defining Nested Layouts

Layout files wrap their subtree automatically. A root layout provides the outer shell; a section-specific layout adds structure for that section only:

// app/layout.js
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <header>My App Header</header>
        {children}
        <footer>My App Footer</footer>
      </body>
    </html>
  );
}

// app/blog/layout.js
export default function BlogLayout({ children }) {
  return (
    <div className="blog-container">
      <aside>Blog Sidebar</aside>
      <main>{children}</main>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

When a user visits /blog/my-post, Next.js renders the root header and footer around the blog sidebar and the post content — with no manual wrapping required. Only the content that changes between navigations re-renders, which reduces unnecessary layout re-renders and improves performance.

Managing Shared State

For state that needs to cross layout boundaries, React context is a clean solution for small to medium applications:

// components/ThemeProvider.js
'use client';
import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext();

export const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

export const useTheme = () => useContext(ThemeContext);
Enter fullscreen mode Exit fullscreen mode

Note the 'use client' directive — any component using hooks or browser APIs must opt into client-side rendering within the app directory.

As your app scales, frequent context updates can trigger re-renders across deeply nested components. When that becomes a problem, libraries like Zustand or Redux offer more granular control.

Metadata

The app directory also provides a clean API for defining page metadata without a separate <Head> component:

// app/blog/layout.js
export const metadata = {
  title: 'Blog — My App',
  description: 'Read the latest posts on our blog.',
};
Enter fullscreen mode Exit fullscreen mode

Next.js merges metadata from nested layouts and pages automatically, with child values overriding parents.

3. Advanced Navigation Patterns

Programmatic Navigation

The useRouter hook (or useRouter from next/navigation in the app directory) enables navigation triggered by user interactions or application logic:

import { useRouter } from 'next/router';

const NavigateButton = () => {
  const router = useRouter();
  return (
    <button onClick={() => router.push('/dashboard')}>
      Go to Dashboard
    </button>
  );
};
Enter fullscreen mode Exit fullscreen mode

Use router.replace instead of router.push when you don't want the navigation to add an entry to the browser history — useful for redirects after form submission.

Scroll Restoration

By default, Next.js scrolls to the top on each navigation. To restore a user's previous scroll position when they navigate back, enable scroll restoration in next.config.js:

module.exports = {
  experimental: {
    scrollRestoration: true,
  },
};
Enter fullscreen mode Exit fullscreen mode

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.

Optimized Preloading

next/link prefetches in-viewport links automatically. For routes the user is very likely to visit next but that aren't yet on screen, you can trigger prefetching programmatically:

import { useEffect } from 'react';
import Router from 'next/router';

const HomePage = () => {
  useEffect(() => {
    Router.prefetch('/dashboard');
  }, []);

  return <h1>Welcome</h1>;
};
Enter fullscreen mode Exit fullscreen mode

Be selective here. Prefetching every possible route increases memory usage and can degrade overall performance. Focus on routes that appear immediately after the user's current step — the next page in an onboarding flow, for instance, or the most commonly clicked item in a navigation menu.

Middleware for Access Control

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));
  }
}

export const config = {
  matcher: ['/dashboard/:path*'],
};
Enter fullscreen mode Exit fullscreen mode

The matcher config limits the middleware to specific paths, so it doesn't run on every request. For more complex scenarios, you can inspect the token to check roles and redirect to different destinations based on permissions.

Recommended Folder Structure

As your routing logic grows, keeping the project organized becomes critical. Here's a structure that accommodates all the patterns above:

/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
Enter fullscreen mode Exit fullscreen mode

Conclusion

Next.js routing is powerful precisely because it scales with your application. You can start with a handful of static pages and gradually introduce dynamic segments, nested layouts, and middleware as complexity demands — without restructuring what you've already built.

The patterns covered here — dynamic and catch-all routes, layout composition, scroll restoration, selective prefetching, and middleware-based access control — address the most common challenges you'll encounter as a Next.js application matures. Master these, and you'll have a routing layer that's both flexible enough to handle edge cases and structured enough to stay maintainable as your team and codebase grow.

Top comments (0)