DEV Community

loading...
Cover image for Introducing AdonisJS - Routes & Controllers

Introducing AdonisJS - Routes & Controllers

amanvirk1 profile image Aman Virk ・5 min read

Let's start dipping our toes in the code now by creating some routes and controllers.

For anyone unaware of the term routing. In terms of web development, it is a mapping of URLs and their handlers that you want your app to handle. URLs outside of this mapping will result in a 404.

Defining routes

Routes in AdonisJS are defined inside the start/routes.ts file. Using this file is a convention and not a technical limitation. Let's open the file and replace its contents with the following code snippet.

import Route from '@ioc:Adonis/Core/Route'

Route.get('/', async () => {
  return 'This is the home page'
})

Route.get('/about', async () => {
  return 'This is the about page'
})

Route.get('/projects', async () => {
  return 'This is the page to list projects'
})
Enter fullscreen mode Exit fullscreen mode
  • We begin by importing the Route module.
  • Using the Route.get method, we define a total of 3 routes.
  • A typical route accepts a route pattern and a handler to respond to the requests.
  • In the above example, the handler is an inline function. Later we will look into using controllers as well.
  • Finally, the return value of the function is sent back to the client making the request.

Let's give this code a try by visiting the URLs for the registered routes.

Supported data types

You can return most of the Javascript data types from the route handler and AdonisJS will properly serialize them for you.

Route.get('/', async () => {
  // return 28
  // return new Date()
  // return { hello: 'world' }
  // return [1, 2, 3]
  // return false
  // return '<h1> Hello world </h1>'
})
Enter fullscreen mode Exit fullscreen mode

HTTP Context

Every route handler receives an instance of the HTTP context as the first parameter. The context holds all the information related to the current request, along with the response object to customize the HTTP response.

Route.get('/', async (ctx) => {
  console.log(ctx.inspect())

  return 'handled'
})
Enter fullscreen mode Exit fullscreen mode

Following is the output of the ctx.inspect().

If you are coming from a framework like express, then there is no req and res objects in AdonisJS. Instead you have access to ctx.request and ctx.response.

Also note that the API of request and response is not compatible with express and neither it is a goal for us.

The HTTP context has an extendible API and many AdonisJS packages add their properties to the context. For example: If you install the @adonisjs/auth module, it will add the ctx.auth property.

Using controllers

Controllers in AdonisJS are vanilla ES6 classes stored inside the app/Controllers/Http directory. You can create a new controller by running the following ace command.

node ace make:controller TodoController

# CREATE: app/Controllers/Http/TodosController.ts
Enter fullscreen mode Exit fullscreen mode

Let's open the newly created file and replace its contents with the following code snippet.

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

export default class TodosController {
  public async index(ctx: HttpContextContract) {
    return 'Hello world from the todos controller'
  }
}
Enter fullscreen mode Exit fullscreen mode

How should we now go about using this controller inside our routes file?
Let's begin with zero magic and simply import the controller inside the routes file. Open the start/routes.ts file and add another route using the controller.

import Route from '@ioc:Adonis/Core/Route'
import TodosController from 'App/Controllers/Http/TodosController'

Route.get('/todos', (ctx) => new TodosController().index(ctx))
Enter fullscreen mode Exit fullscreen mode

Visit the http://localhost:3333/todos URL and you will surely see the return value from the controller method.

Lazy loading controllers

Now, imagine an app with 40-50 controllers. Every controller will also have its own set of imports, making the routes file a choke point.

Lazy loading is the perfect solution to the above problem. Instead of importing all the controllers at the top level, we can lazily import them within the route's handler.

import Route from '@ioc:Adonis/Core/Route'

Route.get('/todos', async (ctx) => {
  const TodosController = (await import('App/Controllers/Http/TodosController'))
    .default
  return new TodosController().index(ctx)
})
Enter fullscreen mode Exit fullscreen mode

Now, the TodosController is only loaded when the request for the /todos route comes in. Since the import/require statements are cached in Node.js, you don't have to worry about reading the same file multiple times from the disk.


Are are you happy with the above code?
I am personally not. There is too much boilerplate and you would expect a framework to do a better job here and cleanup things for you and AdonisJS does that.

Replace the previously written route with the following code snippet.

Route.get('/todos', 'TodosController.index')
Enter fullscreen mode Exit fullscreen mode

This is the recommended way of referencing controllers within your routes file.

  • We already know that your controllers are inside the app/Controllers/Http directory and hence there is no need to define the complete path.
  • You only need to define the file name and the method to be called on the exported class.
  • Behind the scenes, AdonisJS will lazily import the controller. Creates an instance of it and executes the referenced method.

What about the type safety?
The verbose implementation has the extra benefit of being type safe. This is something missing when using the string based expression. Or I will say, it is missing for now.

We need two things to achieve type safety when referencing controller.method as a string expression.

  1. The ability to tokenize the expression and create a full path to the controller and its method. This is achievable with Typescript 4.1 beta release. Here is a proof of concept for the same.
  2. Next is the ability to have an Import type with support for generics. There is an open issue for it and I am positive that it will make its way to the Typescript in the future, as it adheres to the Typescript design goals.

To summarize, we bet in the future of Typescript and decided to remove all the extra boilerplate required to reference controllers within the routes file and expose a simple to use API.

Wrap up

Alright, let's wrap up this post. In the next post, we will begin designing the web page for our todo app.

Meanwhile, lemme share some code examples for commonly required tasks that you may perform when creating a web app.

Render views

Render views using the AdonisJS template engine

Route.get('todos', async ({ view }) => {
  return view.render('todos/index', {
    todos: [{ id: 1, title: 'Writing an article', isCompleted: false }],
  })
})
Enter fullscreen mode Exit fullscreen mode

Modify outgoing response

Route.get('/', async ({ response }) => {
  response.redirect('/to/a/url')
  response.status(301)
  response.header('x-powered-by', 'my-app-name')
  response.cookie('foo', 'bar')
})
Enter fullscreen mode Exit fullscreen mode

Stream files from the disk

Route.get('/', async ({ response }) => {
  response.download('/path/to/some/file.txt')
})
Enter fullscreen mode Exit fullscreen mode

Read request data

Route.get('/', async ({ request }) => {
  console.log(request.url())
  console.log(request.method())
  console.log(request.cookie('cookie-name'))

  // request body + query string
  console.log(request.all())

  // get a single file & validate it too
  const avatar = request.file('avatar', {
    size: '2mb',
    extnames: ['jpg', 'png', 'jpeg'],
  })

  // All uploaded files as an object
  console.log(request.allFiles())
})
Enter fullscreen mode Exit fullscreen mode

Discussion

pic
Editor guide