Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Middleware

Middleware is an intermediate handler: code the request passes through before the controller, and the response after it. It is the place for shared checks — "is the user logged in", "are they blocked" — and for logging requests, without repeating code in every controller.

Creating it

node ace make:middleware log_requests

The command asks which stack to add the middleware to — choose named. The file app/middleware/log_requests_middleware.ts appears:

// app/middleware/log_requests_middleware.ts
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
 
export default class LogRequestsMiddleware {
  async handle(ctx: HttpContext, next: NextFn) {
    const start = Date.now()
    const output = await next()
    console.log(`${ctx.request.method()} ${ctx.request.url()} — ${Date.now() - start} ms`)
    return output
  }
}

Code before await next() runs before the controller, code after it runs when the response is ready. If you do not call next(), the request goes no further.

Registering it

All middleware is listed in start/kernel.ts. Named middleware goes into the middleware object:

// start/kernel.ts
export const middleware = router.named({
  guest: () => import('#middleware/guest_middleware'),
  auth: () => import('#middleware/auth_middleware'),
  logRequests: () => import('#middleware/log_requests_middleware'),
})

The make:middleware command has already added the last line. Attach the middleware to a route:

// start/routes.ts
import { middleware } from '#start/kernel'
 
router.get('/posts', [PostsController, 'index']).use(middleware.logRequests())

After each request to /posts the server terminal shows a line like GET /posts — 12 ms.

Three stacks

StackWhere it is registeredWhen it runs
Serverserver.use([...])On every request, even without a route
Routerrouter.use([...])On every matched route
Namedrouter.named({...})Only where attached with .use()

Protecting a route group

Most often named middleware is attached to a group. All post management pages are available only to logged-in users:

// start/routes.ts
router
  .group(() => {
    router.get('/posts/create', [PostsController, 'create']).as('posts.create')
    router.post('/posts', [PostsController, 'store']).as('posts.store')
  })
  .use(middleware.auth())

A guest who opens /posts/create is sent to the login page — the redirect address is set in app/middleware/auth_middleware.ts by the redirectTo property.

The user on every page

To make auth.user known on public pages too, you need a middleware that simply checks the session. Create it with node ace make:middleware silent_auth and choose the router stack:

// app/middleware/silent_auth_middleware.ts
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
 
export default class SilentAuthMiddleware {
  async handle(ctx: HttpContext, next: NextFn) {
    await ctx.auth.check()
    return next()
  }
}

check does not throw for guests, it only fills auth.user if the user is logged in. The command adds the middleware to the router stack after the auth initialization:

// start/kernel.ts
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'),
])

Now the blog header shows the user's name on any page.

Common mistake

The await before next() is forgotten. Then the "after" code runs before the controller has finished: the timing shows 0 ms, and controller errors bypass the middleware. Always write const output = await next() and return output, as in the example.

Thanks for reading!

Donate to the site — we work hard for you.