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

Forget File System Routing. Here's How To Setup Custom Routing in Nuxt 3

Custom Routing With Nuxt 3 image banner

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 crucial. 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.

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.


Want to learn something new?

Subscribe to the newsletter!


Thomas Findlay photo

About author

Thomas Findlay is a 5 star rated mentor, full-stack developer, consultant, technical writer and the author of "The Road To Enterprise" books. He works with many different technologies such as JavaScript, Vue, Nuxt, React, Next, React Native, Node.js, Python, PHP, and more. He has obtained MSc in Advanced Computer Science degree with Distinction at Exeter University, as well as First-Class BSc in Web Design & Development at Northumbria University.

Over the years, Thomas has worked with many developers and teams from beginners to advanced and helped them build and scale their applications and products. He also mentored a lot of developers and students, and helped them progress in their careers.

To get to know more about Thomas you can check out his Codementor and Twitter profiles.