In the last lesson, we went over routes and route handling. In this lesson, we'll be extending upon the last by learning how we can drastically clean up our routes.ts
file by extracting our route handlers off into controllers. Then, once we've learned about controllers we're home free to dive into Services, Resources, and Namespacing.
Controllers
Controllers allow us to take cleanliness within our route definitions a giant leap forward. By using controllers we can extract the route handling (the callback function we're currently passing to our HTTP Method routes) out of our route definition file and into its own topic-based file.
So, for example, we could have the following controllers within a simple project management application.
UsersController
- for all user-based actionsProjectsController
- for all project-based actionsTasksController
- for all task-based actions.
In this lesson, we'll be explicitly focusing on converting the user routes we defined in the last lesson so they utilize controllers.
// routes.ts Route.group(() => { Route.get('/', async ({ view }: HttpContextContract) => { return view.render('users') }).as('index') Route.get('/:id', async ({ view, params }: HttpContextContract) => { return view.render('profile', {}) }).as('show') Route.post('/', async ({ request, response }: HttpContextContract) => { }).as('post') Route.put('/:id', async ({ request, response }: HttpContextContract) => { }).as('update') Route.delete('/:id', async ({ request, response }: HttpContextContract) => { }).as('delete') }).prefix('/users').as('users.')
Copied!
Creating A Controller
First, we'll need to create our UsersController
. To do this, we can head into our terminal and use the Ace CLI to generate a new controller by running:
$ node ace make:controller UserController # CREATE: app/Controllers/Http/UsersController.ts
Copied!
Note here that I'm creating a controller UserController
instead of UsersController
. One nice thing Adonis will do for us is take into account a standard pluralization and singularization of names. Controllers will be pluralized, then later after we add Lucid, Adonis' ORM, to our project we'll see migrations and pluralized and models are singular. Controllers and migrations are pluralized because they're responsible for all entities for the given table. Models are singular because they represent a single entity.
So, when we run the Ace command to create our controller you'll see it creates UsersController
instead of the provided UserController
and it's created at app/Controllers/Http/UsersController.ts
.
If we head into our project and inspect our newly created UsersController
you should see a stubbed UsersController
class and commented out import for our HttpContextContract
.
// UsersController.ts // import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' export default class UsersController { }
Copied!
Populating Our Controller
Now that we have our UsersController
created, we can begin copy/pasting our user route's handlers from our routes.ts
file into our UsersController
.
Firstly, let's begin by uncommenting our HttpContextContract
import in our UsersController
, since we'll be making use of it here.
Next, let's cut our users.index
route handler out of our routes.ts
file and into our UsersController
class.
// routes.ts Route.group(() => { Route.get('/', ).as('index') Route.get('/:id', async ({ view, params }: HttpContextContract) => { return view.render('profile', {}) }).as('show') Route.post('/', async ({ request, response }: HttpContextContract) => { }).as('post') Route.put('/:id', async ({ request, response }: HttpContextContract) => { }).as('update') Route.delete('/:id', async ({ request, response }: HttpContextContract) => { }).as('delete') }).prefix('/users').as('users.')
Copied!
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' export default class UsersController { async ({ view }: HttpContextContract) => { return view.render('users') } }
Copied!
- app
- Controllers
- Http
- UsersController.ts
In addition to just pasting in our users.index
route handler inside our UsersController
, we're also going to need to make it a valid class method by making it public, giving it a name of index
, and removing the arrow from the old arrow function.
We should end up with something like the below:
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' export default class UsersController { public async index({ view }: HttpContextContract) { return view.render('users') } }
Copied!
- app
- Controllers
- Http
- UsersController.ts
Lastly, inside our routes.ts
file we need to tell our users.index
route to use our UsersController.index
method to handle the route. To do this, instead of providing a callback function for the route handler, we can provide the controller and controller method as a string object-reference, like below.
// routes.ts Route.group(() => { Route.get('/', 'UsersController.index').as('index') // other user routes }).prefix('/users').as('users.')
Copied!
Now we just need to do the same thing for the remainder of our user routes.
// routes.ts Route.group(() => { Route.get('/', 'UsersController.index').as('index') Route.get('/:id', 'UsersController.show').as('show') Route.get('/create', 'UsersController.create').as('create') Route.post('/', 'UsersController.store').as('store') Route.get('/:id/edit', 'UsersController.edit').as('edit') Route.put('/:id', 'UsersController.update').as('update') Route.delete('/:id', 'UsersController.destroy').as('destroy') }).prefix('/users').as('users.')
Copied!
Also, make note we're adding two additional route definitions, create and edit.
export default class UsersController { public async index({ view }: HttpContextContract) { return view.render('users') } public async show({ response, params }: HttpContextContract) { return response.json({ userId: params.id }) } public async create(ctx: HttpContextContract) { } public async store(ctx: HttpContextContract) { } public async edit(ctx: HttpContextContract) { } public async update(ctx: HttpContextContract) { } public async destroy(ctx: HttpContextContract) { } }
Copied!
- app
- Controllers
- Http
- UsersController.ts
Services
As we begin utilizing our controllers we may find we need to share some code or that some of our controller methods are getting rather large. This is where services come into play. Services allow us to create reusable methods that we can then use anywhere in our Adonis codebase.
Unlike controllers, there isn't a built-in way to generate a new service using the Ace CLI. Fortunately, they're super simple to create on our own. To start, right inside our app
directory let's create a new directory called Services
. Then inside our app/Services
directory let's create a file called UsersService.ts
.
Inside our UsersService.ts
we can create and export a UsersService
class.
class UsersService { } export default UsersService
Copied!
- app
- Services
- UsersService.ts
Beyond that, we can use our new UsersService
class however we wish.
To show you how to use a service inside a controller, let's add a test method to our UsersService
that returns back a string. I'll be making my method static
so that I don't need to create an instance of the UsersService
class to access my method.
class UsersService { public static test(): string { return 'working' } } export default UsersService
Copied!
- app
- Services
- UsersService.ts
Now let's use our UsersService.test
method in our UsersController
.
import UsersService from 'App/Services/UsersService' export default class UsersController { // index ... public async show({ response, params }: HttpContextContract) { const test = UsersService.test(); return response.json({ userId: test }) } // other methods ... }
Copied!
- app
- Controllers
- Http
- UsersController.ts
Resources
Resources allow us to define resource routes in one call by defining the resource path, then the controller that will handle the resource. Resources are a great way to further clean up our route definitions.
What Are Resource Routes?
Resource routes are routes that you'd typically define for a given model. They're a combination of routes to list, show, create, store, edit, update, and destroy.
/model
- to list the model's records/model/:id
- to display a single model record/model/create
- to display create a form for a new model record/model/store
- to persist the values from the create form as a new model record/model/edit
- to display edit form for a model record/model/:id/update
- to persist the updated values from the edit form for an existing model record/model/destroy
- to delete a model record
Does this look familiar? If you take a look at your routes.ts
file, you'll see we manually defined all routes for our user resource.
Defining Resource Routes
When it comes to resource routes, we don't need to manually define them as we did with our users. We can cut all our user route definitions down to a single line by using the resource
method to define all routes in one go.
// routes.ts //Route.group(() => { // // Route.get('/', 'UsersController.index').as('index') // // Route.get('/:id', 'UsersController.show').as('show') // // Route.get('/create', 'UsersController.create').as('create') // // Route.post('/', 'UsersController.store').as('store') // // Route.get('/:id/edit', 'UsersController.edit').as('edit') // // Route.put('/:id', 'UsersController.update').as('update') // // Route.delete('/:id', 'UsersController.destroy').as('destroy') // //}).prefix('/users').as('users.') Route.resource('users', 'UsersController')
Copied!
With this, both the commented-out routes and the routes defined by our user's resource are the same.
Eliminating Certain Routes from a Resource
Sometimes you want to utilize the shorthand syntax resources provide, but you don't need all the routes a resource provides. Adonis provides two methods we can chain off our resource definition to either blacklist or whitelist routes for our resource.
Route.resource('users', 'UsersController').only(['index', 'show'])
Copied!
By chaining off our resource the only
method, we can provide an array of resource routes that should only be defined for the resource. So in the above example, our resource will only define routes for the index
and show
routes.
Route.resource('users', 'UsersController').except(['destroy'])
Copied!
Inversely, if we chain the except
method off our resource we can provide an array of routes we don't want to be defined for our resource. So, in the above example, our resource will define all resource routes except destroy.
Namespacing
Namespacing allows us to segregate controllers into modules. Essentially, a namespace is a separate directory for a group of controllers. For example, if our site has a public and admin section to it, we wouldn't want logic for the two sections to be intertwined because it'd become too confusing and easy to mix up what's what.
We can use namespacing to create an Admin
namespace that contains all administrative controllers, then we can use the default namespace for all non-admin controllers.
Creating A Namespace
You can structure your namespaces however you wish. I prefer to create new namespaces within my Http
directory, but some people separate their namespaces all the way out in app.
app/[namespace]/Controllers/Http
- creating namespaces all the way out in appapp/Controllers/Http/[namespace]
- creating namespaces in Http.
If you plan on using WebSockets the former might make more sense than the latter.
So, in order to create a new namespace all we need to do is create a new directory for our namespaced controllers to reside, then tell those routes to use the newly created namespace. To tell our route to use a non-default namespace we need to use the namespace
method and pass in the path to our namespace.
export default class UsersController { public async index({ response }: HttpContextContract) { return response.json({ isAdminNamespace: true }) } }
Copied!
- app
- Controllers
- Http
- Admin
- UsersController.ts
// routes.ts Route.group(() => { Route.get('/', 'UsersController.index').as('index') Route.get('/:id', 'UsersController.show').as('show') Route.get('/create', 'UsersController.create').as('create') Route.post('/', 'UsersController.store').as('store') Route.get('/:id/edit', 'UsersController.edit').as('edit') Route.put('/:id', 'UsersController.update').as('update') Route.delete('/:id', 'UsersController.destroy').as('destroy') }).prefix('/users').as('users.') Route.get('/admin/users', 'UsersController.index') .as('admin.users.index') .namespace('App/Controllers/Http/Admin')
Copied!
With this, we've now created an admin namespace containing a user's index page and registered a route for it at /admin/users
.
Next Up
Now that we understand routing and how to handle routes with controllers we can move onto database integration. In the next lesson, we'll be installing and integrating Lucid, Adonis' ORM, into our project.
Join The Discussion! (2 Comments)
Please sign in or sign up for free to join in on the dicussion.
ad
Please sign in or sign up for free to reply
tomgobich
Please sign in or sign up for free to reply