As I blogged last week, AngularJS provides the $resource class as an abstraction layer between your client-side code and the server-side API. This makes performing CRUD-style operations across the network fairly easy. But what happens when you need to execute a command on a RESTful resource that falls outside the standard CRUD (ie. Create, Read, Update, Delete) methods? Luckily, AngularJS handles RESTful "controllers" quite nicely.
According to The REST API Design Rulebook by Mark Masse, there are four resource archetypes:
- Document - A resource representation.
- Collection - A collection of server-managed data.
- Store - A collection of client-managed data.
- Controller - An action to perform.
Document, Collection, and Store are all about representing resources. Controllers, on the other hand, are about mutating resources. You can already use HTTP verbs to mutate a resource; but, sometimes, run-of-the-mill CRUD operations don't quite make sense. Take, as an example, the following resources:
Given this Collection of messages (first resource) and an individual message (second resource), how would you go about clearing all messages? Or, how about archiving the given message? CRUD doesn't quite fit the bill. But, Controllers work perfectly for these types of operations:
Here, we're using the "clear-all" Controller and the "archive" Controller to mutate the Collection and the Document representations, respectively.
When we start using Controllers in our resources, we are now left with a URL schema that has more variability. AngularJS $resource's can handle this; but, you have to get a little "clever" with how you define your parameter bindings.
Pulling the messages concept from above into an AngularJS context, we'd have to define our resource using the following template:
The problem with this is that there is now a slight ambiguity around the first URL parameter. Is ":id" referring to my Collection-based Controller? Or is it referring to my Document ID?
To clear up this ambiguity, AngularJS allows us to define multiple parameters in the same portion of the URL template:
Notice that the second item in my URL template contains two parameters:
As long as I only use one of these at a time, AngularJS will construct the RESTful resource properly. To demonstrate, I've put together a demo that defines and then invokes the messages resource from above:
Notice that my AngularJS resource accounts for two different controllers:
- :listController - acting on our message collection.
- :docController - acting on a specific message.
When I invoke the resource methods above, I end up making four requests to the following URLS:
- GET http://.../api.cfm/messages
- POST http://.../api.cfm/messages/clear-all
- GET http://.../api.cfm/messages/4
- POST http://.../api.cfm/messages/8/archive
As you can see AngularJS properly negotiated the appropriate RESTful URLs. Hella sweet!
If you are interested, here is my test API file:
<!--- Get the raw resourced path that was requested. ---> <cfset resourcePath = cgi.path_info /> <!--- NOTE: ColdFusion 10 seems to hang until I make a request to get the POST body. Not sure why. ---> <cfif ( cgi.request_method neq "GET" )> <cfset requestBody = getHTTPRequestData().content /> </cfif> <!--- Identify the type of request that has come in based on the pattern of the requested resource path. ---> <cfif reFind( "^/messages$", resourcePath )> <cfset response = "GET without ID." /> <cfelseif reFind( "^/messages/clear-all$", resourcePath )> <cfset response = "POST with clear-all Controller." /> <cfelseif reFind( "^/messages/\d+$", resourcePath )> <cfset response = "GET with ID." /> <cfelseif reFind( "^/messages/\d+/archive+$", resourcePath )> <cfset response = "POST with archive controller" /> <cfelse> <cfset response = "Hmm, couldn't match resource." /> </cfif> <!--- Serialize the response as JSON. AngularJS will know how to parse this. ---> <cfset serializedResponse = serializeJSON( response ) /> <!--- Add header for debugging the path. ---> <cfheader name="X-Debug-Path" value="#cgi.path_info#" /> <!--- Stream response back to the client. ---> <cfcontent type="application/json" variable="#charsetDecode( serializedResponse, 'utf-8' )#" />
Want to use code from this post? Check out the license.