Now, when it comes time to show details about a specific competition, we don't want to define a route for each individual competition. Instead, we want to allow the route to accept a dynamic identifier for the competition we're after. That's where route parameters come into play. They allow us to specify one or more portions of a route's pattern as dynamic under the pattern name we provide.
For example, if we want to identify a competition by its id, we can define the pattern as /competitions/:id. Route parameters are designated by a colon (:), and the term that follows is the parameter's name, the property with which we can access the dynamic value.
router.get('/challenge/:id', async (ctx) => { const challengeId = ctx.params.id return challengeId })Copied!
- start
- routes.ts
Our HttpContext provides a params object containing parsed route parameters. Since we've named our param id, that is the property name we can access the value with. Up til now, we've rendered an HTML page using EdgeJS. For now, let's just simply return this ID to see what we get!
When we request this with our browser, we get back a plaintext response. By default, AdonisJS will utilize content negotiation to determine the appropriate content type to respond with using the Accept header. If we instead specify this specifically as a JSON response, using the response on our HttpContext, we'll see our browser switches to a JSON viewer to match the new content-type of our response.
router.get('/challenge/:id', async (ctx) => { const challengeId = ctx.params.id return ctx.response.json({ id: challengeId }) })Copied!
- start
- routes.ts
Route Machers/Validators
If you noticed, our value is coming back as a string. Meaning, if we were to move our challenges array out where this route can utilize it as well, we would need to convert the type of the parameter in order to find a strict-equality match.
import { controllers } from '#generated/controllers' import { middleware } from '#start/kernel' import router from '@adonisjs/core/services/router' const challenges = [ { id: 1, text: 'Learn AdonisJS', points: 10 }, { id: 2, text: 'Learn EdgeJS', points: 5 }, { id: 3, text: 'Build an AdonisJS app', points: 20 }, ] router.on('/').render('pages/home').as('home') router.on('/terms').render('pages/terms') router.get('/challenges', async (ctx) => { return ctx.view.render('pages/challenges/index', { challenges }) }) router.get('/challenge/:id', async (ctx) => { const challenge = challenges.find((row) => row.id === Number(ctx.params.id)) return ctx.response.json({ challenge }) })Copied!
- start
- routes.ts
Instead of doing this, we can utilize route parameter matches and validation to:
Ensure the ID is a valid number for this route pattern to actually be matched against
Cast the ID route parameter to a number within our
paramsobject.
router.get('/challenge/:id', async (ctx) => { const challenge = challenges.find((row) => row.id === Number(ctx.params.id)) return ctx.response.json({ challenge }) }).where('id', /^[0-9]+$/)Copied!
- start
- routes.ts
Here, we're using regex to merely ensure the id route parameter is a number. If it isn't, the route won't be matched, and currently, that would result in us getting a 404 Not Found exception.
If we wanted to cast it to a number, we could add a cast to the validation.
router .get('/challenge/:id', async (ctx) => { const challenge = challenges.find((row) => row.id === ctx.params.id) return ctx.response.json({ challenge }) }) .where('id', { match: /^[0-9]+$/, cast: (value) => Number(value) })Copied!
- start
- routes.ts
Now, we're both ensuring it's a number and casting it from a string value to a number value. So our ctx.params.id is actually a number now, meaning we can get rid of the cast there. This is a common scenario, and AdonisJS knows that, so there is a convenient matcher we can use to perform this exact flow in a simplified manner.
router.get('/challenge/:id', async (ctx) => { const challenge = challenges.find((row) => row.id === ctx.params.id) return ctx.response.json({ challenge }) }).where('id', router.matchers.number())Copied!
- start
- routes.ts
You'll notice there is also a matcher for slug and UUID, doing similar things there. If, like our IDs, you're always going to want a route parameter to be cast or validated, we can apply this globally across all our routes.
import { controllers } from '#generated/controllers' import { middleware } from '#start/kernel' import router from '@adonisjs/core/services/router' const challenges = [ { id: 1, text: 'Learn AdonisJS', points: 10 }, { id: 2, text: 'Learn EdgeJS', points: 5 }, { id: 3, text: 'Build an AdonisJS app', points: 20 }, ] router.where('id', router.matchers.number()) router.on('/').render('pages/home').as('home') router.on('/terms').render('pages/terms') router.get('/challenges', async (ctx) => { return ctx.view.render('pages/challenges/index', { challenges }) }) router.get('/challenge/:id', async (ctx) => { const challenge = challenges.find((row) => row.id === ctx.params.id) return ctx.response.json({ challenge }) })Copied!
- start
- routes.ts
Now, anytime we use the name id for a route parameter, our number matcher will be used to validate and cast it.
Okay, finally, let's rig this up to a page!
node ace make:view pages/challenges/showCopied!
@layout() <div> <h1>{{ challenge.text }}</h1> <p>Points: {{ challenge.points }}</p> </div> @endCopied!
- resources
- views
- pages
- challenges
- show.edge
Then, we need to render the page instead of returning JSON as well. We can also spread specific properties out of our HttpContext as well to simplify our code a little as well.
router.get('/challenge/:id', async ({ view, params }) => { const challenge = challenges.find((row) => row.id === params.id) return view.render('pages/challenges/show', { challenge }) })Copied!
- start
- routes.ts
We can now also link to this page from our /challenges page using an anchor element.
@let(availablePoints = challenges.reduce((total, challenge) => total + challenge.points, 0)) @if (completedChallenges?.length) @let(completedPoints = completedChallenges.reduce((total, challenge) => total + challenge.points, 0)) @assign(availablePoints = availablePoints - completedPoints) @endif @layout() <div> <h1>Challenges</h1> <p>Welcome to the challenges page.</p> <div> Total Points Available: {{ availablePoints }} </div> @if (!challenges.length) <p>No challenges available.</p> @else <p>Below are our available challenges:</p> @endif <ul> @each(challenge in challenges) <li> <a href="/challenges/{{ challenge.id }}"> <h3>{{ challenge.text }}</h3> <p>Points: {{ challenge.points }}</p> </a> </li> @endeach </ul> </div> @endCopied!
- resources
- views
- pages
- challenges
- index.edge
Our interpolation here will inject the challenge ID we're currently looping over into the href path to form our completed URL. We'll expand on this later on with a more sustainable linking approach.