Playing Next Lesson In
seconds

Let's Learn AdonisJS 7 #2.9

Middleware & Grouping Routes

In This Lesson

Learn AdonisJS middleware for intercepting HTTP requests. Control request flow, implement logging, and secure your applications.

Created by
@tomgobich
Published

Middleware are functions that intercept incoming HTTP requests before they reach your route handlers. They allow you to perform actions like authentication checks, logging, modifying requests or responses, and much more.

Each middleware can:

  • Pass the request to the next middleware by calling next()

  • Modify the request before passing it along

  • Modify the response coming back

  • Stop the pipeline and return a response early

Creating Middleware

Let's create our own middleware to track how long requests take to process. We can use the make:middleware command to do this. When we run this command, it'll ask us which type of middleware we're creating.

  • Server middleware will run for every request entering our application

  • Router middleware will run for every request hitting a defined route in our application

  • Named middleware will run only on routes where we specifically add them

node ace make:middleware request_logger
# ❯ Under which stack you want to register the middleware? · server
# DONE:    create app/middleware/request_logger_middleware.ts
# DONE:    update start/kernel.ts file
Copied!

Let's select server for now. This will create a new middleware within app/middleware and register this as a new server middleware within our start/kernel.ts. The kernel file is responsible for registering middleware in our application.

Middleware Execution Flow

Our new middleware should look like the one below.

import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'

export default class RequestLoggerMiddleware {
  async handle(ctx: HttpContext, next: NextFn) {
    /**
     * Middleware logic goes here (before the next call)
     */
    console.log(ctx)

    /**
     * Call next method in the pipeline and return its output
     */
    const output = await next()
    return output
  }
}
Copied!
  • app
  • middleware
  • request_logger_middleware.ts

As you can see, we have access to our HttpContext for the request here, just like in our controller methods. There is also a next() function as well. This next function is what instructs AdonisJS to move on from this middleware to the next one. When the route runs out of middleware, it'll move onto the route handler. Once the route handler is done, it'll work backwards through our middleware, running anything we've defined after the next() function, like a mountain pipeline.

├── Request
│   ├── Server Middleware (before next)
│   │   ├── Router Middleware (before next)
│   │   │   ├── Route Middleware (before next)
│   │   │   │   └── Route Handler
│   │   │   └── Route Middleware (after next)
│   │   └── Router Middleware (after next)
│   └── Server Middleware (after next)
└── Response

We can demonstrate this by tracking how long it takes to get from before next to after next!

import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import { DateTime } from 'luxon'

export default class RequestLoggerMiddleware {
  async handle(ctx: HttpContext, next: NextFn) {
    const start = DateTime.now()

    await next()

    const end = DateTime.now()
    const duration = end.diff(start).as('milliseconds').toFixed(2)

    ctx.logger.info(`[${ctx.request.method()}] ${ctx.request.url()} - ${duration} ms`)
  }
}
Copied!
  • app
  • middleware
  • request_logger.ts

Our HttpContext also has access to a logger for our request. We can use this to print out an info message so that when we visit /challenges in our application, we should see something like:

INFO (89496): [GET] /challenges - 11.00 ms

Middleware Types

Since we have this as a server middleware, this will run even if we request a page that doesn't exist. So, if we request /not-found we'll see it still logs out.

INFO (89496): [GET] /not-found - 29.00 ms

Let's compare this to router middleware. If we jump into our start/kernel.ts file we'll see the below.

/*
|--------------------------------------------------------------------------
| HTTP kernel file
|--------------------------------------------------------------------------
|
| The HTTP kernel file is used to register the middleware with the server
| or the router.
|
*/

import router from '@adonisjs/core/services/router'
import server from '@adonisjs/core/services/server'

/**
 * The error handler is used to convert an exception
 * to an HTTP response.
 */
server.errorHandler(() => import('#exceptions/handler'))

/**
 * The server middleware stack runs middleware on all the HTTP
 * requests, even if there is no route registered for
 * the request URL.
 */
server.use([
  () => import('#middleware/container_bindings_middleware'),
  () => import('@adonisjs/static/static_middleware'),
  () => import('@adonisjs/vite/vite_middleware'),
  () => import('#middleware/request_logger_middleware')
])

/**
 * The router middleware stack runs middleware on all the HTTP
 * requests with a registered route.
 */
router.use([
  () => import('@adonisjs/core/bodyparser_middleware'),
  () => import('@adonisjs/session/session_middleware'),
  () => import('@adonisjs/shield/shield_middleware'),
  () => import('@adonisjs/auth/initialize_auth_middleware'),
  () => import('#middleware/silent_auth_middleware'),
])

/**
 * Named middleware collection must be explicitly assigned to
 * the routes or the routes group.
 */
export const middleware = router.named({
  guest: () => import('#middleware/guest_middleware'),
  auth: () => import('#middleware/auth_middleware'),
})
Copied!
  • start
  • kernel.ts

We've got our error handling middleware, server middleware, router middleware, and finally named middleware. Lazy-imports are how we register a middleware to a specific middleware type. So, if we move our request logger middleware from server.use to router.use it'll convert from being a server middleware to instead being a router middleware.

// ...

/**
 * The server middleware stack runs middleware on all the HTTP
 * requests, even if there is no route registered for
 * the request URL.
 */
server.use([
  () => import('#middleware/container_bindings_middleware'),
  () => import('@adonisjs/static/static_middleware'),
  () => import('@adonisjs/vite/vite_middleware'),
])

/**
 * The router middleware stack runs middleware on all the HTTP
 * requests with a registered route.
 */
router.use([
  () => import('@adonisjs/core/bodyparser_middleware'),
  () => import('@adonisjs/session/session_middleware'),
  () => import('@adonisjs/shield/shield_middleware'),
  () => import('@adonisjs/auth/initialize_auth_middleware'),
  () => import('#middleware/silent_auth_middleware'),
  () => import('#middleware/request_logger_middleware'),
])

// ...
Copied!
  • start
  • kernel.ts

If we request /not-found one more time, we'll notice we no longer get a log of our request time. However, if we request /challenges again, our request time log goes through just fine, verifying that server middleware runs for any request and router middleware only runs for registered routes. Note, server and router middleware are executed in the order they're imported here.

Finally, if we move this from router.use down to our router.named we'll switch this to a named middleware, so we also need to give it a name.

// ...

/**
 * The router middleware stack runs middleware on all the HTTP
 * requests with a registered route.
 */
router.use([
  () => import('@adonisjs/core/bodyparser_middleware'),
  () => import('@adonisjs/session/session_middleware'),
  () => import('@adonisjs/shield/shield_middleware'),
  () => import('@adonisjs/auth/initialize_auth_middleware'),
  () => import('#middleware/silent_auth_middleware'),
])

/**
 * Named middleware collection must be explicitly assigned to
 * the routes or the routes group.
 */
export const middleware = router.named({
  guest: () => import('#middleware/guest_middleware'),
  auth: () => import('#middleware/auth_middleware'),
  requestLogger: () => import('#middleware/request_logger_middleware'),
})
Copied!
  • start
  • kernel.ts

If we request /challenges again, note we no longer get a log. This is because named middleware only runs on routes we've specifically added the middleware to. So, we need to first add requestLogger to challenges.index in order for it to log.

import { controllers } from '#generated/controllers'
import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'

// ...

router.get('/challenges', [controllers.Challenges, 'index']).use(middleware.requestLogger())
router.get('/challenges/:id', [controllers.Challenges, 'show'])
router.get('/challenges/create', [controllers.Challenges, 'create'])
router.post('/challenges', [controllers.Challenges, 'store'])
router.get('/challenges/:id/edit', [controllers.Challenges, 'edit'])
router.put('/challenges/:id', [controllers.Challenges, 'update'])

// ...
Copied!
  • start
  • routes.ts

Request /challenges one more time, and voila! There's our log again.

Middleware & Route Groups

The use method accepts one or more middleware we want to define on the specific route or route group. Anything we add to route groups gets applied to all routes within the group. So, we could wrap our challenges in a group to easily apply this middleware to all these routes in one go.

// ...

router
  .group(() => {
    router.get('/challenges', [controllers.Challenges, 'index'])
    router.get('/challenges/:id', [controllers.Challenges, 'show'])
    router.get('/challenges/create', [controllers.Challenges, 'create'])
    router.post('/challenges', [controllers.Challenges, 'store'])
    router.get('/challenges/:id/edit', [controllers.Challenges, 'edit'])
    router.put('/challenges/:id', [controllers.Challenges, 'update'])
  })
  .use(middleware.requestLogger())

// ...
Copied!
  • start
  • routes.ts

We can take this a step further as well and move /challenges to the group-level as well if we wish. This works for names, domains, and matchers as well.

// ...

router
  .group(() => {
    router.get('/', [controllers.Challenges, 'index'])
    router.get('/:id', [controllers.Challenges, 'show'])
    router.get('/create', [controllers.Challenges, 'create'])
    router.post('/', [controllers.Challenges, 'store'])
    router.get('/:id/edit', [controllers.Challenges, 'edit'])
    router.put('/:id', [controllers.Challenges, 'update'])
  })
  .prefix('/challenges')
  .use(middleware.requestLogger())

// ...
Copied!
  • start
  • routes.ts

Middleware & Resources

Okay, I think we're ready to condense this down to a simple resource route definition. Again, this takes the shared pattern (/challenges) and the controller and AdonisJS will do the rest.

// ...

router.resource('challenges', controllers.Challenges).use('*', middleware.requestLogger())

// ...
Copied!
  • start
  • routes.ts

Note that here use requires two arguments. In the first, we specify which route in the resource to target, * will target them all. Then, we provide the middleware. The first argument can also be an array of routes as well.

// ...

router
  .resource('challenges', controllers.Challenges)
  .use(['index', 'edit', 'create'], middleware.requestLogger())

// ...
Copied!
  • start
  • routes.ts

Prematurely Ending A Request

Finally, we can prematurely stop a request in its tracks with middleware as well by returning a response before the next method.

import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import { DateTime } from 'luxon'

export default class RequestLoggerMiddleware {
  async handle(ctx: HttpContext, next: NextFn) {
    if (ctx.request.input('bail')) {
      return ctx.response.json({ message: 'Bailed out!' })
    }

    const start = DateTime.now()

    await next()

    const end = DateTime.now()
    const duration = end.diff(start).as('milliseconds').toFixed(2)

    ctx.logger.info(`[${ctx.request.method()}] ${ctx.request.url()} - ${duration} ms`)
  }
}
Copied!
  • app
  • middleware
  • request_logger_middleware.ts

the input method on our request will attempt to find the key provided in either our request body or query string. So, if we request /challenges?bail=true then we'll get our "Bailed out!" message. If we omit the bail query param, our request will go on as usual!

Join the Discussion 0 comments

Create a free account to join in on the discussion
robot comment bubble

Be the first to comment!