Routing
Table of contents
This guide covers the HTTP routing capabilities of the framework. By the end of this guide, you will know:
- What are HTTP routes.
- How to define routes following the REST principles.
- How to define dynamic routes using parameters.
- Binding controller methods to routes.
- Advanced concepts like shallow resources and pattern matching.
What are HTTP routes?
The users of your website or web application can visit different URL's like /
, /about
or /contact
. In order to make these URLs work, you will have to define them as routes inside the start/routes.ts
file.
Let's start by defining the following routes.
import Route from '@ioc:Adonis/Core/Route'
Route.get('/', async () => {
return 'This is the home page'
})
Route.get('/about', async () => {
return 'This is the about page'
})
Route.get('/contact', async () => {
return 'This is the contact page'
})
Make sure to start the HTTP server by running node ace serve --watch
command.
The above example registers a total of following three URLS. Each one will respond with the route handler return value.
If you try to visit a different URL, for example: /blog
, you will receive a 404 (Page not found) error.
Points to note
- A route always has a URL pattern and a handler function to handle requests for that specific URL.
- The
start/routes.ts
file is preloaded by AdonisJS during the application boot process. - You can view the registered routes by running
node ace list:routes
command inside the terminal.
Http Methods
You can define routes for different HTTP methods by calling the equivalent method names on the Route
object.
Http method | Route method |
---|---|
GET | Route.get |
POST | Route.post |
PUT | Route.put |
PATCH | Route.patch |
DELETE | βRoute.delete |
OPTIONS | βRoute.options |
The above mentioned methods only covers the commonly used HTTP methods. However, you can also define routes for any valid HTTP method.
Route.route('/', ['TRACE'], async () => {
return 'Responds to TRACE HTTP method'
})
If you only need to render a view (like a static page), you don't need to write a complete route handler.
// The view resources/view/home.edge will be automatically
// rendered when reaching /.
Route.on('/').render('home')
Route Handlers
Every route must have a corresponding handler to handle the request. Handlers can be inline closures or reference to a controller method.
The inline closures are functions, directly attached on the route definition. They are great for quickly testing out some code. However, we highly recommend keeping the routes file clean by using Controllers.
On the other hand, Controllers provides a great structure to your application by making you extract the code for handling requests to dedicated controller files. Make sure to read the guide on controllers for in-depth understanding.
Dynamic URL's
Not every URL can be as simple as /about
or /contact
. Many URL's will need dynamic values in order to be functional. A great example of this is a blog or an e-commerce website, where you want to lookup database records for a given id.
Let's continue with the blog example and see how we can accept the blog post id as part of the url and then render the correct blog post.
Route.get('/posts/:id', async ({ params }) => {
return `You are viewing a blog post with id ${params.id}`
})
Make sure to start the HTTP server by running node ace serve --watch
command.
Now, if you will visit the http://localhost:3333/posts/1, you will see the following message.
What just happened?
- The
:id
is an expression (known as route parameters) that tells AdonisJS to accept any value and store it asid
under theparams
object. - You can give any name to the parameter, just make sure that it starts with
:
(a colon). - The
params
property exists on thectx
object. You can access it asctx.params
or use the JavaScript destructuring as shown in the above example.
Optional Parameters
You can also mark parameters as optional by adding the ?
suffix. Consider the following example:
π
Route.get('/posts/:id?', async ({ params }) => {
if (params.id) {
return `You are viewing a blog post with id ${params.id}`
} else {
return `You are viewing all blog posts`
}
})
Regular Expression Matchers
You can constraints the route parameters to have a specific shape by adding the where
modifier.
Route.get('/posts/:id', async () => {
// ...
}).where('id', /^[0-9]+$/) π
When using them inside a Route Group, the route modifier take precedence over the group one.
Route.group(() => {
Route.get('/:id', 'handler').where('id', /^[0-9]+$/) // π This one will be used
}).where('id', /^[a-z]+$/)
CRUD Actions
The principles of REST provides a great way to map CRUD operations with HTTP methods without making the URL's verbose.
For example: The URL /posts
can be used to view all the posts and also to create a new post, just by using the correct HTTP method.
Route.get('/posts', () => {
return 'List of posts'
})
π
Route.post('/posts', () => {
return 'Create a new post'
})
Using the principles of REST, following are the routes to perform CRUD operations on blog posts.
Route.post('/posts', async () => {
return 'Perform Create'
})
Route.get('/posts', () => {
return 'Perform Read'
})
Route.get('/posts/:id', () => {
return 'Perform Read on a single post'
})
Route.put('/posts/:id', () => {
return 'Perform Update'
})
Route.delete('/posts/:id', () => {
return 'Perform Delete'
})
Since, the above pattern is so popular and widely used. AdonisJS provides a shortcut to define all the RESTful routes using Route resources.
Route resources
Route.resource('posts', 'PostsController')
The Route.resource
method registers the following routes along with the appropriate controller methods.
The Resourceful routes has two implications.
- The dynamic parameter name is always
:id
. - You always need to assign a controller to a resource.
If you notice, the Route.resource
method creates two extra routes.
/posts/create
route is meant to display the form for creating a new post./posts/:id/edit
route is meant to display the form for updating an existing post.
However, when creating an API server, the routes to display forms are redundant and can be removed using the apiOnly
method.
Route
.resource('posts', 'PostsController')
.apiOnly() π
Filtering Resourceful Routes
In many situations, you would want to prevent some of the resourceful routes from getting registered. For example: You decided to restrict the users of your blog from updating or deleting their comments and hence routes for the same are not required.
Route
.resource('comments', 'CommentsController')
.except(['update', 'destroy']) π
The opposite of except
is the only
method to whitelist selected routes.
Route
.resource('comments', 'CommentsController')
.only(['index', 'create', 'store', 'show', 'edit']) π
The except
and only
methods takes an array of subset of route names to blacklist or whitelist. When both are applied together, the only
method will win.
Learn more about route names.
Nested resources
Nested resources are registered using the dot notation (.)
. Even though there is no technical limitation on the nesting level, it is recommended to avoid deeply nested resources.
Route.resource('posts.comments', 'CommentsController')
The above example registers the following routes. The parent resource id gets prefixed with the resource name, ie: post_id
.
Route Groups
AdonisJS provides a great way to group multiple routes of similar nature and bulk configure them instead of re-defining 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.
Route Prefix
You can prefix the group routes URL using the .prefix
method. For example: Adding /blog
prefix to all blog related routes.
Route
.group(() => {
Route.get('/', 'PostsController.index')
Route.get('/posts/:id', 'PostsController.show')
})
.prefix('/blog')
In case of nested groups, all prefixes will be applied starting from the outer to the inner groups. For example
Route
.group(() => {
Route.get('/', 'PostsController.index')
Route
.group(() => {
Route.get('/', 'PostsApiController.index')
})
.prefix('/api')
})
.prefix('/blog')
You can also define middleware, sub-domains and route names on a group of routes.
Route's Controller Namespace
You can change the default namespace of your controller by using the .namespace
method.
Route
.group(() => {
Route.get('/', 'PostsController.index')
})
.namespace('App/Controllers/Http/Api')
In this example, PostsController
will be imported from App/Controllers/Http/Api
instead of App/Controllers/Http
.
The namespace should be an absolute path from the root of your application.
Route Middleware
Middleware offers an API to execute a series of actions before a given HTTP request reaches the route handler.
AdonisJS provides two layers at which middleware can be executed.
- Global middleware are executed for every HTTP request.
- Route middleware are executed when request for the corresponding route is received.
This section focuses on the usage of Middleware with Routes. Make sure to read the dedicated guide on middleware to learn more about them.
You can add middleware to the routes using the middleware
method, as shown below:
Route
.get('/users/:id', async () => {
return 'Show user'
})
.middleware(async (ctx, next) => {
console.log(`Inside middleware ${ctx.request.url()}`)
await next()
})
Middleware Classes
Just like Controllers, you can extract inline middleware functions to their own dedicated classes and keep your routes file clean and tidy.
Creating and using middleware classes involves an extra step of registering middleware aliases and then using the alias as an identifier on the route. The middleware guide covers the concept of aliases in depth.
Run the following command to create a new middleware class.
node ace make:middleware Sample
Copy/paste the following contents inside
app/Middleware/Sample.ts
file.import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext' export default class SampleMiddleware { public async handle (ctx: HttpContextContract, next) { console.log(`Inside middleware ${ctx.request.url()}`) await next() } }
Register the middleware namespace with an alias under
start/kernel.ts
file.Server.middleware.registerNamed({ sample: 'App/Middleware/Sample', })
Reference the registered alias on the route.
Route .get('/users/:id', 'UserController.show') .middleware('sample') π
Applying Middleware to Route Groups
You can also apply middleware to a group of routes. The group middleware will be executed before the middleware registered on individual routes.
Route
.group(() => {
Route
.put('/posts/:id', 'PostsController.update')
.middleware('postAuthor')
})
.middleware('auth')
In the above example, the auth
middleware will executed before the postAuthor
middleware.
Applying Middleware to Route Resources
The Route.resource
method also exposes the API to register middleware on all or selected routes registered by a given resource.
Route
.resource('users', 'UsersController')
.middleware({ '*': ['auth'] })
When selectively applying middleware to certain routes, the object key is the name of the route and value is an array of middleware to apply.
Route
.resource('posts', 'PostsController')
.middleware({
store: ['auth'],
update: ['auth'],
destroy: ['auth'],
})
Domains
AdonisJS makes it super easy to create multi domain applications. Using domains
, you can restrict routes to be accessible only for a given domain.
Route
.group(() => {
// Scoped to blog.adonisjs.com only
})
.domain('blog.adonisjs.com')
When running your application, AdonisJS will make sure that routes defined inside the above group are only accessible by the blog.adonisjs.com
domain.
You can also define dynamic parameters within the domain name. For example: Creating a multi-tenant app and accepting the tenant subdomain as part of the URL.
In the following route, you can access the tenant
value from the subdomains
object.
Route
.group(() => {
Route.get('/', async ({ subdomains }) => {
return `tenant: ${subdomains.tenant}`
})
})
.domain(':tenant.myapp.com')
Naming Routes
The routes are defined inside a single file ie. start/routes.ts
, but they can be referenced across the application to generate URLs. For example:
Instead of hardcoding the form action inside HTML, you can make use of the route
helper to generate a URL for you.
Currently, there is a problem with our code. If we were to change the route pattern /users/:id
to something else, we will have to make the same change inside the template as well.
To overcome this problem, AdonisJS allows you to give unique names to your routes. When generating the URL, you can use the name instead of the route pattern.
Route
.get('/users/:id', 'UsersController.show')
.as('showUser') π
Naming Resourceful Routes
The routes registered by the Route.resource
method are automatically named after the resource name.
For example: The users
resource will have the following names.
users.index
users.create
users.store
users.show
users.edit
users.update
users.delete
Similarly the nested resources will be named after the parent.child
resource.
Naming Route Groups
Using route groups, you can prefix the routes names for that given group. This is helpful, when you have similar routes outside the group and want to avoid name collisions. For example:
Route.resource('users', 'UserController')
Route
.group(() => {
Route.resource('users', 'UserController')
})
.prefix('v1')
The above code will not work, since we have two identical resources, both having the same names. We can fix the issue by prefixing the resourceful routes names, declared inside the group.
Route.resource('users', 'UserController')
Route
.group(() => {
Route.resource('users', 'UserController')
})
.prefix('v1')
.as('apiv1') π