Vue - The Road To
        Enterprise logo
The Most Advanced Vue Book Buy Now

Nuxt 3 Custom Routing with pages:extend: A Feature-Driven Architecture Guide

Custom Routing With Nuxt 3 image banner
By Thomas Findlay30 May 2023Updated: 2 Mar 2026

TL;DR: Nuxt 3 has three ways to add custom routes: a router.options.ts file, the pages:extend hook in nuxt.config.ts, and a custom Nuxt module. The pages:extend hook is the most practical: it supports definePageMeta, integrates with Nuxt's build process, and can be automated to pick up route files automatically. The module approach is useful if you need the routing logic reusable across multiple apps.

File system routing is a quick and convenient way of setting up routing in various frameworks, such as Nuxt or Next. You don't need to configure any router yourself. Instead, you get a route for each component file in the pages directory. For example, the following project structure:

pages
├── home
│   └── index.vue
├── profile
│   └── account.vue
├── about.vue
└── index.vue

will result in the following routes being created automatically:

- /
- /home
- /about
- /profile/account

How easy and convenient, isn't it? To be honest, I don't mind using file system routing for smaller projects, as it is quick, and most of the time, there is no need for anything more fancy. However, it's not that great for larger projects.

What's The Problem With File System Routing?

For someone who worked on many medium to large-scale projects, file system routing becomes more of an annoyance than a benefit. In larger projects, a clean and maintainable project architecture is important. Personally, I'm a big fan of feature-driven architecture that focuses on encapsulating common logic in its own feature folder. For instance, for a feature called user profile account, we might need a few different form components, such as change-password, notification-settings, user-preferences, and so on. Since these components are only ever used on the account page, I want to keep them close to the route component.

pages
└── profile
    ├── components
    │   ├── change-password.vue
    │   ├── notification-settings.vue
    │   └── user-preferences.vue
    └── account.vue

Quite often, we also need to create some helpers, validation schemas and composables to extract logic from components, so maybe we would want to have a structure like this:

pages
└── profile
    ├── components
    │   ├── change-password.vue
    │   ├── notification-settings.vue
    │   └── user-preferences.vue
    ├── composables
    │   └── useAccountForm.ts
    ├── helpers
    │   ├── parseAccountData.ts
    │   └── prepareAccountPayload.ts
    ├── schema
    │   └── account.schema.ts
    └── account.vue

Well, we can't really have that in the pages directory if we're using the default file-system routing. A workaround is to put your features outside of the pages folder and have your page components do nothing but import and render a feature component. Unfortunately, with this approach, we now need to play around with imports and the code for specific pages is placed around the codebase instead of being close together. That's why I prefer to use custom routing. And let's be honest. It actually takes little time and effort to configure new routes. Most of the time, we just do it once, and that's it. Thus, I don't think the route setup convenience we get from the file-system routing is worth the trade-off for larger projects. So, let's have a look at how we can set up custom routing in Nuxt 3.

File system routing vs custom routing

Here is a quick comparison before diving into the implementation:

File system routingCustom routing
Setup effortNoneOne-time config
Route colocationNot possibleFull control
definePageMeta supportYesYes (with pages:extend)
Non-route files in pages dirNot allowedN/A (you choose the folder)
Best forSmall projects, prototypesMedium to large projects

Custom Routing In Nuxt 3

A quick look at the docs shows there are three main approaches that will allow us to configure custom routes.

The first one is by adding a router.options.ts file in the app directory.

import type { RouterConfig } from '@nuxt/schema'
// https://router.vuejs.org/api/interfaces/routeroptions.html
export default <RouterConfig>{
  routes: _routes => [
    {
      name: 'home',
      path: '/',
      component: () => import('~/pages/home.vue').then(r => r.default || r),
    },
  ],
}

However, any routes defined in the router.options.ts file will not be augmented by Nuxt with metadata defined in definePageMeta. That's not the best, so off we head to the second approach in the docs - pages:extend hook.

The pages:extend Nuxt Hook To The Rescue

The pages:extend is a nuxt hook that can be used to add, change or remove pages. Here's a simple example of how to use it to add a profile page.

export default defineNuxtConfig({
  hooks: {
    'pages:extend'(pages) {
      // add a route
      pages.push({
        name: 'profile',
        path: '/profile',
        file: '~/extra-pages/profile.vue',
      })
    },
  },
})

Technically, we could add all our routes here, but it wouldn't be so clean. I think it's nicer to have files with routes in the features folders. Here's an example structure:

views
├── dashboard
│   ├── home
│   │   └── home.vue
│   ├── profile
│   │   └── profile.vue
│   ├── settings
│   │   └── settings.vue
│   └── dashboard.routes.ts
└── auth
    ├── login
    │   └── login.vue
    ├── register
    │   └── register.vue
    └── auth.routes.ts

and here are example route files

import { NuxtPage } from '@nuxt/schema'

const dashboardRoutes: NuxtPage[] = [
  {
    path: '/',
    name: 'Home',
    file: '@/views/dashboard/home/home.vue',
  },
  {
    path: '/profile',
    name: 'Profile',
    file: '@/views/dashboard/profile/profile.vue',
  },
  {
    path: '/settings',
    name: 'Settings',
    file: '@/views/dashboard/settings/settings.vue',
  },
]

export default dashboardRoutes
import { NuxtPage } from '@nuxt/schema'

const authRoutes: NuxtPage[] = [
  {
    path: '/login',
    name: 'Login',
    file: '@/views/auth/login/login.vue',
  },
  {
    path: '/register',
    name: 'Register',
    file: '@/views/auth/register/register.vue',
  },
]

export default authRoutes

Next, we need to register those routes in the pages:extend hook. Importing every file manually can be a bit tedious, but fortunately, we can automate it with a little script.

import { defineNuxtConfig } from 'nuxt/config'
import path from 'node:path'
import { glob } from 'glob'
export default defineNuxtConfig({
  srcDir: "./src",
  hooks: {
    async `pages:extend`(pages) {
      const routesFilesPaths = await glob('./src/views/**/*.routes.ts')
      const routes = await Promise.all(
        routesFilesPaths.map(routesFilePath => {
          return import(path.join(path.resolve(), routesFilePath))
        })
      )
      pages.push(...routes.flat(1))
    }
  }
})

The glob function is used to find all files with the routes.ts suffix in the src/views folder. If your routes are somewhere else, then update the regex accordingly. Similarly, if you're not using TypeScript, just replace the .ts file extension with .js. We loop through all the files found and import them in parallel, as we need to get access to the routes defined in each file. Last but not least, we flatten the array of arrays with routes and add them. And that's it. Now every time you need to add new routes, you can create a file that follows *.routes.ts naming convention and export an array of routes.

Custom Routing In Nuxt 3 Using Modules

If, for any reason, you need to have the custom routing initialised inside of a Nuxt Module, the auto importing logic we just covered can be converted into a module like this:

import { defineNuxtModule } from '@nuxt/kit'
import path from 'node:path'
import { glob } from 'glob'

export default defineNuxtModule({
  hooks: {
    async 'pages:extend'(pages) {
      const routesFilesPaths = await glob('./src/views/**/*.routes.ts')
      const routes = await Promise.all(
        routesFilesPaths.map(routesFilePath => {
          return import(path.join(path.resolve(), routesFilePath))
        })
      )
      pages.push(...routes.flat(1))
    },
  },
})

Next we just need to update the Nuxt config file.

import { defineNuxtConfig } from 'nuxt/config'
import { createResolver } from '@nuxt/kit'
const { resolve } = createResolver(import.meta.url)
export default defineNuxtConfig({
  srcDir: './src',
  modules: [resolve('./modules/setupRoutes')],
})

Summary

We have covered how to configure custom routing in Nuxt 3 using the pages:extend nuxt hook. While File System routing is easy and convienient to use, it's not so great for larger projects, as custom routing provides much more control and flexibility. You can find the final code for this article in this GitHub repository.

If you're interesting in learning more about architecture and routing patterns in Vue projects, make sure to check out Vue - The Road To Enterprise book.

Frequently asked questions


Want to learn something new?

Subscribe to the newsletter!


Thomas Findlay photo

About the author

Thomas Findlay is a CTO, senior full-stack engineer, and the author of React - The Road To Enterprise and Vue - The Road To Enterprise. With 13+ years of experience, he has built and led engineering teams, architected multi-tenant SaaS platforms, and driven AI-augmented development workflows that cut feature delivery time by 50%+.

Thomas has spoken at international conferences including React Summit, React Advanced London, and Vue Amsterdam, and has written 50+ in-depth technical articles for the Telerik/Progress blog. He holds an MSc in Advanced Computer Science (Distinction) from the University of Exeter and a First-Class BSc in Web Design & Development from Northumbria University.

With a 5-star rating across 2,500+ mentoring sessions and 1,250+ reviews on Codementor, Thomas has helped developers and teams worldwide with architecture consulting, code reviews, and hands-on development support. Find him on LinkedIn, GitHub, or Twitter/X, or get in touch directly. Read the full bio →