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.
export default class PostsController { }
Copied!
- app
- Controllers
- Http
- PostsController.ts
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') })
Copied!
We'd move the route handler from the route definition
Route.get('/posts', /* Goodbye route handler */)
Copied!
To the PostsController
export default class PostsController { public async index({ view }) { return view.render('posts/index') } }
Copied!
- app
- Controllers
- Http
- PostsController.ts
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')
Copied!
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
Copied!
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
Copied!
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 postscreate
- Displaying the post create pagestore
- Creating a post record within the databaseshow
- Displaying a postedit
- Displaying the post update pageupdate
- Updating a post record within the databasedestroy
- 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
Copied!
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') })
Copied!
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 { }
Copied!
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) { } }
Copied!
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
Copied!
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')
Copied!
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!
Join The Discussion! (0 Comments)
Please sign in or sign up for free to join in on the dicussion.
Be the first to Comment!