Today we're going to be walking through various ways we can extend the Route module within Adonis. We'll specifically be adding additional chainable methods to the Route object, our group chain, our resource chain, and our brisk route chain. Lastly, we'll learn how we can add our own matcher onto the Route.matchers
object.
Where To Extend
Before we can extend any of these objects, we must first learn where we can extend them since things can go awry if we add them too soon or too late within the Adonis lifecycle.
In most cases, when we extend things we'll want to do so within a provider since these have hooks we can use for particular lifecycle events. In this particular case, we'll want to extend our route objects just after the IoC container is ready to use, so the correct lifecycle event for us today is the boot hook.
So, head into your AppProvider
at /providers/AppProvider.ts
and you'll be greeted with the below.
import { ApplicationContract } from '@ioc:Adonis/Core/Application' export default class AppProvider { constructor (protected app: ApplicationContract) { } public register () { // Register your own bindings } public async boot () { // IoC container is ready } public async ready () { // App is ready } public async shutdown () { // Cleanup, since app is going down } }
Copied!
- providers
- AppProvider.ts
First things first, let's get our Route module imported so we can extend it. Now, we're not going to want to import it at the top level, like our ApplicationContract is, because we need to wait for the IoC container to be ready so that everything is bound and ready to go on our Route module. So, we'll want to import our Route module within our boot method.
To make importing within our boot method easier we can make use of our app's IoC container, accessible at this.app.container
. This object contains a method called use
that we can use to import using the Route modules binding lookup, `Adonis/Core/Route`.
public async boot () { // IoC container is ready const Route = this.app.container.use('Adonis/Core/Route') }
Copied!
With that, we're ready to move forward and start extending!
Macroable Constructor Properties
Within our Route module, Adonis provides macroable versions of certain properties. It has one for our Route, RouteGroup, RouteResource, BriskRoute, and RouteMatchers. Each of these contains three methods getter, hydrate, and macro. That's the structure of the MacroableConstructor.
Macro is what we'll be focusing on today. It's what's going to allow us to easily add additional chainable methods to our Route module. The first argument is the name of the macro function that'll be registered to the Route module. Below is how we can use it.
// macro definition Route.Route.macro('macroName', function (args) { // do stuff // allow macro to be chained further return this }) // macro usage Route.get('posts', () => 'post handler').macroName()
Copied!
One very important note to make here is that you want a scoped function, so don't use an arrow function. The macro callback will be given the scope of the route chain, so in order to allow our macro to be chained off of further, we'll want to return this
to continue the scope down the chain. Also, note you can pass arguments into your macro function and they'll be passed to your macro callback where I have args
above.
Getter is very similar, though, I can't think of any use-cases where it'd be useful with our Route module. The only difference here would be we'd return back whatever it is we're trying to get and instead of calling as a method it's just a property.
// getter definition Route.Route.getter('getterName', function () { return 'your data' }) // getter usage Route.getterName
Copied!
Hydrate is a function we can call to remove dynamically added methods and properties, so if we need to remove a macro after we're done using it we can call hydrate to make that happen.
Route.Route.hydrate()
Copied!
Extending the Route & RouteContract
To start with we're going to run through the example Adonis has on their documentation because it aligns really well with where we left off in the last lesson with Signed URLs. We'll be extending the Route module to include an additional chain called mustBeSigned
that will add a middleware to verify requests for the route contain a valid Signed URL signature.
Get AppProvider Up-To-Date
So, let's get our AppProvider
up-to-date with what we've covered already.
import { ApplicationContract } from '@ioc:Adonis/Core/Application' export default class AppProvider { constructor (protected app: ApplicationContract) { } public register () { // Register your own bindings } public async boot () { // IoC container is ready const Route = this.app.container.use('App/Core/Route') Route.Route.macro('mustBeSigned', function () { // do stuff return this }) } public async ready () { // App is ready } public async shutdown () { // Cleanup, since app is going down } }
Copied!
- providers
- AppProvider.ts
Define MustBeSigned Macro
Next, we're going to want to define a middleware to run for the route when we chain this macro to the route. This middleware will be in charge of verifying the route has a valid Signed URL signature on it. To keep things brief, I'm going to focus on the boot method in our AppProvider
from now on.
> boot() public async boot () { // IoC container is ready const Route = this.app.container.use('App/Core/Route') Route.Route.macro('mustBeSigned', function () { this.middleware(async (ctx, next) => { if (!ctx.request.hasValidSignature()) { ctx.response.badRequest('Invalid signature') return } await next() }) return this } }
Copied!
- providers
- AppProvider.ts
Here we're using the scope of our Route chain to self-define a middleware to the route that calls the hasValidSignature
method on our HttpContext's request object to verify that the route has a valid signature. If it does we'll await next, otherwise, we'll return back that the request has an invalid signature.
Inform TypeScript by Extending RouteContract
Lastly, before we can use our fancy new macro, we need to inform TypeScript about it. To do this, let's create a new file within our contracts
directory called route.ts
. Within this file, we'll want to declare the module @ioc:Adonis/Core/Route
and extend the interface RouteContract
with our macro method.
declare module '@ioc:Adonis/Core/Route' { interface RouteContract { mustBeSigned(): this } }
Copied!
- contracts
- route.ts
Using Your New Macro
The last thing to do is to put it to use! So, on the reset password route, we defined in the last lesson, we can now refactor it to the below, making use of our mustBeSigned
macro to verify the signature is valid on the route. We'll know, thanks to the middleware we defined in our macro, that if we're inside the route handler, that the signature is indeed valid.
Route.get('/reset-password/:email', async ({}) => { return 'signature is valid' }).as('reset.password').mustBeSigned()
Copied!
- start
- routes.ts
Extending Route Group & GroupContract
Next, let's go ahead and get extending the route groups out of the way. We'll breeze through this one since it's the exact same as the route extension.
First We Define the Macro
Instead of defining our macro on Route.Route
, we’ll instead want to use Route.RouteGroup
. Everything else remains the same.
public async boot () { // IoC container is ready const Route = this.app.container.use('App/Core/Route') // ... route macro Route.RouteGroup.macro('mustBeSigned', function () { this.middleware(async (ctx, next) => { if (!ctx.request.hasValidSignature()) { ctx.response.badRequest('Invalid signature') return } await next() }) return this }) }
Copied!
- providers
- AppProvider.ts
Then We Inform TypeScript
Again, instead of extending the RouteContract
interface we’ll extend the RouteGroupContract
interface.
declare module '@ioc:Adonis/Core/Route' { // ... other interfaces interface RouteGroupContract { mustBeSigned(): this } }
Copied!
- contracts
- route.ts
Last We Put It To Use
Since we've added mustBeSigned
to our route groups as well, we can now verify that all requests to routes inside the group have a valid signature on them.
Route.group(() => { Route.get('/reset-password/:email', async ({}) => { return 'signature is valid' }).as('reset.password') }).mustBeSigned()
Copied!
- start
- routes.ts
Extending Brisk Routes & BriskRouteContract
I don't have a very good use-case for why you'd need to extend a brisk route, but let's cover it in case you do. Most things behave the same way as the route and route group, however, they do have a limited scope so there's no access to middleware or, really, anything the other routes have. Which, that's of course why they're brisk routes. So, let's just create a utility method to redirect to a static route.
public async boot () { // IoC container is ready const Route = this.app.container.use('App/Core/Route') // ... other macros Route.BriskRoute.macro('goHome', function() { this.redirect('/') }) }
Copied!
- providers
- AppProvider.ts
declare module '@ioc:Adonis/Core/Route' { // ... other interfaces interface BriskRouteContract { goHome(): this } }
Copied!
- contracts
- route.ts
Route.on('/testing').goHome()
Copied!
- start
- routes.ts
So, now if we visit /testing
in our application our goHome
macro will redirect us back to the home page.
Extending Route Resource & RouteResourceContract
Now, we haven't covered resource routes yet because they use Controllers, which we haven't covered yet. So, we'll be covering resource routes while we're covering Controllers. However, I do want to go ahead and cover extending the resource route chain here.
If you're not familiar with resource routes, they register all routes needed to show pages and perform CRUD operations on a given resource. So, they're defining multiple routes all in one call. Say we were working with posts, a post resource route would define routes for
Getting all posts
Getting a single post
Showing the create page
Storing the post
Showing the update page
Updating the post
Deleting the pos
So, with that in mind, we can't define middleware the same way we are with the others because we need granular control over which route(s) the middleware should apply to. To aid with this, we'll provide a function to the middleware call and we can define a particular key for the route we want to add the middleware to. Alternatively, you can also define the key as '*'
to define the middleware for all routes in the resource.
public async boot () { // IoC container is ready const Route = this.app.container.use('App/Core/Route') // ... other macros Route.RouteResource.macro('mustBeAdmin', function () { this.middleware({ // perform on specific route destroy: async (ctx, next) => { // perform some check to ensure record can be deleted if (ctx.auth.user.roleId !== 1) { ctx.response.unauthorized("You don't have the needed permission") } await next() }, // perform on all resource routes '*': async (ctx, next) => { await next() } }) return this }) }
Copied!
- providers
- AppProvider.ts
declare module '@ioc:Adonis/Core/Route' { // ... other interface extensions interface RouteResourceContract { mustBeAdmin(): this } }
Copied!
- contracts
- route.ts
Route.resource('posts', 'PostsController').mustBeAdmin()
Copied!
- start
- routes.ts
Extending Route Matchers & RouteMatchersContract
Lastly, let's cover how we can add our own matcher to the available Route.matchers
. Unlike the others, we'll want to return back a valid matcher object instead of returning back the scope. Remember, a matcher object consists of a match
key with a RegExp
value and a cast
key with a function that receives the value and needs to return the casted value.
public async boot () { // IoC container is ready const Route = this.app.container.use('App/Core/Route') // ... other macros Route.RouteMatchers.macro('alphaString', function () { return { match: /^[a-z]+$/i, cast: (value) => value.toString() } }) }
Copied!
- providers
- AppProvider.ts
Note I have the cast above to show how you can include it, but since all route parameters come through as strings there's no need for it in this use case.
declare module '@ioc:Adonis/Core/Route' { // ... other interface extensions interface RouteMatchersContract { alphaString(): { match: RegExp, cast: (value: string) => string } } }
Copied!
- contracts
- route.ts
Route.get('/test/:testing').where('testing', Route.matchers.alphaString())
Copied!
- start
- routes.ts
Next Up
So, we've covered how you can add your own route matcher, and add additional chainable methods on your route, route group, route resource, and brisk route objects. In the next lesson, we'll start cleaning up our route files by extracting our route definitions out into controllers.
Join The Discussion! (0 Comments)
Please sign in or sign up for free to join in on the dicussion.
Be the first to Comment!