Introducing AdonisJS - Routes & Controllers
Tags: node, adonisjs
Published: 2020-11-13 08:32:08

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.

```ts
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'
})
```

- 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.

![](https://res.cloudinary.com/adonis-js/image/upload/v1605247138/devto/routes-demo_gm4wck.gif)

### Supported data types

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

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

### 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.

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

  return 'handled'
})
```

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

![](https://res.cloudinary.com/adonis-js/image/upload/v1605247676/devto/ctx-inspect.png)

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](https://preview.adonisjs.com/guides/auth/introduction) 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.

```sh
node ace make:controller TodoController

# CREATE: app/Controllers/Http/TodosController.ts
```

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

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

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

---

**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.

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

Route.get('/todos', (ctx) => new TodosController().index(ctx))
```

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.

```ts
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)
})
```

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.

```ts
Route.get('/todos', 'TodosController.index')
```

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](https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#template-literal-types). Here is a [proof of concept](https://www.typescriptlang.org/play?ts=4.1.0-beta#code/MYewdgzgLgBASiArlApjAvDA3gKBjAcxSgB4AJAQzABMAbFAJxhQA9UaIZoGBLMAgHwAKAA4UoqBmABcXKL34AaGAAsqdRrMo16DAJSyY2jU1btqnAAYASLHwBmjGAEEAvgDpbDpwCFXlmAB+bDx8GFAweRBaXVlLZxERAHoAYXAomMYIJLIJZNs3S0VQ-ABbYhUQalkfUNcYWUQwAGswEAB3MBxXHF6kpKMQADcnYacoFTQAIgYkVCmYIYpeCgAjehh1LhQ0CfEYdrRlo-XdkBgxBggjuQUCGD4oc6fmlEgcCOgYWeQ0TARfu4iFAhAByJKg5SgsggcppSKzTIMdx8aisUF6IA) for the same.
2. Next is the ability to have an `Import` type with support for generics. There is an [open issue for it](https://github.com/microsoft/TypeScript/issues/31090) 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](https://preview.adonisjs.com/guides/views/introduction)

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

### Modify outgoing response

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

### Stream files from the disk

```ts
Route.get('/', async ({ response }) => {
  response.download('/path/to/some/file.txt')
})
```

### Read request data

```ts
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())
})
```