Route & Controller Resources

In this lesson, we'll learn what a resource is and how we can quickly and easily define a route resource and handle each resourceful route with a resource controller.

Published
Dec 02, 21
Duration
13m 42s

Developer, dog lover, and burrito eater. Currently teaching AdonisJS, a fully featured NodeJS framework, and running Adocasts where I post new lessons weekly. Professionally, I work with JavaScript, .Net C#, and SQL Server.

Adocasts

Burlington, KY

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.

  1. Commented 2 months ago

    Great Lesson!

    2

    Please sign in or sign up for free to reply

    1. Commented 2 months ago

      Thank you, Alex!!

      1

      Please sign in or sign up for free to reply

Playing Next Lesson In
seconds