Multi-File Route Grouping Strategies

In this lesson, we'll discuss strategies you can use to apply multiple files worth of route definitions into a single route group without redefining the route group in each file.

Published
Oct 06, 21
Duration
5m 31s

Developer, dog lover, and burrito eater. Currently teaching AdonisJS, a fully featured NodeJS framework, and running Adocasts where I post new lessons weekly. Professionally, I work with JavaScript, .Net C#, and SQL Server.

Adocasts

Burlington, KY

I received a great question on the last lesson covering route groups and I wanted to answer it for everyone before moving forward. It's in regard to having routes split among several different files.

The question was if I have a route group I want to apply to more than one file, do I need to redefine the group within each file? In other words, for the below file structure, in order to have a route group for /api/v1 do you need to define the group in both the posts.ts file and the series.ts file?

start/
β”œβ”€ routes/
β”‚  β”œβ”€ api/
β”‚  β”‚  β”œβ”€ v1/
β”‚  β”‚  β”‚  β”œβ”€ posts.ts
β”‚  β”‚  β”‚  β”œβ”€ series.ts
β”œβ”€ routes.ts

That would be a pain! Thankfully, the answer is no, you don't. So long as the code defining a route definition executes within a route group, that route definition will be created as a member of the group.

For example, we can wrap any set of routes within a function then call the function inside two different groups. The result will be the routes defined in the function will be defined twice, once for each group.

function postRoutes() {
  Route.get('/posts', () => 'get all posts')
}

Route.group(() => {
  // this will define a route for GET: /accounts/:accountId/posts
  postRoutes()
}).prefix('/accounts/:accountId')

Route.group(() => {
  // this will define a route for GET: /hub/posts
  postRoutes()
}).prefix('/hub')

We can then use this same methodology to our advantage when it comes to defining routes to a single group from multiple files.

Using A Function

First, let's take the above example and apply it to our multi-file scenario. We can wrap the routes we have defined within our posts.ts file and series.ts file in a function that we export. Then, we can import those functions and execute them within our group, like the below.

// start/routes/api/v1/posts.ts

import Route from '@ioc:Adonis/Core/Route'

export default function postRoutes() {
  Route.group(() => {
    Route.get('/', () => 'get all posts').as('index')
  }).prefix('/posts').as('posts')
}
// start/routes/api/v1/series.ts

import Route from '@ioc:Adonis/Core/Route'

export default function postRoutes() {
  Route.group(() => {
    Route.get('/', () => 'get all series').as('index')
  }).prefix('/series').as('series')
}
// start/routes.ts

import Route from '@ioc:Adonis/Core/Route'
import postRoutes from './routes/api/v1/posts'
import seriesRoutes from './routes/api/v1/series'

Route.group(() => {
  postRoutes()
  seriesRoutes()
}).prefix('/api/v1').as('api.v1')

Since the code registering the post and series routes is executed within our /api/v1 route group, the group is applied both. So, our routes end up looking like this:

/api/v1/posts    AS api.v1.posts.index
/api/v1/series   AS api.v1.series.index

Using this knowledge you can also extract the /api/v1 group into its own file within /start/routes/api as well if you'd wish, then you can import it within routes.ts the same way we are our post and series routes.

Importing Using Require

The second option we'll be looking at is importing using require. By using require we can import our code directly where we need it to execute, eliminating the need to wrap everything in an additional function the way we are with the first approach we looked at.

So, with the same structure we used with our function approach, let's see how it'd look using require.

// start/routes/api/v1/posts.ts

import Route from '@ioc:Adonis/Core/Route'

Route.group(() => {
  Route.get('/', () => 'get all posts').as('index')
}).prefix('/posts').as('posts')
// start/routes/api/v1/series.ts

import Route from '@ioc:Adonis/Core/Route'

Route.group(() => {
  Route.get('/', () => 'get all series').as('index')
}).prefix('/series').as('series')
// start/routes.ts

import Route from '@ioc:Adonis/Core/Route'

Route.group(() => {
  require('./routes/api/v1/posts')
  require('./routes/api/v1/series')
}).prefix('/api/v1').as('api.v1')

The first thing you'll notice is our post and series files no longer are wrapped with an exported function. Since we're importing using require, we no longer need a way to put off the execution of this code.

The next thing you'll notice is that we're directly using require inside of our group.

Apart from that, the file structure and the files themselves look very similar. The end-result routes defined are the exact same as well.

/api/v1/posts    AS api.v1.posts.index
/api/v1/series   AS api.v1.series.index

Moving The API Group To Routes/API

If you'd like to move the /api/v1 group into a file within the /start/routes/api directory you can absolutely do that using this approach as well. There's one thing to keep in mind, though. If your routes.ts file isn't just import statements, then you'll need to take into consideration how you import from ./routes/api. If the precedence of your routes allows you to import using import β€˜./pathhere’ you can do that. Otherwise, you may want to use require directly where you need the routes imported.

So, let's say you created a file at /start/routes/api/index.ts and move your /api/v1 route group definition into that file.

// start/routes/api/index.ts

import Route from '@ioc:Adonis/Core/Route'

Route.group(() => {
  require('./v1/posts')
  require('./v1/series')
}).prefix('/api/v1').as('api.v1')

You can import it using import inside your routes.ts if this works with the precedence order of your other route definitions. Remember, a request will stop and use the first route definition matching the requested url.

// start/routes.ts

// ... other imports

import './routes/api'

// ... other imports / routes

If using import will cause problems with your route precedence, then you can use an IIFE.

// ... other imports / routes

require('./routes/api')

// ... other routes

Next Up

So, we've covered a couple of ways you can share a group definition with multiple files without redefining a group. This cuts back on redundancies and also gives you more control over the other of your routes as opposed to defining your groups over again in each file. In the next lesson, we'll get back to our regularly scheduled lessons by covering how to generate route URLs.


Corrections

Thanks to Arthur Emanuel Rezende for correcting me about require being synchronous instead of asynchronous. I had a mental lapse there haha :)

Join The Discussion! (18 Comments)

Please sign in or sign up for free to join in on the dicussion.

  1. Commented 3 months ago

    I noticed on some lessons, you have the text with screenshots like this one, and other lessons you don't. I love this format because I would much rather read than watch a video and it is great to be able to come back to it and scroll to the section I need. This is well-done. Hey, I put in a plug for Adonis to hire you for their tutorials when they start them :)

    1

    Please sign in or sign up for free to reply

    1. Commented 3 months ago

      Yeah, I used to provide both written and video formats for every lesson, but ended up dropping the written portion as it's a decent time suck to produce.

      For example, back when both formats were being produced I was only able to create one, maybe two, lessons per week. Now, with just the video format, we're trending at around 5-9 lessons per week.

      0

      Please sign in or sign up for free to reply

      1. Commented 3 months ago

        Yeah, absolutely, I can imagine. I'm sure you are trying to crank out a million now to get people up to speed with Adonis 6. Must be crazy with all the changes they made. My below comment was because I'm trying to figure out for myself how to make these Adonis 5 tutorials work in Adonis 6, since you have so much stuff for A5 that I need to use.

        0

        Please sign in or sign up for free to reply

        1. Commented 3 months ago

          Yeah lol, that's the goal it to get as much out as possible! Definitely! A lot of the principals translate pretty well between 5 and 6, so the majority of v5 content is still usable, but the module change and some API changes can cause some confusion.

          0

          Please sign in or sign up for free to reply

  2. Commented 3 months ago

    If we were to modify this to Adonis 6, we would need to change that require to an import, right? I was thinking we would want to put "await import('./routegroup.ts')" where you have the require? I guess if the enclosing function is not already async we would have to use the .then construction. Hm. As you can see, I'm still trying to master these concepts

    1

    Please sign in or sign up for free to reply

    1. Commented 3 months ago

      Yeah, the primary thing that matters is where the route definition is run. If it's run within the group, it'll be defined within the group. For example, you could also use functions!

      const postRoutes = () => {
        router.get('/posts').as('posts.index')
        router.post('/posts').as('posts.store')
        router.patch('/posts/:id').as('posts.update')
        router.delete('/posts/:id').as('posts.destroy')
      }
      
      router.group(() => {
        postRoutes()
      }).as('api')
      
      /*
        Our final post routes would be named:
          - api.posts.index
          - api.posts.store
          - api.posts.update
          - api.posts.destroy
      */
      Copied!

      Since the route definitions are run inside the api group, they'll be defined as a portion of that group. So, in essence that's the same behavior as requiring/importing those routes shown in this lesson.

      0

      Please sign in or sign up for free to reply

      1. Commented 3 months ago

        Yeah, I used the functions on an earlier test and it worked great. Probably easier but I'm trying to learn the whole module imports thing so I wanted to try it this way. I made a subdir routes off of start and this is what I did. It appears to be working (didn't throw an error). Thanks!

        import router from '@adonisjs/core/services/router'
        import { fsReadAll } from '@adonisjs/core/helpers' 
        const files = await fsReadAll(new URL('./routes', import.meta.url), { pathType: 'url' }) 
        console.log(files) 
        const imps: string[] = files.map((el) => { return el.substring(el.lastIndexOf('/') + 1, el.lastIndexOf('.')) }) 
        console.log(imps) 
        router.group(async () => { 
        for (let domain of imps) { 
        await import(`./routes/${domain}.js`) 
        } 
        })
        
        
        Copied!
        0

        Please sign in or sign up for free to reply

        1. Commented 3 months ago

          I guess it didn't work. Server did not throw an error but list:routes does not show any routes. Back to the drawing board!

          0

          Please sign in or sign up for free to reply

          1. Commented 3 months ago

            Oh rats!! I think something like this might work:

            
            
            import fs from 'node:fs/promises'
            import app from '@adonisjs/core/services/app'
            import router from '@adonisjs/core/services/router'
            
            const files = await fs.readdir(app.startPath('routes'))
            for (const filename of files) {
              await import(app.startPath(`routes/${filename.replace('.ts', '.js')}`))
            }
            Copied!
            • start
            • routes.ts
            
            
            import router from '@adonisjs/core/services/router'
            
            router.get('/test-1', () => {}).as('test.1')
            
            Copied!
            • start
            • routes
            • test.ts
            
            
            import router from '@adonisjs/core/services/router'
            
            router.get('/test-2', () => {}).as('test.2')
            
            Copied!
            • start
            • routes
            • another.ts
            0

            Please sign in or sign up for free to reply

            1. Commented 3 months ago

              I see what you did there. I will give that a try and report back! lol Thanks

              …. It worked! I have routes!

              0

              Please sign in or sign up for free to reply

        2. Commented 3 months ago

          Yeah, if you're looking for something that'll dynamically pick up new route files as you add them, that looks like a valid solution! Hopefully that'll continue to work for you! 😊

          0

          Please sign in or sign up for free to reply

          1. Commented 3 months ago

            So I am getting "Exception Cannot GET:" in the browser on all of my routes. It points to node_modules/send/index.js

            debug('stat "%s"', path)

            fs.stat(path, function onstat (err, stat) {

            if (err && err.code === 'ENOENT' && !extname(path) && path[path.length - 1] !== sep) {

            // not found, check extensions

            return next(err)

            }

            if (err)

            return self.onStatError(err)

            if (stat.isDirectory())

            return self.redirect(path)

            self.emit('file', path, stat)

            self.send(path, stat)

            Sorry, I don't know how to make it format code in these comment editors. Earlier it just did it automatically.

            0

            Please sign in or sign up for free to reply

            1. Commented 3 months ago

              Ah - yeah I see! It works fine if you have it at the top-level but once put inside a group I see the same error as you. It almost seems like the dynamic import is hoisted or something as this is the same error you guy when AdonisJS can't find a route matching the request.

              It's not pretty, and can probably be cleaned up by using macros, but exporting as functions, importing at the top-level, then calling the functions within the group does seem to get things working.

              So, the base router would look like:

              
              
              import app from '@adonisjs/core/services/app'
              import router from '@adonisjs/core/services/router'
              import fs from 'node:fs/promises'
              
              const files = await fs.readdir(app.startPath('routes'))
              const definitionFunctions: Function[] = []
              
              for (const filename of files) {
                const { default: definition } = await import(app.startPath(`routes/${filename.replace('.ts', '.js')}`))
                definitionFunctions.push(definition)
              }
              
              router.group(async () => definitionFunctions.map((fn) => fn()))
              Copied!
              • start
              • routes.ts

              Then, the sub-files being imported would export as functions.

              
              
              import router from '@adonisjs/core/services/router'
              
              export default () => {
                router.get('/test-1', () => 'working 1').as('test.1')
              }
              Copied!
              • start
              • routes
              • test.ts

              There's gotta be a cleaner way, but that's the best I can figure at the moment.


              Also, yeah sorry about the formatting, I really need to add a legend/guide on what all is available. Basic Markdown is supported so you can enter a code block using three backticks. You can also open a palette by typing /

              0

              Please sign in or sign up for free to reply

              1. Commented 3 months ago

                Thanks again. It's weird that the routes show up when you run the ace command, but not in the app. I will try the function solution from your video and see if we get the same issue.

                edit: I see what you did in your last comment. You made it dynamic instead of hard-coded like in the tutorial. Ok, I will try the comment approach.

                console.log('Hello World!')
                Copied!
                0

                Please sign in or sign up for free to reply

                1. Commented 3 months ago

                  Anytime!! Yeah, I honestly don't fully understand how it could work okay in one but not the other, could be a bug or maybe just a difference between the server and console applications.

                  Yeah, you could absolutely hard-code the imports if you're looking for something simple, but exporting as functions seems to be the only way it's completely happy for some reason. 😊

                  0

                  Please sign in or sign up for free to reply

              2. Commented 3 months ago

                Oh hell, I think I know the problem and it has nothing to do with the import being hoisted. These routes are prefixed by a domain and I am not browsing to the domain. Duh!

                0

                Please sign in or sign up for free to reply

                1. Commented 3 months ago

                  Yup, that was it. I commented out the domain() and it works. Haha sorry about the wild chicken chase!

                  0

                  Please sign in or sign up for free to reply

                  1. Commented 3 months ago

                    Oh, lol! No worries at all, I'm happy you were able to get everything figured out and working! πŸ˜„

                    0

                    Please sign in or sign up for free to reply