A resource can be thought of as a single table within your database, like posts. In order to create, read, update, and delete, or CRUD for short, we typically need very similar routes defined for each resource. Because of this, a lot of developers follow a naming convention when defining routes to handle CRUD operations for each resource. These routes are called Resourceful Routes or Resource Routes.
Resource Routes
Resource routes use HTTP Methods to define the action being performed.
Get for retrieving data
Post for creating data
Put for updating data, providing the entire resource record
Patch for updating data, providing a partial resource record
Delete for deleting data
AdonisJS natively supports defining routes using each of these HTTP Methods, as we covered in the routing module.
Then, the plural name of the resource is typically used for the URL. A complete resourceful route set will look like the following, using posts as an example.
METHOD PATH PURPOSE
GET /posts - Get all posts
GET /posts/create - Display post create form
POST /posts - Create a post record
GET /posts/:id - Display details for the post with the provided id
GET /posts/:id/edit - Display post edit form
PUT/PATCH /posts/:id - Update the post with the provided id
DELETE /posts/:id - Delete the post with the provided id
Resource Controllers
When your application utilizes controllers for handling routes, as AdonisJS does, we can mimic the resource route naming convention within our controllers to help keep things easy to understand. These are called resource controllers. If you recall back to our Ace CLI lesson, AdonisJS supports resource controllers and makes it super easy to create one by providing --resource
, or -r
for short, when we create a controller using the Ace CLI.
When we create a resource controller AdonisJS will stub our controller with all the standard methods a resource controller utilizes.
METHOD PATH METHOD
GET /posts - index
GET /posts/create - create
POST /posts - post
GET /posts/:id - show
GET /posts/:id/edit - edit
PUT/PATCH /posts/:id - update
DELETE /posts/:id - destroy
AdonisJS & Resources
So far, we've learned what resource routes and controllers are. Now, let's go over how we can define one within AdonisJS.
AdonisJS makes it super easy to define routes for a resource. Off of the Route module is a method called resource. This method accepts two arguments, the first is the name of the resource and the second is the resource controller that should handle all the routes for the resource. From there, AdonisJS will automatically generate all the resource routes for the defined resource for us and automatically match the appropriate resource controller methods for us!
Creating A Resource Route & Controller
Let's continue with our posts example, so first, we'll define a set of resource routes for our post resource.
// start/routes.ts
Route.resource('posts', 'PostsController')
With this, AdonisJS will create the following routes for us. Also, note AdonisJS automatically names our routes as well using the same naming convention as our resource controller.
METHOD PATH Name
GET /posts posts.index
GET /posts/create posts.create
POST /posts posts.store
GET /posts/:id posts.show
GET /posts/:id/edit posts.edit
PUT/PATCH /posts/:id posts.update
DELETE /posts/:id posts.destroy
Last, all we'd need to do is ensure we have a resourceful PostsController
created for the resource route to utilize.
node ace make:controller Post --resource
That'll create the following controller for us.
// app/Controllers/Http/PostsController.ts
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class PostsController {
public async index ({}: HttpContextContract) {
}
public async create ({}: HttpContextContract) {
}
public async store ({}: HttpContextContract) {
}
public async show ({}: HttpContextContract) {
}
public async edit ({}: HttpContextContract) {
}
public async update ({}: HttpContextContract) {
}
public async destroy ({}: HttpContextContract) {
}
}
With that, we've successfully defined a complete set of routes and controller methods to perform CRUD operations for our post resource. Of course, we'll want to handle the route by performing their specific actions, but we'll be covering this later in this series when we discuss Lucid.
Excluding Routes
In some cases, you're not going to need a complete set of resourceful routes for your resource. You may only need routes to create, update, and delete a resource record without any of the display functionality. AdonisJS allows us to easily exclude routes from a resource by chaining an exclude
method off our resource route definition containing an array of the methods we don't need.
So, for example, if we only needed store, update, and destroy to create, update, and delete a resource record we could define our resource like so:
// start/routes.ts
Route.resource('posts', 'PostsController')
.except(['index', 'create', 'show', 'edit'])
However, if like in the example above you need to exclude more than you're including it may be easier to use the inverse and define only the methods we're going to need.
// start/routes.ts
Route.resource('posts', 'PostsController')
.only(['store', 'update', 'destroy'])
Both of these approaches will only define the following resource and exclude the remaining.
METHOD PATH Name
POST /posts posts.store
PUT/PATCH /posts/:id posts.update
DELETE /posts/:id posts.destroy
Excluding Routes for an API
When you're developing an API, you aren't going to need the two resource routes used to display the create and edit forms. These two routes are named create and edit. Instead of manually typing these routes out as exclusions on each resource AdonisJS provides a helper called apiOnly
that will exclude these routes for you.
// start/routes
Route.resource('posts', 'PostsController')
.apiOnly()
Middleware
Middleware can be applied to a resource as a whole and individual routes within a resource. To allow this, when chaining the middleware method off a resource the method will accept an object instead of the standard string or array.
To assign middleware to the entire resource, use the key '*'
to define the middleware inside the object argument. Otherwise, you can specify the individual methods to apply the middleware to.
// start/routes.ts
Route.resource('posts', 'PostsController')
.middleware({
'*': ['auth'],
update: ['owner'],
destroy: ['ownerOrAdmin']
})
Here the auth middleware is applied to the entire resource. While owner
is only applied to the update route and ownerOrAdmin
is only applied to the destroy route.
Altering Resource Name
In some cases, you may want to use a different name for your resource due to personal preference or other reasons. In these cases, you can add the as
method to your resource chain and whatever name you provide it will then replace the resource name on each of the resource's route names.
// start/routes.ts
Route.resource('posts', 'PostsController')
.as('lessons')
The generated routes would then be:
METHOD PATH Name
GET /posts lessons.index
GET /posts/create lessons.create
POST /posts lessons.store
GET /posts/:id lessons.show
GET /posts/:id/edit lessons.edit
PUT/PATCH /posts/:id lessons.update
DELETE /posts/:id lessons.destroy
Nested Resources
Sometimes your resource may belong to another resource. For example, comments belong to a specific post. Therefore, we may need both the post's id and the id for the comment within the comments resource. We can utilize dot notation to specify that our comments resource is nested or a child of the posts resource.
// start/routes.ts
Route.resource('posts', 'PostsController')
Route.resource('posts.comments', 'CommentsController')
The dot notation in the name will then prefix our comments resource with that of the posts resource, so the following routes will be generated.
POSTS RESOURCE
METHOD PATH Name
GET /posts posts.index
GET /posts/create posts.create
POST /posts posts.store
GET /posts/:id posts.show
GET /posts/:id/edit posts.edit
PUT/PATCH /posts/:id posts.update
DELETE /posts/:id posts.destroy
POSTS.COMMENT RESOURCE
METHOD PATH Name
GET /posts/:post_id/comments posts.comments.index
GET /posts/:post_id/comments/create posts.comments.create
POST /posts/:post_id/comments posts.comments.store
GET /posts/:post_id/comments/:id posts.comments.show
GET /posts/:post_id/comments/:id/edit posts.comments.edit
PUT/PATCH /posts/:post_id/comments/:id posts.comments.update
DELETE /posts/:post_id/comments/:id posts.comments.destroy
Our posts.comment
resource is prefixed with /posts/:post_id
so each comment handler will have access to the specified post id via params.post_id
. Then, the comment resource is then created just like a normal resource after that prefix. The same applies for each route's name.
Shallow Resources
In some cases, with nested resources, you aren't going to need the parent resource's id when you already have the child resources id. In these cases, we can exclude the parent resource's path when we have the child resource's id by creating a shallow resource instead of a nested resource.
Unlike nested resources, shallow resources actually have a specific method to define it's routes, called shallowResource
. We can then define the nested resource the same as before.
// start/routes.ts
Route.resource('posts', 'PostsController')
Route.shallowResource('posts.comments', 'CommentsController')
As you can see the arguments remain the same, only the method we're calling changes. However, the greater change is within the generated routes.
POSTS RESOURCE
METHOD PATH Name
GET /posts posts.index
GET /posts/create posts.create
POST /posts posts.store
GET /posts/:id posts.show
GET /posts/:id/edit posts.edit
PUT/PATCH /posts/:id posts.update
DELETE /posts/:id posts.destroy
POSTS.COMMENT RESOURCE
METHOD PATH Name
GET /posts/:post_id/comments posts.comments.index
GET /posts/:post_id/comments/create posts.comments.create
POST /posts/:post_id/comments posts.comments.store
GET /comments/:id posts.comments.show
GET /comments/:id/edit posts.comments.edit
PUT/PATCH /comments/:id posts.comments.update
DELETE /comments/:id posts.comments.destroy
So, now you can see the posts/:post_id
is only prefixed on our nested resource when the route doesn't have an id
route parameter on the child resource. The name of the routes is left as-is, so it matches that of a nested resource.
Next Up
We've now covered routing and controllers. The next thing we'll begin to dive into is services so we can easily define code once and use it multiple times across the same controller and different controllers.
Join The Discussion! (2 Comments)
Please sign in or sign up for free to join in on the dicussion.
alex-galhardo
Great Lesson!
Please sign in or sign up for free to reply
tomgobich
Thank you, Alex!!
Please sign in or sign up for free to reply