Let's Learn Adonis 5: Routes and Route Handling

In this lesson, we learn the basics of Adonis routing and route handling by covering how to define a route, render a page, respond with JSON, simplify our route definitions, and more.

Published
Dec 26, 20
Duration
11m 42s

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 this lesson, we're going to be focusing on the basics of Adonis routing. So, in the last lesson, we learned that we have a start directory that contains a routes.ts file where our route definitions will reside. When we create a new project we're given a starter route that renders our welcome page.

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

Route.on('/').render('welcome')
Copied!

Note that the example route is calling a method called on, which is given a path pattern of /. This on method is a shorthand method that will allow us to quickly render a page without doing any business logic or additional handling.

Chained off the on method is the render method, which accepts a view name from our resource/views directory. The provided view name will then be used to render that view for the defined path pattern.

The render method is only chainable off on, since on is a shorthand route definition that doesn't allow route handling.

HTTP Methods

In most cases, you're going to have some business logic to perform for your routes. This is where HTTP Methods come into play. HTTP Methods allow us to define routes for particular actions.

  • GET - Used to fetch data or render a page.

  • POST - Used to store new data or dispatch data to the server.

  • PUT/PATCH - Used to update existing data.

  • DELETE - Used to delete existing data.

Using these HTTP Methods when defining our routes allows us to keep our route paths clean and concise because we can utilize the same path for each method. We can have a route for GET /users, POST /users, PUT /users, and DELETE /users and the server will understand which route to use based on the HTTP Method the request was sent with.

HTTP Methods Route Definitions

With Adonis, we have access to define the HTTP Method for our route directly off the Route module. Unlike the on method, the HTTP Methods all require some form of route handler for each route. For example, if we wanted to replicate the default welcome route using the get method, we can do so like this:

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

Route.get('/', async ({ view }: HttpContextContract) => {
  return view.render('welcome')
})
Copied!

The first argument is still the path pattern, but instead of chaining render off get we instead pass in our route handler callback function. Our callback function is given an argument of type HttpContextContract, which contains properties like view, request, response, params, etc. We can use the view property to then access the render method.

This is the structure for all our route HTTP Methods; get, post, put, patch, and delete.

Dynamic Routing with Route Parameters

Dynamic routes allow us to use the same route for different data, think a blog post or user profile page. We can define a dynamic route in Adonis by adding a colon before our route parameter name. For example, if we want to render a user profile page, we could define a route with a route parameter of the user's id. We can then use the params property off our HttpContextContract to access our route parameter.

Route.get('/users/:id', async ({ view, params }: HttpContextContract) => {
  const userId = params.id;
	
  const user = // TODO: get the user from the db

  return view.render('profile', { user })
})
Copied!

Note here that the route parameter name (/:id) and the key the value is stored within on the params object is a one-to-one relationship. They'll always match.

Optional Route Parameter

We can make the route parameter optional by adding a question mark behind it. If we have a route parameter value the value will be on the params object. If we don't have a parameter value the value won't be on the params object.

Route.get('/users/:id?', async ({ view, params }: HttpContextContract) => {
  const userId = params.id;
  let user = null;

  if (userId) {
    user = // TODO: get the user from the db
  }

  return view.render('profile', { user })
})
Copied!

By making the route parameter id optional this route will handle requests for both /users and /users/1, where 1 is a user's id.

Naming Routes

Naming routes is a powerful tool that allows us to reference a route without statically mentioning its path. To give a route a name all we need to do is chain the as method of the route and provide the as method a name string.

Route.get('/users', async ({ view }) => {/*  */}).as('users');
Route.get('/users/:id', async ({ view }) => {/*  */}).as('users.show');
Copied!

A route's name can be formatted to your liking; snake-case, camel-case, etc. The dot syntax is something I've personally always used.

Sending A JSON Response

So, we know we can use view.render('name') to render out a view, and sending a JSON response isn't much different. Instead of the view property, we'll want to use the response property and on the response property, we have access to both a json and jsonp method. Using one of these two methods we can either send a JSON or JSONP response.

Route.get('/', async ({ response }: HttpContextContract) => {
  return response.json({ hello: 'world' });
})
Copied!

Grouping

Let's say we have a number of routes whose paths all start the exact same, like user resource routes.

Route.get('/users', async ({ view }: HttpContextContract) => {
  return view.render('users')
}).as('users.index')

Route.get('/users/:id', async ({ view, params }: HttpContextContract) => {
  return view.render('profile', {})
}).as('users.show')

Route.post('/users', async ({ request, response }: HttpContextContract) => {

}).as('users.post')

Route.put('/users/:id', async ({ request, response }: HttpContextContract) => {

}).as('users.update')

Route.delete('/users/:id', async ({ request, response }: HttpContextContract) => {

}).as('users.delete')
Copied!

It can become pretty repetitive to constantly repeat the shared definitions over and over, especially if we have a lot of routes. To simplify this we can use route grouping to group like-routes. A group can be used to apply a name prefix, a route prefix, middleware, subdomain, and even namespace to a number of routes at the same time.

For example, we can simplify the above to just:

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!

Here we're prefixing all routes within the group with the path prefix of /users and name prefix of users..

Order Matters

When defining our routes it's important to keep in the back of our minds that the order of our routes does matter. Adonis will use the first route definition, from the top down, it finds that matches the current request.

This most notably comes into play when using optional route parameters, since the route can also be used for the path with the parameter omitted.

For example, if we make a request for the page at /users with the below route definitions our request will stop at the /users/:id? definition since the route parameter is optional. It's the first defined route that matches our request, so it's the one that'll be used.

Route.get('/users/:id?', async ({ view, params }: HttpContextContract) => {
  return view.render('profile', {})
}).as('users.show')

Route.get('/users', async ({ view }: HttpContextContract) => {
  return view.render('users')
}).as('users.index')
Copied!

Next Up

Right now you might be thinking, "gosh, defining and handling all my routes in this routes.ts file is going to get massive!" Fear not, in the next lesson we'll be massively simplifying our routes.ts file by extracting our route definitions out into Controllers. Using Controllers, our route definitions will shrink down to about a single line per-definition.

Join The Discussion! (0 Comments)

Please sign in or sign up for free to join in on the dicussion.

robot comment bubble

Be the first to Comment!