Let's Learn Adonis 5 Routing Lesson 2.5

Generating URLs and Signed URLs

tomgobich avatar
tomgobich
8 min
Quick Summary

In this lesson, we'll be focusing specifically on generating URLs from our route definitions. We'll cover generating generic URLs, signed URLs, redirect URLs, and more.

As we referred to when we covered naming routes a couple of lessons ago, Adonis allows us to generate URLs for our route definitions. By generating routes instead of manually defining URLs where we need them we can help our future selves by making things easier to refactor. So, in this lesson, we're going to be learning about all the different ways we can generate a URL in Adonis.

MakeUrl

The Route module has a method on it called makeUrl. We can use this method anywhere within our routes, controllers, services, middleware, etc to dynamically generate a URL for a route definition.

The first argument is the route identifier which can either be the route path, route name, or a controller identifier.

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

Route.get('/posts', () => 'get all posts').as('posts.index')

// using the name as the route identifier
const postRoute1 = Route.makeUrl('posts.index')

// using the path as the route identifier
const postRoute2 = Route.makeUrl('/posts')

// using the controller identity as the route identifier
// we haven't covered controllers yet, so don't get caught up on this
const postRoute3 = Route.makeUrl('PostsController.index')

console.log({
  postRoute1, // /posts
  postRoute2, // /posts
  postRoute3  // /posts
})

The second argument can either be route parameters if the route needs them or additional options.

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

Route.get('/posts', () => 'get all posts').as('posts.index')
Route.get('/posts/:id', () => 'get single post').as('posts.show')

// addition options as 2nd argument
const postIndexUrl = Route.makeUrl('posts.index', { 
  qs: {
    test: 'this-is-a-query-string'
  },
  prefixUrl: '<http://localhost:3333>'
})

// route parameter as 2nd argument as object
const postShow1Url = Route.makeUrl('posts.show', { id: 1 })

// route parameter as 2nd argument as array
const postShow2Url = Route.makeUrl('posts.show', [2])

// additional options as 3rd argument
const postShow3Url = Route.makeUrl('posts.show', { id: 3 }, {
  prefixUrl: '<http://localhost:3333>'
})

console.log({
  postIndexUrl, // <http://localhost:3333/posts?test=this-is-a-query-string>
  postShow1Url, // /posts/1
  postShow2Url, // /posts/2
  postShow3Url  // <http://localhost:3333/posts/3>
})

In the first example here, our route doesn't have any route parameters, so we can jump straight to providing additional options. First, we're providing qs. This allows us to add query string items onto our final URL. On our final URL, our qs here will suffix our URL with ?test=this-is-a-query-string. Then, the prefixUrl will do exactly as the property name suggests, prefix our URL with whatever is provided.

In the second and third examples, we're generating a route that requires a route parameter so we're provided the route parameter value as the second argument. One is using an object and the other is using an array. The object matches the route parameter name to the object key name and gives it that key's value. The array will assign based on ordering. So, index zero will be assigned to the first route parameter to appear in the route's path, index one as the second, and so on.

The third argument is only applicable when we need to provide additional options, but also need to provide route parameters.

This argument structure will also be applicable to most of the other URL generation methods we'll be covering.

URL Builder

Alternatively, if builders are more your preference, there's a method called builder on the Route module we can use to build out our route URL. To use the builder we'll call Route.builder(). The builder method will then return back the URL builder allowing us to chain off the builder to set things like our route parameters, query strings, and prefixes.

The main thing to note here is that we provide the make method within the builder, our route identifier and make will initiate the build of our URL and return our final URL. So, we'll need to ensure we always call make last within our chain.

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

Route.get('/posts', () => 'get all posts').as('posts.index')
Route.get('/posts/:id', () => 'get single post').as('posts.show')

const postIndexUrl = Route.builder()
  .qs({ test: 'this-is-a-query-string' })
  .prefixUrl('<http://localhost:3333>')
  .make('posts.index')

const postShow1Url = Route.builder()
  .params({ id: 1})
  .make('posts.show')

const postShow2Url = Route.builder()
  .params([2])
  .make('posts.show')

console.log({
  postIndexUrl, // <http://localhost:3333/posts?test=this-is-a-query-string>
  postShow1Url, // /posts/1
  postShow2Url, // /posts/2
})

Here, for our posts.index URL builder, we're calling qs to add a query string. This, again, accepts an object that Adonis will convert into a query string for us. Then we're calling prefixUrl which we can use to prefix our final URL with any string. Lastly, we're calling make and providing it our posts.index route identifier.

For our two [posts.show](<http://posts.show>) URL builders, we're just specifying the route parameters using the params method. Then, we're calling make and providing our posts.show route identifier. Note that here params accepts either an array of parameter values or an object, which works the same as our makeUrl method.

Signed URLs

Adonis also makes it super easy to generate signed URLs. Signed URLs are URLs that have a hash signature attached to them via a query string. This signature can then be checked when the URL is used to verify the URL hasn't been altered or tampered with in any way.

An example use-case for a signed URL would be for a password reset link or an email verification link. We can generate a signed URL that expires an hour after it's generated and kick that off to the user. The user then will have one hour to use the signed URL to reset their password.

Since signed URLs include hashing and unhashing a value, we'll need to make sure we have our hashing library installed. So, to ensure it's installed let's run the following in our terminal before moving forward.

npm i phc-argon2

MakeSignedUrl

Just like we have makeUrl, the Route module also has a method called makeSignedUrl, which we can use to generate a URL for our route definition and also add a hashed signature to it. The makeSignedUrl accepts all the same options as makeUrl, but has two additional options specific to the signature, expiresIn and purpose.

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

Route.get('/reset-password/:email', () => {
  // TODO
}).as('reset.password')

const resetPasswordUrl = Route.makeSignedUrl('reset.password', { 
  email: 'test@test.com' 
}, {
  expiresIn: '1h'
})

console.log({
  resetPasswordUrl // /reset-password/test@test.com?signature=ij5s2KjSDFlIjoiL3ZlcmlmeS9mb29AYmFyLmNvbSJ9.LDIX-SsDxu_E4O0sJxeAhyhUU5TVMPtxHGNz4bY9skxqRo
})

So, as you can see makeSignedUrl is almost identical to makeUrl apart from those two additional options. Here we're providing it our route identifier as the first argument. Then, the route parameters as the second argument. Lastly, we provide additional options, in this case we're expiring our signature after one hour.

For the expiresIn value, we can use time shorthands like 30s for seconds, 30m for minutes, 30h for hours, etc to represent the amount of time to expire in.

It’s also important to note that if we don’t provide expiresIn in our makeSignedUrl options the signature will be valid forever. So definitely be sure to include an expiresIn value when you need it.

Signed URL Builder

When generating a signed URL via the builder, we actually still use the builder method off of the Route module. The only difference here is instead of calling make, we'll want to call a method called makeSigned. This method is then what accepts the options specific to creating a signed URL.

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

Route.get('/reset-password/:email', () => {
  // TODO
}).as('reset.password')

const resetPasswordUrl = Route.builder()
  .params({ email: 'test@test.com' })
  .makeSigned('reset.password', { expiresIn: '1h' })

console.log({
  resetPasswordUrl // /reset-password/test@test.com?signature=ij5s2KjSDFlIjoiL3ZlcmlmeS9mb29AYmFyLmNvbSJ9.LDIX-SsDxu_E4O0sJxeAhyhUU5TVMPtxHGNz4bY9skxqRo
})

Validating A Signed URL Signature

Now, in order to actually check to see if a signed URL is valid, Adonis provides a method called hasValidSignature on our request object. This method will then grab the signature value off the requested URL's query string and validate it for us, returning back a boolean. It'll be true if our signature is valid, and false if it's invalid.

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

Route.get('/reset-password/:email', async ({ request }) => {
  if (request.hasValidSignature()) {
    return 'signature is valid'
  }

  return 'signature is invalid'
}).as('reset.password')

const resetPasswordUrl = Route.builder()
  .params({ email: 'test@test.com' })
  .makeSigned('reset.password', { expiresIn: '1h' })

console.log({
  resetPasswordUrl // /reset-password/test@test.com?signature=ij5s2KjSDFlIjoiL3ZlcmlmeS9mb29AYmFyLmNvbSJ9.LDIX-SsDxu_E4O0sJxeAhyhUU5TVMPtxHGNz4bY9skxqRo
})

Generating Redirect URL

Let's say on our password reset example above, when the signature is invalid we want to redirect the user to a /forgot-password page so they can re-initiate the flow. We can direct the user there by generating a URL off our redirect call using a method called toRoute. This method accepts the exact same argument set as makeUrl.

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

Route.get('/forgot-password/:email?', async ({ view }) => {
  return view.render('/auth/forgot-password', { email })
}).as('forgot.password')

Route.get('/reset-password/:email', async ({ request, response, params }) => {
  if (request.hasValidSignature()) {
    return 'signature is valid'
  }

  return response.redirect().toRoute('forgot.password', { 
    email: params.email 
  })
}).as('reset.password')

Here when our user's signature is invalid we'll redirect the user to our forgot.password route at /forgot-password. Since we have an email in our reset.password route we can pass that along as well by plucking it off our params object.

Generating A URL In Edge / Views

The last way we're going to cover how to generate URLs in within Edge, Adonis' template engine. Adonis provides a number of globals when we're working within Edge, one of which is a function called route. We can use this route function the exact same way we use makeUrl, only within our views. This provides a convenient way to link from one page to another, generate form post URLs, and even API endpoint URLs.

Now, since we haven't covered Edge at all yet, I'd like to make a quick note that all HTML is valid Edge, and Edge adds additional functionality like interpolation, looping, conditions, partials, components, layouts, etc.

In Edge, we can use double curly braces to gain access to its globals and to execute JavaScript expressions. So we can have <div>My name is {{ 'tom'.toUpperCase() }}</div> and Edge will render <div>My name is TOM</div>.

<!-- /resources/views/welcome.edge -->

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>AdonisJS - A fully featured web framework for Node.js</title>
  @entryPointStyles('css/app')
  @entryPointScripts('js/app')
</head>
<body>

  <main>
    <!-- /posts -->
    <a href="{{ route('posts.index') }}">
      View All Posts
    </a>
    
    <!-- /posts/1 -->
    <a href="{{ route('posts.show', { id: 1 }) }}">
      View Post 1
    </a>
  </main>

</body>
</html>

So, here we're making use of the route Edge global function to generate our URLs for our posts.index route and our [posts.show](<http://posts.show>) route.

Next Up

We're just about wrapped up with routing in Adonis! We just have one lesson left and that's to briefly cover how we can expand the Route module. Once we cover that we'll be moving our route handlers into controllers and that's where things really start to get interesting.