Route parameters give us a way to add dynamic routes to our route definitions so that we don't have to define a route for each identity that's within our database.
For example, without route parameters, in order to have a route for posts with an id
of 1, 2, and 3, we'd need to manually define the same route for each individual id
. That quickly becomes a scalability issue, as you can see below.
Route.get('/posts/1', () => 'get post 1') Route.get('/posts/2', () => 'get post 2') Route.get('/posts/3', () => 'get post 3') Route.put('/posts/1', () => 'get post 1') Route.put('/posts/2', () => 'get post 2') Route.put('/posts/3', () => 'get post 3') Route.delete('/posts/1', () => 'get post 1') Route.delete('/posts/2', () => 'get post 2') Route.delete('/posts/3', () => 'get post 3')
Copied!
With route parameters, we can make the id
portion of the URL dynamic so that we only need to define the route once.
Defining A Route Parameter
Route parameters in Adonis start with a colon (:
), followed by whatever you want to name that parameter. Typically you'd name the route parameter after what value it should hold.
Route.get('/posts/:id', () => 'get post with provided id') Route.put('/posts/:id', () => 'update post with provided id') Route.delete('/posts/:id', () => 'delete post with provided id')
Copied!
Here, we've named our route parameter id
to signify that the route parameter is meant to be a post id
value. So regardless if we send a GET request for http://localhost:3333/posts/1
, http://localhost:3333/posts/2
, or http://localhost:3333/posts/3
our GET route definition will be used.
Of course, be mindful of this because our :id
route parameter will match anything that's passed into that position in the url. For example, http://localhost:3333/posts/not-an-id
.
Accessing A Route Parameter Value
At this point, you might be asking, "how do I, as the developer, know what the user is passing through as the id for the route?". If you recall back to the HttpContext lesson, we actually have a property called params
within our HttpContext. This params
property is how we can access any and all route parameters a user is providing for a given route.
Route.get('/posts/:id', ({ params }) => { return `get post with id of ${params.id}` }) Route.put('/posts/:id', ({ params }) => { return `update post with id of ${params.id}` }) Route.delete('/posts/:id', ({ params }) => { return `delete post with id of ${params.id}` })
Copied!
Since we've named our route parameter id
, it's value will be added to our params
object as id
. So, we can access the route parameter value via params.id
.
Route.get('/posts/:identity', ({ params }) => { return `get post with identity of ${params.identity}` })
Copied!
Had we named our route parameter something different, like identity
, we'd need to use that name to access the parameter value via params.identity
.
Route.get('/posts/:id', (ctx) => { return `get post with identity of ${ctx.params.id}` })
Copied!
Also remember, we're just extracting params
out of our HttpContext object, you can of course just access it directly off the context object as well.
Optional Route Parameters
Now, there are going to be some use-cases where there may or may not be a route parameter needed. In those cases, we can actually make a route's parameter optional by adding a question mark as a suffix to the name.
Route.get('/posts/topics/:topic?', ({ params }) => { if (params.topic) { return `get all posts for specific topic: ${params.topic}` } return `get all topics` })
Copied!
By adding the question mark at the end of our parameter's name, we're noting that the route parameter is optional. An optional route parameter means our route will match with our without a value for the parameter. In this case, both http://localhost:3333/posts/topics
and http://localhost:3333/posts/topics/adonis
are going to match this route definition.
Route Order Matters
When you start adding route parameters into your route definitions, the order of your routes begins to matter. When Adonis receives a request for a url, it'll search for a match within our route definitions starting from the top to the bottom of our defined routes. Once a match is found, Adonis will stop searching and will use that first matching route definition to handle the request.
Let's inspect the below as an example.
Route.get('/posts/:id', ({ params }) => { return `get post: ${params.id}` }) Route.get('/posts/topics', ({ params }) => { return `get post topics` })
Copied!
With the order used here, our second route definition will never be used since topic
can be used as the id
for /posts/:id
. The same applies if we add back the optional parameter to our topics route.
Route.get('/posts/:id', ({ params }) => { return `get post: ${params.id}` }) Route.get('/posts/topics/:topic?', ({ params }) => { return `get topics: ${params.topic ?? 'all'}` })
Copied!
If we don't provide a topic for our topic
route parameter, the second route definition won't be used. This essentially makes our optional topic
route parameter required, since the route won't be matched without it.
Solve With Ordering
The first option we have to resolve this issue is ordering. Since Adonis will search for a matching route definition from the top of our file to the bottom, we can simply move our topics definition above our /posts/:id
definition. Of course, keep in mind we can never support topic
as a value for /posts/:id
with this solution.
Route.get('/posts/topics/:topic?', ({ params }) => { return `get topics: ${params.topic ?? 'all'}` }) Route.get('/posts/:id', ({ params }) => { return `get post: ${params.id}` })
Copied!
Now if we request http://localhost:3333/posts/topics
our intended topics route definition will be used since it's the first route definition that'll match.
Solve with Route Validation
The second option we have to solve this issue is by using route validation by using matchers. Adonis actually allows us to add regex validators for our route parameters. Meaning, we can specify that /posts/:id
should only match when a numeric value is provided for the id
route parameter.
Route.get('/posts/:id', ({ params }) => { return `get post: ${params.id}` }).where('id', /^[0-9]+$/) Route.get('/posts/topics/:topic?', ({ params }) => { return `get topics: ${params.topic ?? 'all'}` }).where('topic', /^[a-z0-9]+(?:-[a-z0-9]+)*$/gm)
Copied!
Here we're using the where
method to add validation to our two routes. For our /posts/:id
route, we're specifying that for our id
route parameter we only want it to match for the request if the value is a number. For our /posts/topics/:topic?
route, we're specifying we only want it to match if the topic
value is alphanumeric containing -
as a separator.
With these validations in place, we can safely have our topics route after our /posts/:id
route since this route requires a number in the id
position and the topics route has the word topic
in that position.
Cast Route Parameters
Since Adonis has no way to determine what type a route parameter should come through as it's going to provide all values as strings by default. However, being the accommodating framework that it is, Adonis allows us to cast route parameter values via the definition.
Route.get('/posts/:id', ({ params }) => { return `get post: ${params.id}` }).where('id', { match: /^[0-9]+$/, cast: (id) => Number(id) })
Copied!
To do this, we can make use of the where
method to specify what parameter we want to cast, in this case, id
. Then, instead of providing just regex as the second argument, we can provide an object. Our regex moves to the match
property. Then, we can define a cast
property as a callback function, which Adonis will provide us that parameter value that we can then use to cast to whatever type we need it to be, in this case, a number.
Adonis Validator & Cast Utilities
Adonis also has three pre-defined matcher methods we can use that will return back an object with the match and cast defined.
Number
First is a utility for validating a parameter is a number and casting the value to a number. This can be used via Route.matchers.number()
.
Route.get('/posts/:id', ({ params }) => { return `get post: ${params.id}` }).where('id', Route.matchers.number())
Copied!
Slug
The second is a utility to verify a URL safe string, also known as a slug. This can be used via Route.matchers.slug()
.
Route.get('/posts/:slug', ({ params }) => { return `get post: ${params.id}` }).where('slug', Route.matchers.slug())
Copied!
UUID
Last is a utility to verify a UUID or universally unique identifier. Some folks prefer to use this as an id value instead of a numeric id. This can be used via Route.matechers.uuid()
.
Route.get('/posts/:id', ({ params }) => { return `get post: ${params.id}` }).where('id', Route.matchers.uuid())
Copied!
Global Validation & Cast
We can also define a global validation and cast for a particular parameter name by calling the where
method directly off the Route module.
Route.where('id', Route.matchers.number()) Route.where('slug', Route.matchers.slug())
Copied!
Doing this globally will automatically validate all route parameters within our route definitions with the name of id
or slug
with their given global validation. Additionally, it'll also globally cast the values as well. So now all our id
are guaranteed to be numeric and we'll get their values inside our route handlers as numbers instead of strings.
Wildcard Route Parameters
You may come across a use-case where anything beyond a certain point in a url can be absolutely dynamic. An example of this would be if you need to search for something within a physical file structure, like for an image or file. We can make use of wildcard route parameters to make this happen.
Route.get('/img/*', ({ params }) => { return params['*'] })
Copied!
A few things to note here.
Wildcard parameters will match anything. So, it'll match
/img/test
and/img/this/is/a/test
.We access wildcard route parameters via a property off params called
*
. So we'd access them withparams['*']
.The value of our wildcard parameters will be provided as an array since there's no key to match the values up against. So, for
/img/test
we'd get an array of['test']
. Forimg/this/is/a/test
we'd get['this', 'is', 'a', 'test']
.
Named & Wildcard Route Parameters
Maybe we have an image server that allows a user to create folders and upload images to those folders. Off our root directory, each user gets their own folder, so we need the user's id to be in the URL for sure, followed by whatever directory path they're trying to access.
For this, we can define a named route parameter for the user's id, followed by a global route parameter.
Route.get('/img/:userId/*', ({ params }) => { return { userId: params.userId, directoryPath: params['*'].join('/') } })
Copied!
So, we can access this route with all of the following.
/img/1/dogs/boradors/janet.jpg
Here ouruserId
route parameter is 1, and our joined directory path isdogs/boradors/janet.jpg
./img/255/ford/mustangs/1969/blue.jpg
Here ouruserId
is 255, and our joined directory path isford/mustangs/1969/blue.jpg
.
Next Up
So, we've taken a deep dive into route parameters and what we can do with them. We've learned the order of our route definitions matters. We've covered that we can validate our routes using matches and that we can also cast our route parameters as needed. We can also validate and cast on a global basis.
In the next lesson, we'll be focusing on the organization and grouping of our routes.
Join The Discussion! (3 Comments)
Please sign in or sign up for free to join in on the dicussion.
imfcbrkkfhdosowe
Amazing tutorial, thanks to you i learned a lot
Please sign in or sign up for free to reply
tomgobich
Thank you!! I’m happy to hear it was helpful! :)
Please sign in or sign up for free to reply
jony254
Lesson 6✅
Please sign in or sign up for free to reply