Unread Notifications

Latest Notifications

No Notifications

You're all set! Start a discussion by leaving a comment on a lesson or replying to an existing comment.

Let's Learn AdonisJS 5

Handling Routes with Controllers

7 MIN READ
2 MONTHS AGO

In this lesson, we'll learn about Controllers and how we can keep our route definitions clean and easy to scan by extracting our route handlers into these Controllers.

Watch on YouTube

Now that we have the AdonisJS Router pretty well covered, let's go ahead and move into cleaning up our route file(s) by extracting our route definition's route handlers out of the route definition itself and into what's called a controller.

What is a Controller

Controllers are defined as a JavaScript Class that's the default export of its file. By default within AdonisJS controllers reside within the app/Controllers/Http directory. A specific Http directory exists because we can have many different types of controllers, like Websocket controllers. The Http notes that these controllers are specifically meant for HTTP requests.

// app/Controllers/Http/PostsController.ts

export default class PostsController {
}

Within AdonisJS it's common to have a Controller per resource, for example for a Post model we'd have a PostsController, for a Series model we'd have a SeriesController, and for a Topic model we'd have a TopicsController.

Then, instead of defining the route handler directly within the route definition, we'd instead define the route handler as a method on the Controller Class applicable to that route.

So, for the following route definition

Route.get('/posts', async ({ view }) {
  return view.render('posts/index')
})

We'd move the route handler from the route definition

Route.get('/posts', /* Goodbye route handler */)

To the PostsController

// app/Controllers/Http/PostsController.ts

export default class PostsController {
  public async index({ view }) {
    return view.render('posts/index')
  }
}

Then, to tell the route definition to use the PostsController and specifically, it's index method, we'd provide an object path string as the second argument in our route definition instead of the handler itself.

Route.get('/posts', 'PostsController.index')

This then keeps your route definitions clean, easy to scan, and more importantly easy to maintain.

Creating A Controller

When it comes to creating a controller, like most things in AdonisJS we have two options. We can create the file ourselves or we can use the Ace CLI. I always prefer to create my controllers, and really everything I can, using the Ace CLI because it performs some automated normalizations we'll talk about here momentarily.

First, let's take a look at the Ace CLI's help info on creating a Controller.

node ace make:controller --help
-------------------------------
Make a new HTTP controller


Usage: make:controller <name>


Arguments
  name              Name of the controller class


Flags
  -r, --resource    Add resourceful methods to the controller class
  -e, --exact       Create the controller with the exact name as provid

So, as we can see the Ace CLI accepts one argument to the make:controller command and that's the controller's name. We also have two flags available as well, --resource and --exact.

Based off the above, in order to create our posts controller, we'd want to run the following

node ace make:controller Post
-----------------------------
CREATE: app/Controllers/Http/PostsController.ts

Note that we provided the name Post but AdonisJS created a file called PostsController.ts. This is very intentional, and we'll get into this when we discuss the --exact flag.

Resource Flag

We haven't discussed resources yet, and we'll be doing so in-depth in the next lesson. However, to give you an overview, a resource is a concept defined by the REST architecture. It's a standard group of routes and route handlers meant to perform CRUD (create, read, update, delete) operations on a given database table, also known as the resource. So, if we had defined a resource for a Post model, we'd have routes and route handlers for the following:

  • index - Getting all posts

  • create - Displaying the post create page

  • store - Creating a post record within the database

  • show - Displaying a post

  • edit - Displaying the post update page

  • update - Updating a post record within the database

  • destroy - Deleting a post record within the database

So, by adding the --resource or -r flag when creating a Controller with the Ace CLI, your Controller will be created with a method for each of the above.

Exact Flag

One thing AdonisJS does for you so you don't have to think about it is normalizing the names when creating files with the Ace CLI. These include keeping Models singular since they represent a single instance of a record (we'll get into that in the Lucid module) and keeping Controllers plural since they're in charge of managing all the records for the resource.

When we created our PostsController above we provided the name Post to the Ace CLI make:controller command but were created a file called PostsController. That's a prime example of AdonisJS normalizing your filenames for you so you don't have to worry about it. If, however, you have a specific use-case where you don't want this or if you have a different preference for your project, the --exact flag provides you a way to override this default normalization behavior. For example, if we run our node ace make:controller Post command again this time using the --exact flag, you'll see our file is created exactly as we provided it.

node ace make:controller Post
-----------------------------
CREATE: app/Controllers/Http/Post.ts

TypeScript and the HttpContext

Now, one thing you may notice is that AdonisJS provides a commented-out line importing the type HttpContextContract when you create a new non-resource controller. This is the TypeScript type for our HttpContext object. When we define our route handlers directly in the route definition AdonisJS has the ability to provide the type for the HttpContext object that's provided as the argument for our callback.

//                            ↓ AdonisJS can provide the type automatically
Route.get('/posts', async ({ view }) => {
  return view.render('posts/index')
})

When we instead use Controllers, AdonisJS doesn't really have a way to provide that type for our Controller method arguments. A workaround for this is to import the type and set the type for the argument. AdonisJS does this for you automatically when you create a resource Controller. When you create a normal controller, AdonisJS will import the type for you and comment it out since it's not currently in use.

Default Controllers

When you create a new controller without the resource flag, this is what AdonisJS will start you with. Note the HttpContextContract import that's commented out for convenience.

// import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class PostsController {
}

Then, when you create a new controller with the resource flag, this is what AdonisJS will start you with. Note the HttpContextContract is imported and set as the type for each of the method's parameters.

import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class PostsController {
  public async index ({}: HttpContextContract) {
  }


  public async create ({}: HttpContextContract) {
  }


  public async store ({}: HttpContextContract) {
  }


  public async show ({}: HttpContextContract) {
  }


  public async edit ({}: HttpContextContract) {
  }


  public async update ({}: HttpContextContract) {
  }


  public async destroy ({}: HttpContextContract) {
  }
}

Namespacing Controllers and Routes

It's common, especially on projects working with posts, to have an administrative portion of the application where public access is limited and those with access have heightened powers when it comes to managing items. When these scenarios occur within an application usually a good idea to separate the functionality that’s handling the limited powers versus the heightened powers so the two don't get mixed up. When working with Controllers, we can do this by having separate Controllers for the two.

Now, we could just create another controller for our heightened powers called AdminPostsController, but that becomes hard to group when it comes to routes, middleware, and permission checking. Instead, we can utilize something known as Namespacing.

What is Namespacing?

Namespacing is a way to group files and code based on permissions, functionality, or really any other reason you'd need to group files and code together. In the case of Controllers in AdonisJS, Namespacing can best be visualized as a folder. We could say our HTTP Controllers are namespaced to App/Controllers/Http. We could say our Websocket controllers (we don't have any, but roll with me here) are namespaced to App/Controllers/Websocket.

Creating A Controller Namespace

First, we'll want to create the folder path for the Namespace we want to create. This folder path can be whatever you'd like. Some common usages are:

  • App/{namespace}/Controllers/Http

  • App/Controllers/Http/{namespace}

  • App/Modules/{namespace}/Controllers/Http

If you go with the second approach, we can actually utilize the Ace CLI to create the folder path for us. So, say we want a new PostsController within the Namespace Admin we could run the following Ace CLI command to create that Namespace and Controller.

node ace make:controller Admin/Post --resource
----------------------------------------------
CREATE: app/Controllers/Http/Admin/PostsController.ts

This then creates our PostsController within app/Controllers/Http/Admin.

Pointing A Route To A Namespace

Now that we've created our Namespace and a Controller for our Namespace, we're ready to define a route that'll actually use this new Namespace. Thankfully, AdonisJS provides a method we can chain off our route definition called namespace that we can use to define the Namespace to use for the Controller.

Route.group(() => {


  // this will use the controller at:
  // app/Controllers/Http/Admin/PostsController.ts
  Route.get('/posts', 'PostsController.index')

  // ↓ informs all inner routes to use controllers defined here
}).namespace('App/Controllers/Http/Admin').prefix('admin')

Next Up

Now that we've got our route definitions all cleaned up and using Controllers and we're already starting to discuss resources, now would be a great time to dive into resources further and fully learn them. So, in the next lesson, we'll be doing specifically that!

Comment

Prepared By

Tom Gobich

Burlington, KY

Owner of Adocasts, JavaScript developer, educator, PlayStation gamer, burrito eater.

Visit Website