Routing

The users of your website or web application can visit different URLs like /, /about or /posts/1. To make these URLs work, you will have to define them as routes.

Routes are usually defined (but not limited to) inside the start/routes.ts file using the Route module.

A typical route accepts the route pattern as the first argument and route handler as the second argument. For example:

import Route from '@ioc:Adonis/Core/Route'
Route.get('/', () => {
return 'Hello world'
})

The route handler can also reference a controller method.

import Route from '@ioc:Adonis/Core/Route'
Route.get('posts', 'PostsController.index')

Default routes file

Conventionally the routes are registered inside the start/routes.ts file, which is then preloaded by AdonisJS when booting the application. However, this is not a hard restriction, and you can keep your routes in a separate file as well.

Let's explore some different ways to structure and load routes from other files.

Imports inside the routes.ts file

One approach is to define your routes in different files as per your application structure and then import all those files inside the start/routes.ts file.

start/routes.ts
import 'App/Modules/User/routes'
import 'App/Modules/Cart/routes'
import 'App/Modules/Product/routes'

Register as a preload file

Another approach is to get rid of the routes file altogether and use a custom file path to register the routes. In this scenario, do make sure to register the path inside the .adonisrc.json file under the preloads array.

.adonisrc.json
{
"preloads": [
"./start/routes",
"./start/kernel",
"add-path-to-your-routes-file"
]
}

List routes

You can view the registered routes by running the following Ace command.

node ace list:routes

By default, the routes are pretty printed inside a structured table. However, you can also access them as JSON string by defining the --json flag.

node ace list:routes --json > routes.json

HTTP methods

AdonisJS provides shorthand methods to register routes for commonly used HTTP verbs. For example:

Post verb

Route.post('posts', async () => {})

Put verb

Route.put('posts/:id', async () => {})

Patch verb

Route.patch('posts/:id', async () => {})

Delete verb

Route.delete('posts/:id', async () => {})

Rest of the HTTP verbs

For the rest of the HTTP verbs, you can use the Route.route method.

Route.route('/', ['OPTIONS', 'HEAD'], async () => {})

Route for all common HTTP verbs

The Route.any method registers the route to handle the request for all the following HTTP verbs.

  • HEAD
  • OPTIONS
  • GET
  • POST
  • PUT
  • PATCH
  • DELETE
Route.any('csp-report', async () => {})

Route parameters

Route parameters provide a way to register URLs that can accept dynamic values as part of the URL.

A parameter always starts with a colon : followed by the name of the parameter. For example:

Route.get('/posts/:id', async ({ params }) => {
return `Viewing post with id ${params.id}`
})

Optional params

Parameters can also be marked as optional by appending a question mark ? to the name. However, do make sure that the optional parameters come after the required parameters.

Route.get('/posts/:id?', async ({ params }) => {
if (params.id) {
return `Viewing post with id ${params.id}`
}
return 'Viewing all posts'
})

Wildcard parameters

You can also define a wildcard parameter using the * keyword. A wildcard param catches all the URI segments. For example:

Route.get('docs/*', ({ params }) => {
console.log(params['*'])
})
URLWildcard param
/docs/http/introduction['http', 'introduction']
/docs/api/sql/orm['api', 'sql', 'orm']

You can also have named parameters alongside the wildcard parameter. However, do make sure that the wildcard param is after the named param.

Route.get('docs/:category/*', ({ params }) => {
console.log(params.category)
console.log(params['*'])
})

Params matchers

Param matchers allow you to validate the parameters against a given regular expression. If the check fails, the route will be skipped.

Consider the following example in which we want to look up a post by its id and slug both.

Route
.get('/posts/:id', async ({ params }) => {
return `Viewing post using id ${params.id}`
})
.where('id', /^[0-9]+$/)
Route
.get('/posts/:slug', async ({ params }) => {
return `Viewing post using slug ${params.slug}`
})
.where('slug', /^[a-z0-9_-]+$/)
  • The requests passing a numeric id to the /posts URL will be forwarded to the first route. For example: /posts/1 or /posts/300
  • Whereas the request matching the slug regex will be forwarded to the second route. For example: /posts/hello_world or /posts/adonis-101.
  • A 404 is returned when unable to find a match.

You can also define params matchers globally using the Route.where method. The global matchers are applied to all the routes unless overridden specifically at the route level.

Route.where('id', /^[0-9]+$/)

Params casting

Parameters part of the URL are always represented as a string. For example: In the URL /posts/1, the value 1 is a string and not a number since there is no direct way to infer data types for the URI segments.

However, you can manually cast the params to their actual JavaScript data type by defining a cast property with the param matcher.

It is a good practice to validate the param using the match property when using the cast function.

Route
.get('posts/:id', 'PostsController.show')
.where('id', {
match: /^[0-9]+$/,
cast: (id) => Number(id),
})

Inbuilt matchers

The route module ships with the following inbuilt matchers for commonly used data types.

// Validate id to be numeric + cast to number data type
Route.where('id', Route.matchers.number())
// Validate id to be a valid uuid
Route.where('id', Route.matchers.uuid())
// Validate slug to match a given slug regex: regexr.com/64su0
Route.where('slug', Route.matchers.slug())

URL generation

The APIs for URL generation raises an exception when unable to look up a route.

Instead of hardcoding the URLs everywhere in your app, you can leverage the URL generation API to generate URLs for pre-registered routes. For example:

Route.get('/users/:id', 'UsersController.show')
// Positional arguments
const url = Route.makeUrl('/users/:id', [1])
// Named object keys
const url = Route.makeUrl('/users/:id', { id: 1 })

You can also make use of the Controller.method name to reference the route.

const url = Route.makeUrl('UsersController.show', { id: 1 })

Or use the route's unique name as a reference.

Route
.get('/users/:id', 'UsersController.show')
.as('showUser') // 👈 Route name
// Generate URL
const url = Route.makeUrl('showUser', { id: 1 })

Append query string

You can append a query string to the generated URLs by passing a third argument to the makeUrl method.

const url = Route.makeUrl('showUser', [1], {
qs: {
verified: true,
},
})

Lookup inside a domain

Since AdonisJS allows you to register routes for different domains, you can limit your makeUrl search for a specific domain as well.

Route
.get('/users/:id', 'UsersController.show')
.domain(':tenant.adonisjs.com')
// Make URL
const url = Route.makeUrl('UsersController.show', [1], {
domain: ':tenant.adonisjs.com',
})

Prefix a domain

The generated URLs are always relative paths without any domain name. However, you can define one using the prefixUrl property.

const url = Route.makeUrl('UsersController.show', [1], {
prefixUrl: 'https://foo.com',
})

URL builder

URL builder is an alternative to the makeUrl method and provides a fluent API to make the URLs.

const url = Route.builder()
.params({ id: 1 })
.qs({ verified: true })
.prefixUrl('https://foo.com')
.make('UsersController.show')

Make for a domain

const url = Route.builderForDomain(':tenant.adonisjs.com')
.params({ id: 1 })
.qs({ verified: true })
.prefixUrl('https://foo.com')
.makeUrl('UsersController.show')

URL generation within views

You can use the route helper inside your template files to generate the URLs. The route has the same API as the makeUrl method.

Route.post('posts', 'PostsController.store').as('posts.create')
<form method="POST" action="{{ route('posts.create') }}">
</form>

URL generation during redirects

You can also generate a URL to a pre-registered route when redirecting the request. The redirect().toRoute() has the same API as the makeUrl method.

Route
.get('/users/:id', 'UsersController.show')
.as('users.show')
Route.post('users', async ({ response }) => {
// Create user
response.redirect().toRoute('users.show', { id: user.id })
})

Routing for SPA

The flow may look as follows when serving a SPA from the same routing layer as your AdonisJS app.

  • The first request hits the AdonisJS application.
  • You load an HTML layout with your front-end scripts and styles.
  • From there on, the routing and rendering are handled by a front-end framework.

With this flow in place, you would want AdonisJS always to load the same HTML file regardless of the URL, as routing logic is placed inside a front-end application.

You can achieve this result by defining a wildcard route.

start/routes.ts
Route.get('*', async ({ view }) => {
return view.render('app')
})
// Shorter version
Route.on('*').render('app')

All other AdonisJS specific routes (maybe your API) should be defined above the wildcard route.

Route.get('/api/users', 'UsersController.index')
Route.get('/api/posts', 'PostsController.index')
// SPA route
Route.on('*').render('app')

Or better group the API routes with the /api prefix.

Route.group(() => {
Route.get('/users', 'UsersController.index')
Route.get('/posts', 'PostsController.index')
}).prefix('/api')
// SPA route
Route.on('*').render('app')

Route groups

AdonisJS provides a great way to group multiple routes of similar nature and bulk configure them instead of re-defining the same properties on every route.

A group is created by passing a closure to the Route.group method. Routes declared inside the closure are part of the surrounding group.

Route.group(() => {
// All routes here are part of the group
})

You can also create nested groups, and AdonisJS will merge or override properties based upon the behavior of the applied setting.

Prefix routes

All of the following routes inside the group closure will be prefixed with the /api string.

Route
.group(() => {
Route.get('/users', 'UsersController.index')
Route.get('/posts', 'PostsController.index')
})
.prefix('/api')

In the case of nested groups, the prefix will be applied from the outer to the inner group.

Route.group(() => {
Route.group(() => {
Route.get('/users', 'UsersController.index') // /api/v1/users
Route.get('/posts', 'PostsController.index') // /api/v1/posts
}).prefix('/v1')
}).prefix('/api')

Apply middleware

You can apply middleware to a group of routes by using the .middleware method. The group middleware are executed before the route's middleware.

Route.group(() => {
Route.get('users', async () => {
return 'handled'
}).middleware('can:view_users')
}).middleware('auth')

Naming routes

Naming a group will prefix all its routes with the given name. For example:

// Named as users.index, users.store and so on
Route.resource('users', 'UserController')
Route
.group(() => {
// Named as api.users.index, api.users.store
Route.resource('users', 'UserController')
})
.prefix('v1')
.as('api')

Route domains

Using the route module, you can also define routes for a specific domain or a subdomain. In the following example, the routes will only match if the current request hostname is blog.adonisjs.com.

You still have to configure your proxy server to handle requests for the registered subdomains and forward them to your AdonisJS server.

Route
.group(() => {
Route.get('/', 'PostsController.index')
Route.get('/:id', 'PostsController.show')
})
.domain('blog.adonisjs.com')

The domains can be also accept dynamic parameters. For example, a domain that accepts the tenant subdomain.

Route
.group(() => {
Route.get('/', ({ subdomains }) => {
console.log(subdomains.tenant)
})
})
.domain(':tenant.adonisjs.com')

Brisk routes

Brisk routes are defined without any explicit route handler. You can think of them as a shortcut for certain behaviors.

render

In the following example, we render the welcome view by chaining the .render method.

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

The .render accepts the template data as the 2nd argument.

Route.on('/').render('welcome', { greeting: 'Hello world' })

redirect

The .redirect method redirects the request to the pre-defined route. It will use the route params from the actual request to make the redirect route's URL.

Route.on('/posts/:id').redirect('/articles/:id')
// Inline params
Route.on('/posts/:id').redirect('/articles/:id', { id: 1 })
// Custom status
Route.on('/posts/:id').redirect('/articles/:id', undefined, 301)

redirectToPath

To redirect to an absolute URL, you can make use of the redirectToPath method.

Route.on('/posts/:id').redirectToPath('https://medium.com/my-blog')
// Custom status
Route.on('/posts/:id').redirectToPath('https://medium.com/my-blog', 301)

Access registered routes

You can access the registered routes by calling the Route.toJSON method. However, calling this method inside the routes file returns an empty array because the routes are compiled just before starting the HTTP server.

You can run the Route.toJSON() method inside a middleware, controller or the service providers start method. The rule of thumb is to avoid accessing routes before the HTTP server is ready.

providers/AppProvider.ts
import { ApplicationContract } from '@ioc:Adonis/Core/Application'
export default class AppProvider {
public static needsApplication = true
constructor(protected app: ApplicationContract) {}
public async ready() {
const Route = this.app.container.use('Adonis/Core/Route')
console.log(Route.toJSON())
}
}

Extending Router

The Router is a combination of multiple classes can add custom properties/methods to all the classes using the macros or getters.

The best place to extend the Router is inside the boot method of a custom service provider. Open the providers/AppProvider.ts file and write the following code inside it.

import { ApplicationContract } from '@ioc:Adonis/Core/Application'
export default class AppProvider {
public static needsApplication = true
constructor(protected app: ApplicationContract) {}
public async boot() {
const Route = this.app.container.use('Adonis/Core/Route')
Route.Route.macro('mustBeSigned', function () {
this.middleware(async (ctx, next) => {
if (!ctx.request.hasValidSignature()) {
ctx.response.badRequest('Invalid signature')
return
}
await next()
})
return this
})
}
}

In the above example, we have added the mustBeSigned method to the Route class, which internally registers a middleware to verify the request signature .

Now, open the start/routes.ts file to use this method.

start/routes.ts
Route
.get('email/verify', 'OnboardingController.verifyEmail')
.mustBeSigned()

Informing TypeScript about the method

The mustBeSigned property is added at the runtime, and hence TypeScript does not know about it. To inform the TypeScript, we will use declaration merging and add the property to the RouteContract interface.

Create a new file at path contracts/route.ts (the filename is not important) and paste the following contents inside it.

contracts/route.ts
declare module '@ioc:Adonis/Core/Route' {
interface RouteContract {
mustBeSigned(): this
}
}

Extending route resource

You can extend the RouteResource class as follows:

Add macro
Route.RouteResource.macro('yourMacroName', fn)
Extend interface
declare module '@ioc:Adonis/Core/Route' {
interface RouteResourceContract {
yourMacroName(): this
}
}
Use macro
Route.resource().yourMacroName()

Extending route group

You can extend the RouteGroup class as follows:

Add macro
Route.RouteGroup.macro('yourMacroName', fn)
Extend interface
declare module '@ioc:Adonis/Core/Route' {
interface RouteGroupContract {
yourMacroName(): this
}
}
Use macro
Route.group().yourMacroName()

Extending brisk route

You can extend the BriskRoute class as follows:

Add macro
Route.BriskRoute.macro('yourMacroName', fn)
Extend interface
declare module '@ioc:Adonis/Core/Route' {
interface BriskRouteContract {
yourMacroName(): this
}
}
Use macro
Route.on('/').yourMacroName()

Additional reading

Following are some of the additional guides to learn more about the topics not covered in this document.