Let's Learn Adonis 5: Controllers, Services, Resources, and Namespacing

In this lesson, we'll be hammering down several Adonis topics in one swoop. We'll be covering Controllers, which in turn allow us to utilize Services, Resources, and Namespacing.

Published
Jan 02, 21
Duration
11m 43s

Developer, dog lover, and burrito eater. Currently teaching AdonisJS, a fully featured NodeJS framework, and running Adocasts where I post new lessons weekly. Professionally, I work with JavaScript, .Net C#, and SQL Server.

Adocasts

Burlington, KY

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 actions

  • ProjectsController - for all project-based actions

  • TasksController - 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 app

  • app/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.

  1. Commented 3 years ago
    The paths defined in the resource routes in regards to the :id definitions are a bit confusing, are there typos or don't you need to define the :id for edit and destroy in the controller? The same goes for post and delete, they do not have /store and /destroy in the routes definition (this one I might understand since the POST and DELETE methods tell what it is under the user model, correct?
    0

    Please sign in or sign up for free to reply

    1. Commented 3 years ago
      Hi Ad, (so sorry, I don't have replies set up on this yet). Controllers don't need to know anything about your routes. Whatever :id is passed for the route will be available within the controller via the params object on the HttpContextContract. As for the post and delete, I think you're on the right track! These don't need /store and /destroy on the route URL because the Http Method (POST and DELETE) will determine whether we're storing or deleting. Hope that helps!
      0

      Please sign in or sign up for free to reply