| name | adonisjs-routing |
| description | Routing, middleware, and permission patterns for AdonisJS. Use when working with routes, route groups, resource routes, named routes, middleware, route-level authorization, permission guards, role-based access, or when user mentions routing, middleware, named middleware, route protection, auth guards, RBAC, abilities, bouncer, route organization, API versioning, or any route/middleware configuration in an AdonisJS project. |
AdonisJS Routing & Permissions
Patterns for organizing routes, applying middleware, and protecting routes with role/permission-based authorization using AdonisJS Bouncer.
Related guides:
- Policies - Bouncer policies for per-model authorization
- Controllers - Controllers that use route-level authorization
Reference implementations:
Route Basics
Defining routes
import router from '@adonisjs/core/services/router'
const PostsController = () => import('#controllers/posts_controller')
const UsersController = () => import('#controllers/users_controller')
router.get('/', async () => 'Hello World')
router.get('/posts', [PostsController, 'index'])
router.post('/posts', [PostsController, 'store'])
router.get('/posts/:id', [PostsController, 'show'])
router.put('/posts/:id', [PostsController, 'update'])
router.delete('/posts/:id', [PostsController, 'destroy'])
Named routes
router.get('/posts', [PostsController, 'index']).as('posts.index')
router.get('/posts/:id', [PostsController, 'show']).as('posts.show')
Resource routes (RESTful CRUD)
router.resource('posts', PostsController)
router.resource('posts', PostsController).apiOnly()
router.resource('posts.comments', CommentsController)
Route Groups
Prefix + named groups
router
.group(() => {
router.resource('posts', PostsController)
router.resource('users', UsersController)
})
.prefix('/api/v1')
.as('api.v1')
Middleware on groups
import { middleware } from '#start/kernel'
router
.group(() => {
router.resource('posts', PostsController)
router.get('/dashboard', [DashboardController, 'index'])
})
.use(middleware.auth())
router
.group(() => {
router.get('/posts', [PostsController, 'index'])
router.get('/posts/:id', [PostsController, 'show'])
router
.group(() => {
router.post('/posts', [PostsController, 'store'])
router.put('/posts/:id', [PostsController, 'update'])
router.delete('/posts/:id', [PostsController, 'destroy'])
})
.use(middleware.auth())
})
.prefix('/api')
Resource routes with selective middleware
router
.resource('posts', PostsController)
.use(['store', 'update', 'destroy'], middleware.auth())
router
.resource('posts', PostsController)
.use('*', middleware.auth())
Middleware
Three middleware stacks
| Stack | Scope | Example |
|---|
| Server | Every HTTP request (even no-route matches) | Static files, CORS |
| Router | Every request that matches a route | Body parser, session |
| Named | Explicitly assigned to routes/groups | Auth, role check |
Creating named middleware
node ace make:middleware admin
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
export default class AdminMiddleware {
async handle({ auth, response }: HttpContext, next: NextFn) {
const user = auth.getUserOrFail()
if (user.role !== 'admin') {
return response.forbidden({ error: 'Admin access required' })
}
await next()
}
}
Registering named middleware
import router from '@adonisjs/core/services/router'
router.use([
() => import('#middleware/container_bindings_middleware'),
() => import('@adonisjs/core/bodyparser_middleware'),
() => import('@adonisjs/session/session_middleware'),
])
export const middleware = router.named({
auth: () => import('#middleware/auth_middleware'),
guest: () => import('#middleware/guest_middleware'),
admin: () => import('#middleware/admin_middleware'),
can: () => import('#middleware/can_middleware'),
})
Middleware with parameters
export default class RoleMiddleware {
async handle({ auth, response }: HttpContext, next: NextFn, options: { role: string }) {
const user = auth.getUserOrFail()
if (user.role !== options.role) {
return response.forbidden({ error: `Requires "${options.role}" role` })
}
await next()
}
}
router.get('/admin', [AdminController, 'index']).use(middleware.role({ role: 'admin' }))
Permission Patterns
Pattern 1: Role-based middleware
Simplest approach — check the user's role column directly:
router
.group(() => {
router.resource('users', AdminUsersController)
router.get('/settings', [AdminSettingsController, 'index'])
})
.prefix('/admin')
.use(middleware.auth())
.use(middleware.admin())
Pattern 2: Bouncer abilities (gate-style)
AdonisJS @adonisjs/bouncer provides Laravel Gate-equivalent authorization:
import User from '#models/user'
import Post from '#models/post'
import { Bouncer } from '@adonisjs/bouncer'
export const editPost = Bouncer.ability((user: User, post: Post) => {
return user.id === post.userId
})
export const deletePost = Bouncer.ability((user: User, post: Post) => {
return user.id === post.userId || user.role === 'admin'
})
export const manageUsers = Bouncer.ability((user: User) => {
return user.role === 'admin'
})
Use in controllers:
import { editPost, deletePost } from '#abilities/main'
export default class PostsController {
async update({ bouncer, request, params }: HttpContext) {
const post = await Post.findOrFail(params.id)
await bouncer.authorize(editPost, post)
post.merge(request.only(['title', 'body']))
await post.save()
return post
}
async destroy({ bouncer, params }: HttpContext) {
const post = await Post.findOrFail(params.id)
await bouncer.authorize(deletePost, post)
await post.delete()
}
}
Pattern 3: Bouncer policies (per-model authorization)
Group all authorization for a model into a policy class:
import User from '#models/user'
import Post from '#models/post'
import { BasePolicy } from '@adonisjs/bouncer'
import { AuthorizationResponse } from '@adonisjs/bouncer'
export default class PostPolicy extends BasePolicy {
view(_user: User, _post: Post) {
return true
}
create(user: User) {
return user.isVerified
}
edit(user: User, post: Post) {
return user.id === post.userId
}
delete(user: User, post: Post) {
if (user.role === 'admin') return true
return user.id === post.userId
}
publish(user: User, post: Post) {
if (user.id !== post.userId) {
return AuthorizationResponse.deny('Only the author can publish')
}
if (!post.title || !post.body) {
return AuthorizationResponse.deny('Post must have a title and body')
}
return true
}
}
Use in controllers:
import PostPolicy from '#policies/post_policy'
export default class PostsController {
async update({ bouncer, request, params }: HttpContext) {
const post = await Post.findOrFail(params.id)
await bouncer.with(PostPolicy).authorize('edit', post)
post.merge(request.only(['title', 'body']))
await post.save()
return post
}
}
Pattern 4: Route-level permission middleware
Create a reusable can middleware that checks Bouncer abilities on the route:
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import * as abilities from '#abilities/main'
type AbilityName = keyof typeof abilities
export default class CanMiddleware {
async handle(ctx: HttpContext, next: NextFn, options: { ability: AbilityName }) {
const ability = abilities[options.ability]
if (!ability) {
throw new Error(`Unknown ability: "${options.ability}"`)
}
await ctx.bouncer.authorize(ability)
await next()
}
}
router.get('/admin/users', [AdminUsersController, 'index'])
.use(middleware.auth())
.use(middleware.can({ ability: 'manageUsers' }))
Route Organization
File splitting for large apps
import router from '@adonisjs/core/services/router'
import './routes/api.js'
import './routes/web.js'
import './routes/admin.js'
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
const PostsController = () => import('#controllers/api/posts_controller')
const UsersController = () => import('#controllers/api/users_controller')
router
.group(() => {
router.resource('posts', PostsController).apiOnly()
router.resource('users', UsersController).apiOnly()
})
.prefix('/api/v1')
.as('api.v1')
.use(middleware.auth())
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
const DashboardController = () => import('#controllers/admin/dashboard_controller')
const AdminUsersController = () => import('#controllers/admin/users_controller')
router
.group(() => {
router.get('/dashboard', [DashboardController, 'index']).as('dashboard')
router.resource('users', AdminUsersController)
})
.prefix('/admin')
.as('admin')
.use(middleware.auth())
.use(middleware.admin())
API versioning
router.group(() => {
router.resource('posts', PostsControllerV1).apiOnly()
}).prefix('/api/v1').as('api.v1')
router.group(() => {
router.resource('posts', PostsControllerV2).apiOnly()
}).prefix('/api/v2').as('api.v2')
Useful Route Commands
node ace list:routes
node ace make:controller Post
node ace make:middleware Admin
Directory Structure
app/
├── abilities/
│ └── main.ts # Bouncer abilities (gate-style)
├── controllers/
│ ├── api/
│ │ └── posts_controller.ts
│ └── admin/
│ ├── dashboard_controller.ts
│ └── users_controller.ts
├── middleware/
│ ├── auth_middleware.ts
│ ├── admin_middleware.ts
│ ├── can_middleware.ts
│ └── role_middleware.ts
└── policies/
└── post_policy.ts
start/
├── kernel.ts # Named middleware registry
├── routes.ts # Entry point (imports sub-files)
└── routes/
├── api.ts
├── web.ts
└── admin.ts
Summary
| Concept | AdonisJS equivalent |
|---|
| Route definition | router.get(), router.post(), router.resource() |
| Named routes | .as('posts.index'), route('posts.show', { id }) |
| Route groups | router.group(() => {}).prefix().use() |
| Middleware (global) | Router-level in start/kernel.ts |
| Middleware (named) | router.named({}) in start/kernel.ts |
| Middleware on resources | .use(['store', 'destroy'], middleware.auth()) |
| Gate / ability | Bouncer.ability() → bouncer.authorize(ability) |
| Policy (model-level) | BasePolicy class → bouncer.with(Policy).authorize() |
| Route-level permission | Custom can middleware referencing Bouncer abilities |
| Role check | Simple middleware checking user.role |