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')
Copied!
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.
import Route from '@ioc:Adonis/Core/Route' export default function postRoutes() { Route.group(() => { Route.get('/', () => 'get all posts').as('index') }).prefix('/posts').as('posts') }
Copied!
- start
- routes
- api
- v1
- posts.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') }
Copied!
- start
- routes
- api
- v1
- series.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')
Copied!
- start
- routes.ts
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.
import Route from '@ioc:Adonis/Core/Route' Route.group(() => { Route.get('/', () => 'get all posts').as('index') }).prefix('/posts').as('posts')
Copied!
- start
- routes
- api
- v1
- posts.ts
import Route from '@ioc:Adonis/Core/Route' Route.group(() => { Route.get('/', () => 'get all series').as('index') }).prefix('/series').as('series')
Copied!
- start
- routes
- api
- v1
- series.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')
Copied!
- start
- routes.ts
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.
import Route from '@ioc:Adonis/Core/Route' Route.group(() => { require('./v1/posts') require('./v1/series') }).prefix('/api/v1').as('api.v1')
Copied!
- start
- routes
- api
- index.ts
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.
// ... other imports import './routes/api' // ... other imports / routes
Copied!
- start
- routes.ts
If using import
will cause problems with your route precedence, then you can use an IIFE.
// ... other imports / routes require('./routes/api') // ... other routes
Copied!
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.
frp
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 :)
Please sign in or sign up for free to reply
tomgobich
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.
Please sign in or sign up for free to reply
frp
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.
Please sign in or sign up for free to reply
tomgobich
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.
Please sign in or sign up for free to reply
frp
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
Please sign in or sign up for free to reply
tomgobich
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!
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.Please sign in or sign up for free to reply
frp
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!
Please sign in or sign up for free to reply
frp
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!
Please sign in or sign up for free to reply
tomgobich
Oh rats!! I think something like this might work:
Please sign in or sign up for free to reply
frp
I see what you did there. I will give that a try and report back! lol Thanks
…. It worked! I have routes!
Please sign in or sign up for free to reply
tomgobich
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! 😊
Please sign in or sign up for free to reply
frp
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.
Please sign in or sign up for free to reply
tomgobich
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:
Then, the sub-files being imported would export as functions.
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
/
Please sign in or sign up for free to reply
frp
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.
Please sign in or sign up for free to reply
tomgobich
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. 😊
Please sign in or sign up for free to reply
frp
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!
Please sign in or sign up for free to reply
frp
Yup, that was it. I commented out the domain() and it works. Haha sorry about the wild chicken chase!
Please sign in or sign up for free to reply
tomgobich
Oh, lol! No worries at all, I'm happy you were able to get everything figured out and working! 😄
Please sign in or sign up for free to reply