Using RESTful Controllers In An AngularJS Resource

Posted October 17, 2012 at 8:23 AM by Ben Nadel

Tags: ColdFusion, Javascript / DHTML

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:

  • /messages
  • /messages/4

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:

  • /messages/clear-all
  • /messages/4/archive

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:

  • /messages/:id/:controller

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:

  • /messages/:listController:id/:docController

Notice that the second item in my URL template contains two parameters:

  • :listController
  • :id

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:

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  • <title>Using RESTful Controllers In AngularJS Resources</title>
  •  
  • <!--
  • Because we are working with Resources, we have to load both
  • AngularJS and the ngResource module.
  • -->
  • <script type="text/javascript" src="../angular-1.0.2/angular.js"></script>
  • <script type="text/javascript" src="../angular-1.0.2/angular-resource.js"></script>
  • <script type="text/javascript">
  •  
  •  
  • // Tell Angular to load the ngResource before loading our
  • // custom app module.
  • var app = angular.module( "Demo", [ "ngResource" ] );
  •  
  • // Run this when the app is ready.
  • app.run(
  • function( $resource ) {
  •  
  •  
  • // When defining the resource, we get a few actions
  • // out of the box, like get() and query(), based on
  • // standard HTTP verbs. But, we can also use RESTful
  • // controller to change resource state using an
  • // action that falls outside normal CRUD operations.
  • var messages = $resource(
  • "./api.cfm/messages/:listController:id/:docController",
  • {
  • id: "@id",
  • listController: "@listController",
  • docController: "@docController"
  • },
  • {
  • clear: {
  • method: "POST",
  • params: {
  • listController: "clear-all"
  • }
  • },
  • archive: {
  • method: "POST",
  • params: {
  • docController: "archive"
  • }
  • }
  • }
  • );
  •  
  • // Now that we have our resource defined, let's invoke
  • // it with various configurations.
  •  
  • // GET without ID.
  • messages.query();
  •  
  • // Post with our list controller.
  • messages.clear();
  •  
  • // GET with ID.
  • messages.get(
  • {
  • id: 4
  • }
  • );
  •  
  • // POST with our document "controller".
  • messages.archive(
  • {
  • id: 8
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • </script>
  • </head>
  • <body>
  • <!-- Left intentionally blank. -->
  • </body>
  • </html>

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:

api.cfm

  • <!--- 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' )#"
  • />

For some reason, ColdFusion 10 was hanging on POST requests until I accessed the request body (via the getHttpRequestData() function). This only happened if the request body contained JSON (JavaScript Object Notation) data. If it was empty, the request did not hang. I don't know enough about request processing to know if that makes any sense. I can, however, confirm that ColdFusion 9 does not do this.


You Might Also Be Interested In:



Reader Comments

Nov 8, 2012 at 12:04 PM // reply »
63 Comments

First Suspect ...

https://bugbase.adobe.com/index.cfm?event=bug&id=3177732


Nov 8, 2012 at 12:06 PM // reply »
63 Comments

Argh ... Sorry wrong bug ... ;-)

https://bugbase.adobe.com/index.cfm?event=bug&id=3305486


Nov 27, 2012 at 7:39 AM // reply »
1 Comments

Thx for this post. It was very helpful; especially this: /messages/:listController:id/:docController. Didn't find any mention of multiple params in the same url portion in the angular docs or anywhere else


Dec 6, 2012 at 7:26 PM // reply »
1 Comments

I would love to see how you validate users' access to your rest resources.


Dec 8, 2012 at 8:47 PM // reply »
2 Comments

Hi Ben,

Is it possible to do CRUD interactions with Angular without REST services?

Best,
Pardeep.


Jan 11, 2013 at 8:54 PM // reply »
11,243 Comments

@Pardeep,

From what I understand, the $resource module (ie. ngResource) is a RESTful wrapper to the underlying $http service. If you want to, you can just reach down and use the $http service directly (very much akin to jQuery's $.ajax() method I believe).

From what I can tell, the $http service returns promise object ( $q ); $resource, on the other hand, returns empty object / array instances.


Jan 11, 2013 at 8:57 PM // reply »
11,243 Comments

@Atomi,

On the client side, we do security by fact of only certain user interfaces gets rendered for certain users. Beyond that, the client doesn't handle any "smarter" validation. On the server, we then authenticate the user on each request and make sure they have permissions to make the given request.

So, on the client, we have UI-based security; then on the server, we have full, detailed security.


Jan 23, 2013 at 2:29 PM // reply »
1 Comments

Very clever, this is exactly what I was trying to do, and I just didn't think about adding another optional parameter there. Thanks for getting me un-stuck~


Jan 23, 2013 at 3:06 PM // reply »
63 Comments

Okay Ben ... You got me hooked on Angular ...

I've been stalking the webs for the last week or so trying to get up to speed on Angular ...

One pattern I've noticed some of the more experienced NIO developers using is wrapping their module initialization into an anon function for encapsulation ...

===============================
https://gist.github.com/4612238
===============================

Personally I'm using Angular MV on the front with Spring 3 MC on the back ...

OOP through and through ...

I'm basically trying to emulate the pattern that LinkedIn is using by having a platform agnostic front-end via json and REST ...

Here's an awesome post on LinkedIn's engineering team discussing how they've embraced progressive advancement through layering all their technologies ...

http://engineering.linkedin.com/frontend/leaving-jsps-dust-moving-linkedin-dustjs-client-side-templates

Hope this ain't too spammy ;-)


Jan 24, 2013 at 12:54 AM // reply »
11,243 Comments

@Brad,

My pleasure! Glad to help.

@Edward,

As far as wrapping the code in an anonymous function, that's what I do as well when it comes to my real code. I take it is an opportunity to abbreviate the AngularJS library and my app module:

  • (function( ng, app ){
  • . . .
  • })( angular, myAppModule );

angular => ng
myAppmodule => app

I find this makes the subsequent code easier to write :)

I'll have to take a look at the LinkedIn stuff; we connect to an API that is pretty much a REST(ish) API, returns JSON. But, we have two hooks into it for different types of authentication (ex. Cookie vs. Basic Auth).


Jan 24, 2013 at 10:47 AM // reply »
63 Comments

@Ben,

Absolutely ... I'm a big fan of modularity and prototypical {real word?} objects. Great way to keep code manageable :-)


Apr 10, 2013 at 2:30 AM // reply »
1 Comments

Hey guys,

I've made a Service for AngularJS to be able to use RestFul backends easily. Please check https://github.com/mgonto/restangular


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 23, 2013 at 9:55 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Dan, According to the CF Admin, I'm running Java "1.6.0_45". As far as the DB column, in the database it's an INT. I'll see if I can dig into what CF sees it as. @WebManWalking, But h ... read »
May 23, 2013 at 9:49 AM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Ben, I think the problem is that we're used to loose typing in ColdFusion, like JavaScript. If a value is a number but it's needed in an expression to be a string, noooo problem. I've encountered ... read »
May 23, 2013 at 9:47 AM
ColdFusion QueryAppend( qOne, qTwo )
You rock! Thank you, thank you, thank you!!! ... read »
May 23, 2013 at 5:19 AM
Ask Ben: Print Part Of A Web Page With jQuery
How to print also the background color of table cells and table lines ... read »
May 23, 2013 at 3:55 AM
Javascript Array Methods: Unshift(), Shift(), Push(), And Pop()
very interesting and helpful too. ... read »
May 22, 2013 at 5:35 PM
Script Tags, jQuery, And Html(), Text() And Contents()
This is still an issue 2 years later. jQuery is supposed to remediate these cross browser issues, no? I have been unable to find any statement from the jQuery team calling this behavior "by de ... read »
May 22, 2013 at 12:44 PM
Ask Ben: Query Loop Inside CFScript Tags
In cf10, if you call a function that has: local.result = {}; local.result.msg = ""; local.svc = new query(); local.svc.setSQL("SELECT * FROM..."); local.obj = local.svc.exe ... read »
May 22, 2013 at 12:29 PM
Strange Interaction Between DeserializeJson(), ArrayContains(), And Database Values In ColdFusion
@Ben: What version of Java are you using? Also, did you test users.id to see what Java reports as the data type? I wonder if it's not a Java primitive data type, but getting returned as something ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools