Resources & Resource Controllers

Last updated 6 months ago

In the lesson about creating a service, we created a service to manage the rentals. We then updated the rental.get method in the rental controller to return the list of rentals managed by the rental service. There were other methods in the rental service, such as create(), get(id), and remove(id), that we did not use. So, let's update our application to provide routes that call use these methods.

Implementing a Resource Controller

Declaring a Resource Controller

A resource controller is a specialized controller that supports CRUD operations (i.e., create, retrieve, update, and delete). Blueprint provides a base implementation of a resource controller, which you can extend in your application as needed. Since we have a service that supports CRUD operations, let's update the rental controller become a resource controller.

First, update the rental controller by changing Controller to ResourceController, and define the name property on the controller as rental.

app/controllers/rental.js
const {
ResourceController,
Action,
service
} = require ('@onehilltech/blueprint');
/**
* @class rental
*/
module.exports = ResourceController.extend ({
name: 'rental',
rentals: service (),
get () {
return Action.extend ({
execute (req, res) {
const data = this.controller.rentals.rentals;
res.status (200).json ({ data });
}
})
}
});

What we did was extended the ResourceController instead of the Controller. This gives our controller the ability to selectively extend the actions defined in the ResourceController class. The get() method is one of the methods defined on the ResourceController, so we do not have do anything.

The ResourceController extends the Controller, which is why theResourceController is still considered a Controller.

We also defined the name property. This is a required property because the router uses this name when it is auto-generating the routes for the corresponding resource, as shown in the table below with their corresponding method on the ResourceController.

Method

Verb

Path

Description

create()

POST

/

Create a new resource

getAll()

GET

/

Query the resources

getOne()

GET

/:nameId

Get a single resource

update()

PUT

/:nameId

Update an existing resource

delete()

DELETE

/:nameId

Delete an existing resource

Implementing Its Actions

When implementing the actions on of a resource controller, you only need to override methods that correspond to operations that you want to support. In our super-rental API server, we only want to support the following CRUD operations:

  • create

  • getOne

  • getAll

  • delete

The ResourceController methods that are not overridden return a 404 Not Found HTTP response to the client.

Let's first implement the getOne() and getAll(). The getOne() method is define an action that returns a single resource and the getAll() method is define an action that returns resources that match the query. If there is no query, then we should return all the resources. Below is the implementation of getOne() and getAll().

app/controllers/rental.js
const {
ResourceController,
Action,
service
} = require ('@onehilltech/blueprint');
/**
* @class rental
*/
module.exports = ResourceController.extend ({
name: 'rental',
rentals: service (),
// query all the resources
getAll () {
return Action.extend ({
execute (req, res) {
const data = this.controller.rentals.rentals;
res.status (200).json ({ data });
}
})
},
// get a single resources, or return 404 Not Found.
getOne () {
return Action.extend ({
execute (req, res) {
const { rentalId } = req.params;
const rental = this.controller.rentals.get (rentalId);
if (rental) {
res.status (200).json ({ data: [rental] });
}
else {
res.sendStatus (404);
}
}
})
}
});

As shown in the code above, we implement getAll() by changing the method name from get() to getAll(). Later, we will discuss how to update the getAll() action to support queries. The getOne() method is similar to the getAll() method. The main difference is we extract the resource id from the req.params.rentalId property on the request object. This is because the resource name is used to construct the dynamic path for the resource (e.g., GET /:rentalId).

The req and res object are Express request and response objects, respectively.

Now, the route http://localhost:5000/api/rentals will still return the list of rentals. And, you can access the data for each rental via their own url.

We can implement the delete() method in a similar manner as the getOne() method. For example, we get the resource id from req.params.rentalId. We then pass this value to the remove() method on the rentals service. Here is the rental controller with the delete() method implemented.

app/controllers/rental.js
module.exports = ResourceController.extend ({
/// ....
delete () {
return Action.extend ({
execute (req, res) {
const { rentalId } = req.params;
const result = this.controller.rentals.remove (rentalId);
res.status (200).json (result);
}
});
}
});

The last method we need to implement is the create() method. When a client makes a request to create a rental, it will do so using the POST HTTP verb. The data about the rental will be available on the req.body property. To implement the create() method, we need to get the rental data from req.body, and pass it to the add() method on the rentals service. Below is the implementation of the create() method.

app/controllers/rental.js
module.exports = ResourceController.extend ({
// ...
create () {
return Action.extend ({
execute (req, res) {
const { rental } = req.body;
this.controller.rentals.add (rental);
res.status (200).json ({data: [rental]});
}
})
}
});

Declaring Resources in Router

We have defined the controller actions for managing the rental resources. We now need to declare the routes that invoke each action we implemented above. One approach is to define each route the same way we did when we defined the original route for getting the rentals. For example, we could update the rental router with the following specification.

app/routers/rental.js
const { Router } = require ('@onehilltech/blueprint');
module.exports = Router.extend ({
specification: {
'/rentals': {
post: { action: 'rental@create' },
get: { action: 'rental@getAll' },
'/:rentalId': {
get: { action: 'rental@getOne' },
delete: { action: 'rental@delete' },
}
}
}
});

This approach works, but we end up writing the same code each time we need to declare the routes for a resource. Instead, let's use the resource property that is available when defining a route. The following code is equivalent to the code above.

app/routers/rental.js
const { Router } = require ('@onehilltech/blueprint');
module.exports = Router.extend ({
specification: {
'/rentals': {
resource: { controller: 'rental' }
}
}
});

The resource property will auto-generate the routes for a resource using the name defined by the associated resource controller. In this case, the name defined in by the rental resource controller is rental.

By default, the resource property will generate routes that bind with each action supported by the resource controller. For our example, however, we only support a subset of the actions (i.e., create, get, and delete). We can therefore use the allow property to specify the routes the resource controller supports, or deny to specify the routes the resource controller does not support. In our example, we are going to use the allow property to specify the support routes on the resource controller. Below is the updated example.

app/routers/rental.js
const { Router } = require ('@onehilltech/blueprint');
module.exports = Router.extend ({
specification: {
'/rentals': {
resource: {
controller: 'rental',
allow: ['create', 'getOne', 'getAll', 'delete']
}
}
}
});

When the client tries to update a rental resource, the Blueprint application will respond with 404 Not Found.