Thus far, we have been manually writing out the paths of URLs for our redirects, forms, and links. Today, we're going to learn how we can do it even better using names or identifiers for our routes instead of the actual paths.
Naming Routes
When we define routes, we can assign them a name by chaining an as() method on the route definition. We've actually already seen this with our home page.
router.on('/').render('pages/home').as('home')Copied!
- start
- routes.ts
By naming this route definition, we can reference it using "home" instead of its path ("/"). Why might we want to do this?
The first reason is type-safety. When we generate or refer to these routes within TypeScript, AdonisJS takes steps to ensure we're using routes that are actually defined at the type level.
If we check out the .adonisjs folder again, remember this is where AdonisJS places its auto-generated files, we'll find a routes.d.ts file within the server folder. AdonisJS generates this file listing our defined routes and their required parameters as well. If you'll notice, so far, the as() method is only being used at home, yet all of these routes have names associated with them as the key.
// .adonisjs/server/routes.d.ts import '@adonisjs/core/types/http' type ParamValue = string | number | bigint | boolean export type ScannedRoutes = { ALL: { 'home': { paramsTuple?: []; params?: {} } 'challenges.index': { paramsTuple?: []; params?: {} } 'challenges.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } 'challenges.create': { paramsTuple?: []; params?: {} } 'challenges.store': { paramsTuple?: []; params?: {} } 'challenges.edit': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } 'challenges.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } 'new_account.create': { paramsTuple?: []; params?: {} } 'new_account.store': { paramsTuple?: []; params?: {} } 'session.create': { paramsTuple?: []; params?: {} } 'session.store': { paramsTuple?: []; params?: {} } 'session.destroy': { paramsTuple?: []; params?: {} } } GET: { 'home': { paramsTuple?: []; params?: {} } 'challenges.index': { paramsTuple?: []; params?: {} } 'challenges.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } 'challenges.create': { paramsTuple?: []; params?: {} } 'challenges.edit': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } 'new_account.create': { paramsTuple?: []; params?: {} } 'session.create': { paramsTuple?: []; params?: {} } } HEAD: { 'home': { paramsTuple?: []; params?: {} } 'challenges.index': { paramsTuple?: []; params?: {} } 'challenges.show': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } 'challenges.create': { paramsTuple?: []; params?: {} } 'challenges.edit': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } 'new_account.create': { paramsTuple?: []; params?: {} } 'session.create': { paramsTuple?: []; params?: {} } } POST: { 'challenges.store': { paramsTuple?: []; params?: {} } 'new_account.store': { paramsTuple?: []; params?: {} } 'session.store': { paramsTuple?: []; params?: {} } 'session.destroy': { paramsTuple?: []; params?: {} } } PUT: { 'challenges.update': { paramsTuple: [ParamValue]; params: {'id': ParamValue} } } } declare module '@adonisjs/core/types/http' { export interface RoutesList extends ScannedRoutes {} }Copied!
This is because AdonisJS will now default to a name built from the controller's name and the method used for the route from that controller, giving us challenges.index as an auto-generated name for our /challenges route. When we refer to routes using their names, AdonisJS will use this file to ensure that the route exists and that we meet its requirements.
Let's start with our redirects within our ChallengesController as this will allow us to get TypeScript feedback. Instead of doing response.redirect('/challenges'), we can chain an additional toRoute() method off our redirection.
export default class ChallengesController { // ... async store({ request, response }: HttpContext) { const data = await request.validateUsing(challengeValidator) challenges.push({ id: challenges.length + 1, ...data }) return response.redirect().toRoute('challenges.index') } // ... }Copied!
- app
- controllers
- challenges_controller.ts
This accepts the route's name as the first argument and any route parameters as an object in the second. So if, for example, we wanted to redirect the user back to the challenge show page after updating, we could do:
export default class ChallengesController { // ... async update({ params, request, response }: HttpContext) { const data = await request.validateUsing(challengeValidator) // find the challenge being updated by its id, and update its data const challengeIndex = challenges.findIndex((row) => row.id === params.id) challenges[challengeIndex] = { id: params.id, ...data } return response.redirect().toRoute('challenges.show', { id: params.id }) } // ... }Copied!
- app
- controllers
- challenges_controller.ts
Note, with this, if we get the identifier wrong or omit the params, we get a type error to match, keeping things type-safe.
The other reason we might want to use identifiers is that route patterns are prone to change over time. By using the route's name instead of the pattern, we're saving ourselves the pain of having to search down all of the route's usages because the name we're using will automatically pick up pattern changes.
Generating Route URLs
We can also get access to the URL builder that toRoute is used under the hood to build out a URL via the identifier as well. For example:
import { urlFor } from '@adonisjs/core/services/url_builder' export default class ChallengesController { // ... async show({ view, params }: HttpContext) { const challenge = challenges.find((row) => row.id === params.id) const editUrl = urlFor('challenges.edit', { id: params.id }) return view.render('pages/challenges/show', { challenge, editUrl }) } // ... }Copied!
- app
- controllers
- challenges_controller.ts
Finally, this is also available within our EdgeJS templates via a global helper method called route.
@layout() <div> <div class="hero"> <h1>Challenges</h1> @include('partials/challenge/available_points') <a href="{{ route('challenges.create') }}" class="button">Create a new challenge</a> </div> @challenge.grid() @each(challenge in challenges) @!challenge.gridItem({ challenge }) @endeach @end </div> @endCopied!
- resources
- views
- pages
- challenges
- create.edge
We've got a few spots to update this.
<a href="{{ route('challenges.show', { id: challenge.id }) }}"> <h3>{{ challenge.text }}</h3> <p>Points: {{ challenge.points }}</p> </a>Copied!
- resources
- views
- components
- challenge
- grid_item.edge
We gave ourselves an editUrl on our show page, so we can make use of that if we'd like.
@layout() <div class="hero"> <h1>{{ challenge.text }}</h1> <p>Points: {{ challenge.points }}</p> <a href="{{ editUrl }}" class="button">Edit Challenge</a> </div> @endCopied!
- resources
- views
- pages
- challenges
- show.edge
Form Component
Finally, we have our forms. Like the fields, the starter kit also gave us a form component which will automatically include our csrfField() on non-GET requests! This component accepts the route identifier and route params as separate props, then builds those into our route URL for us.
@layout() <div class="form-container"> <div> <h1> Create Challenge </h1> <p> Enter your challenge details below </p> </div> <div> @form({ route: 'challenges.store', method: 'POST' }) <div> @field.root({ name: 'text' }) @!field.label({ text: 'Text' }) @!input.control({ type: 'text' }) @!field.error() @end </div> <div> @field.root({ name: 'points' }) @!field.label({ text: 'Points' }) @!input.control({ type: 'number' }) @!field.error() @end </div> <div> <button type="submit" class="button"> Create Challenge </button> </div> @end </div> </div> @endCopied!
- resources
- views
- pages
- challenges
- create.edge
Of note, the form component also automatically handles our HTTP method spoofing, so we can directly set the method here as "PUT" and it'll do the rest.
@layout() <div class="form-container"> <div> <h1> Edit Challenge </h1> <p> Enter your challenge details below </p> </div> <div> @form({ method: 'PUT', route: 'challenges.update', routeParams: { id: challenge.id } }) <div> @field.root({ name: 'text' }) @!field.label({ text: 'Text' }) @!input.control({ type: 'text', value: challenge.text }) @!field.error() @end </div> <div> @field.root({ name: 'points' }) @!field.label({ text: 'Points' }) @!input.control({ type: 'number', value: challenge.points }) @!field.error() @end </div> <div> <button type="submit" class="button"> Update Challenge </button> </div> @end </div> </div> @endCopied!
- resources
- views
- pages
- challenges
- edit.edge