Controllers

Controllers

HTTP controllers offer an abstraction layer to organize the route handlers inside dedicated files. Instead of expressing all the request handling logic within the routes file, you move it to controller classes.

The controllers are stored within the ./app/controllers directory, representing each controller as a plain JavaScript class. You may create a new controller by running the following command.

See also: Make controller command

node ace make:controller users

A newly created controller is scaffolded with the class declaration, and you may manually create methods inside it. For this example, let's create an index method and return an array of users.

import { HttpContext } from '@adonisjs/core/http'
export default class UsersController {
async index(ctx: HttpContext) {
return [
{
id: 1,
username: 'virk',
},
{
id: 2,
username: 'romain',
},
]
}
}

Finally, let's bind this controller to a route. We will import the controller using the #controllers alias. The aliases are defined using subpath imports feature of Node.js.

start/routes.ts
import router from '@adonisjs/core/services/router'
import UsersController from '#controllers/users_controller'
router.get('users', [UsersController, 'index'])

As you might have noticed, we do not create an instance of the controller class and instead pass it directly to the route. Doing so allows AdonisJS to:

  • Create a fresh instance of the controller for each request.
  • And also construct the class using the IoC container, which allows you to leverage automatic dependency injection.

If you want, you can manually create an instance of the controller and execute the method. However, it is not recommended because you will write more boilerplate code and lose the IoC container benefits.

// 🫤 Naah
router.get('users', (ctx) => {
return new UsersController().index(ctx)
})

Lazy loading controllers

As your codebase grows, you will notice it starts impacting the boot time of your application. A common reason for that is importing all controllers inside the routes file.

Since controllers handle HTTP requests, they often import other modules like models, validators, or third-party packages. As a result, your routes file becomes this central point of importing the entire codebase. To prevent this from happening, we recommend lazy loading the controllers.

Lazy loading is as simple as moving the import statement behind a function and using dynamic imports.

You can use our ESLint plugin to enforce and automatically convert standard controller imports to lazy dynamic imports.

import router from '@adonisjs/core/services/router'
import UsersController from '#controllers/users_controller'
const UsersController = () => import('#controllers/users_controller')
router.get('users', [UsersController, 'index'])

Using magic strings

Another way of lazy loading the controllers is to reference the controller and its method as a string. We call it a magic string because the string itself has no meaning, and it's just the router uses it to look up the controller and imports it behind the scenes.

In the following example, we do not have any import statements within the routes file, and we bind the controller import path + method as a string to the route.

import router from '@adonisjs/core/services/router'
router.get('users', '#controllers/users_controller.index')

The only downside of magic strings is they are not type-safe. If you make a typo in the import path, your editor will not give you any feedback.

On the upside, magic strings can clean up all the visual clutter inside your routes file because of the import statements.

Using magic strings is subjective, and you can decide if you want to use them personally or as a team.

Dependency injection

The controller classes are instantiated using the IoC container; therefore, you can type-hint dependencies inside the controller constructor or a controller method.

Given you have a UserService class, you can inject an instance of it inside the controller as follows.

export default class UserService {
async all() {
// return users from db
}
}
import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
import UserService from '#services/user_service'
@inject()
export default class UsersController {
constructor(protected userService: UserService) {}
index() {
return this.userService.all()
}
}

Method injection

You can inject an instance of UserService directly inside the controller method using method injection. In this case, you must apply the @inject decorator on the method name.

The first parameter passed to the controller method is always the HttpContext. Therefore, you must type-hint the UserService as the second parameter.

import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
import UserService from '#services/user_service'
export default class UsersController {
@inject()
index(ctx: HttpContext, userService: UserService) {
return userService.all()
}
}

Tree of dependencies

Automatic resolution of dependencies is not only limited to the controller. Any class injected inside the controller can also type-hint dependencies, and the IoC container will construct the tree of dependencies for you.

For example, let's modify the UserService class to accept an instance of the HttpContext as a constructor dependency.

import { inject } from '@adonisjs/core'
import { HttpContext } from '@adonisjs/core/http'
@inject()
export default class UserService {
constructor(protected ctx: HttpContext) {}
async all() {
console.log(this.ctx.auth.user)
// return users from db
}
}

After this change, the UserService will automatically receive an instance of the HttpContext class. Also, no changes are required in the controller.

Resource-driven controllers

For conventional RESTful applications, a controller should only be designed to manage a single resource. A resource is usually an entity in your application like a User resource or a Post resource.

Let's take the example of a Post resource and define the endpoints to handle its CRUD operations. We will start by creating a controller first.

You may create a controller for a resource using the make:controller ace command. The --resource flag scaffolds the controller with the following methods.

node ace make:controller posts --resource
import { HttpContext } from '@adonisjs/core/http'
export default class PostsController {
/**
* Return list of all posts or paginate through
* them
*/
async index({}: HttpContext) {}
/**
* Render the form to create a new post.
*
* Not needed if you are creating an API server.
*/
async create({}: HttpContext) {}
/**
* Handle form submission to create a new post
*/
async store({ request }: HttpContext) {}
/**
* Display a single post by id.
*/
async show({ params }: HttpContext) {}
/**
* Render the form to edit an existing post by its id.
*
* Not needed if you are creating an API server.
*/
async edit({ params }: HttpContext) {}
/**
* Handle the form submission to update a specific post by id
*/
async update({ params, request }: HttpContext) {}
/**
* Handle the form submission to delete a specific post by id.
*/
async destroy({ params }: HttpContext) {}
}

Next, let's bind the PostsController to a resourceful route using the router.resource method. The method accepts the resource name as the first argument and the controller reference as the second argument.

import router from '@adonisjs/core/services/router'
const PostsController = () => import('#controllers/posts_controller')
router.resource('posts', PostsController)

Following is the list of routes registered by the resource method. You can view this list by running node ace list:routes command.

Nested resources

Nested resources can be created by specifying the parent and the child resource name separated using the dot . notation.

In the following example, we create routes for the comments resource nested under the posts resource.

router.resource('posts.comments', CommentsController)

Shallow resources

When using nested resources, the routes for the child resource are always prefixed with the parent resource name and its id. For example:

  • The /posts/:post_id/comments route displays a list of all the comments for a given post.
  • And, the /posts/:post_id/comments/:id route displays a single comment by its id.

The existence of /posts/:post_id in the second route is irrelevant, as you can look up the comment by its id.

A shallow resource registers its routes by keeping the URL structure flat (wherever possible). This time, let's register the posts.comments as a shallow resource.

router.shallowResource('posts.comments', CommentsController)

Naming resource routes

The routes created using the router.resource method are named after the resource name and the controller action. First, we convert the resource name to snake case and concatenate the action name using the dot . separator.

ResourceAction nameRoute name
postsindexposts.index
userPhotosindexuser_photos.index
group-attributesshowgroup_attributes.index

You can rename the prefix for all the routes using the resource.as method. In the following example, we rename the group_attributes.index route name to attributes.index.

router.resource('group-attributes', GroupAttributesController).as('attributes')

The prefix given to the resource.as method is transformed to snake_ case. If you want, you can turn off the transformation, as shown below.

router.resource('group-attributes', GroupAttributesController).as('groupAttributes', false)

Registering API only routes

When creating an API server, the forms to create and update a resource are rendered by a front-end client or a mobile app. Therefore, creating routes for these endpoints is redundant.

You can use the resource.apiOnly method to remove the create and the edit routes. As a result, only five routes will be created.

router.resource('posts', PostsController).apiOnly()

Registering only specific routes

To register only specific routes, you may use the resource.only or the resource.except methods.

The resource.only method accepts an array of action names and removes all other routes except those mentioned. In the following example, only the routes for the index, store, and destroy actions will be registered.

router
.resource('posts', PostsController)
.only(['index', 'store', 'destroy'])

The resource.except method is the opposite of the only method, removing all the routes except the mentioned one's.

router
.resource('posts', PostsController)
.except(['destroy'])

Renaming resource params

The routes generated by the router.resource method use id for the param name. For example, GET /posts/:id to view a single post, and DELETE /post/:id to delete the post.

You can rename the param from id to something else using the resource.params method.

router.resource('posts', PostsController).params({
posts: 'post',
})

The above change will generate the following routes (showing partial list).

HTTP methodRouteController method
GET/posts/:postshow
GET/posts/:post/editedit
PUT/posts/:postupdate
DELETE/posts/:postdestroy

You can also rename params when using nested resources.

router.resource('posts.comments', PostsController).params({
posts: 'post',
comments: 'comment',
})

Assigning middleware to resource routes

You may assign middleware to routes register by a resource using the resource.use method. The method accepts an array of action names and the middleware to assign to them. For example:

import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router
.resource('posts')
.use(
['create', 'store', 'update', 'destroy'],
middleware.auth()
)

You may use the wildcard (*) keyword to assign a middleware to all the routes.

router
.resource('posts')
.use('*', middleware.auth())

Finally, you may call the .use method multiple times to assign multiple middleware. For example:

router
.resource('posts')
.use(
['create', 'store', 'update', 'destroy'],
middleware.auth()
)
.use(
['update', 'destroy'],
middleware.someMiddleware()
)