Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at cf.Objective() 2012 (Minneapolis, MN) with: Erik Meier and Jesse Shaffer

Building A Twitter-Inspired RESTful API Architecture In ColdFusion

By Ben Nadel on
Tags: ColdFusion

A while back, I gave a ColdFusion API presentation at Scotch On The Rocks 2010. While I think the content of the presentation was decent, I don't think that it had any right being a presentation. As someone accurately described it in a review (sorry, I can't remember who it was), the presentation was like attending one of my blog posts. This is absolutely true; so, rather than trying to record a video of it for this site, I thought I would just turn it into the RESTful API blog entry that it should have been.

With the incredible popularity of AJAX, scripting libraries like jQuery, and the rapidly increasing field known as, "Software as a Service (SaaS)," APIs (Application Programming Interfaces) are becoming more important than they have ever been before. It's no longer sufficient to think about the simple request-response life-cycle of HTML web pages; now, we have to worry about data transformations, data serialization, status codes, cookie-free session management, security, and so on and so forth. Creating an API presents a whole host of challenges (some old, some new) in a completely different context.

Building an API is very new to me; I'll tell you right now, I don't know all that much about building one. But, I love to learn; and, I find one of the best ways to learn is to look at what the "masters" are doing. For API development, I decided to take a look at Twitter - one of the world's most popular APIs - and see if I could imitate the kinds of functionality that their API was presenting.

When you are start something new, there's always the feeling of, "what if I screw this up?" When it comes to building an API, this feeling can be mitigated by the fact that an API creates a clean separation between the people using your code and the logic behind your code. This beautiful encapsulation of logic means that you can create your API now and then change the internal workings of your API later without disrupting the people who depend on it.

That said, what kind of functionality does Twitter bake into its API? While this is not a comprehensive list, these are the features that I am going to try to emulate with my API knock-off:

  • RESTful URL architecture
  • Basic authentication
  • Multi-format responses
  • Meaningful status code usage
  • Duplicate-post protection
  • Rate limiting

Before we get into the code itself, let's take a brief look at each of these features a bit more closely.

RESTful URL Architecture

REST stands for, "Representational State Transfer." In a RESTful architecture, you have a number of unique "resources" that are defined by the script-name being accessed. The resource is intended to be fully-defined by the resource (script-name) path while the query string is only meant to provide filtering and data-transformation cues. So, in a system where you might normally pass a unique ID as a query string variable, in a RESTful API, you'd actually build that unique ID into the resource path:

Typical: /index.cfm?action=users.view&id=4&returnFormat=json
RESTful: /users/4.json

As you can see above, there are number of differences between a typical URL and a RESTful URL. As I mentioned before, the unique ID of the target resource becomes part of the resource path - not part of the query string. In a RESTful URL, we are also hiding the underlying technology (.cfm file extension) and replacing it with the return format (.json). A proper RESTful architecture is meant to abstract away the underlying technology; the server-side technology is not relevant to the API itself and, as such, should not be represented in the resource URLs.

In a RESTful architecture, you can access resources using the GET verb (default browser request behavior). You can also mutate a resource (or set of resources) using the POST, PUT, and DELETE verbs. While POST, PUT, and DELETE all have different intentions (ie. update, create, delete), I am going to be using GET and POST as these are the most commonly supported HTTP actions.

Not all resources need to support all verbs. Verb detection should be done on a resource-by-resource basis.

Basic Authentication

In Basic Authentication, we pass our username and password values as a Base64-encoded string in the HTTP headers of our request. In ColdFusion, we provide basic authentication whenever we use the Username and Password attributes of the CFHTTP tag. The HTTP header that this creates looks something like:

Authorization: Basic dHJpY2lhOm5hdWdodHk=

The Base64 portion of this header is an encoded version of "username:password". Unless you request this over an SSL connection, basic authentication is not a secure approach; at best, it offers obfuscation of the credentials.

NOTE: For Twitter developers, Basic Authentication is going to be phased out of Twitter. In the very near future, Twitter will only support oAuth sign-ins.

Multi-Format Responses

As I explained in the RESTful URL Architecture section above, the underlying, server-side technology is abstracted away and replaced with the desired return format of the requested resource. Twitter allows its resources to be returned as JSON or XML by using either the ".json" or ".xml" file extensions respectively.

Technically, I believe you can think of the return format as a filter of the resource itself. As such, I believe this would be equally valid as the file extension or as a query string variable. However, since Twitter uses the file-extension approach, that is what we will be using as well.

While not all resources need to support the same response formats, for our purposes, we are going to keep this very simple: all resources will support both JSON and XML generic response formats.

Meaningful Status Codes

While status codes are typically meaningless to the normal web user, they are critical when it comes to an API. If an error occurs (pretty much anything other than a 200 OK response), a meaningful status code provides excellent insight into what went wrong with the request. Twitter uses the following status codes in its API responses:

  • 200 - OK - Everything went fine.
  • 400s - all caused by user interaction
  • 400 - Bad Request - The request was malformed or the data supplied by the end user was not valid. This is also used if the user has exceeded their API usage limit.
  • 401 - Unauthorized - The user is not authorized to access the requested resource. Additional information is typically supplied within the response to tell the end user (API consumer) how to authorize itself (ie. BASIC Authentication).
  • 403 - Forbidden - The user has exceeded their post limit (not bloody likely).
  • 404 - Not Found - The requested resource was not found.
  • 405 - Method Not Allowed - The user attempted to use a verb (ex. GET, POST) on a resource that had no support for the given verb.
  • 406 - Not Acceptable - The user requested a return format (ex. JSON, XML) that is not currently supported.

In my code, you'll see that we create a meaningful status code response architecture through a heavy use of CFTry, CFCatch, and CFThrow tags.

Duplicate-Post Protection

In Twitter, if you post two identical status updates in a row, the API will assume this was a mistake and ignore the second post. As a response, it will simply return to the user on the second response the same thing that it returned on the first response.

Rate Limiting

As you all probably know (and have experienced first-hand), Twitter limits the number of API requests that a user can make in a certain period of time. Typically, this comes into play when a user performs too many GET operations (ex. getting new status updates); such a situation results in a 400 Bad Request response. In much rarer cases, a user can also perform too man PUT operations (ex. posting a new status update); such a situation results in a 403 Forbidden response.

In any case, good or bad requests, Twitter provides some insightful HTTP headers that give the API consumer an understanding of what rate-limiting activity is being performed:

  • X-RateLimit-Limit - How many requests the user is allowed to make in an hour.
  • X-RateLimit-Remaining - How many requests the user can make in the remaning time (before they exceed their rate limit).
  • X-RateLimit-Reset - The time at which the rate limit will be reset (when they enter a new time period).

Putting It All Together To Mimic The RESTful Twitter API

Now that we've seen what kind of functionality Twitter provides in its RESTful API, let's see how we can take those same goals and reach them using ColdFusion. To keep this as simple as possible, I've created a RESTful API that revolves around only two resources, each of which supports a single verb (ie. GET, POST):

GET: /girls/{name}/kiss.{format}

POST: /girls/{name}/french-kiss.{format}

As you can see, the "unique" identifier of each girl - their name - is part of the resource URL, not part of the query string. In order to follow this RESTful standard, I have to have a system that allows for dynamic URLs. Typically, this would be supported with URL rewriting (such as IIS Mod-Rewrite or Apache's mod_rewrite); however, to keep this demo as simple and as portable as possible, I've provided support for this architecture through the use of CGI's path_info property. So, rather than calling the resource directly, it will be appended to an API script:

api.cfm/girls/{name}/kiss.{format}

Notice that our RESTful "resource" comes after the "api.cfm" ColdFusion script-name.

The way I see it, in a RESTful architecture, the "REST" aspect of the HTTP request is a concern of the Controller layer (when thinking in terms of MVC - Model-View-Controller). As such, all of our RESTful behavior and logic will sit between the incoming client request and the underlying Service layer:

 
 
 
 
 
 
RESTful API Controller Layer (MVC) In A RESTful Architecture. 
 
 
 

As you can see, the REST layer is responsible for controlling the incoming request, passing it off to the service layer, and then packaging the response in a way that is meaningful to the client.

For the sake of simplicity, our demo application doesn't really have a service layer. However, if it did, it might be used for various aspects of the RESTful request such as for user authentication, rate limiting, and of course, processing the actual resource.

During a RESTful page request, there are many things that can go wrong. Between the resource formatting, return format, verb selection, user authorization, rate limiting, service layer interaction, and status code creation - to name a few places - there's simply a lot of errors that can pop up. And, when you realize that each of these errors has to result in a well-packaged status code and error message, handling those errors gets even more complicated.

In my experience, embracing the CFTry, CFCatch, and CFThrow tags can result in a much more straightforward work flow. In my code, what you'll see is that every time a problem arises (ex. user is not authorized), I use the CFThrow tag to raise a typed-exception. By doing this, it allows me to assume, at every single step of the page work flow, that all of the previous steps in the work flow have executed properly. This exception-centric processing not only removes a tremendous amount of the conditional logic that one might ordinarily need, it also allows us to package our error responses in a centralized fashion:

 
 
 
 
 
 
CFTry / CFCatch / CFThrow Work Flow In A RESTful API Architure Helps Make Appopriate Status Code Responses Easier To Package. 
 
 
 

Ok, now that we have a high-level understanding of what the page flow is going to look like, let's finally dig into some code.

As I mentioned before, I am going to be using an "api.cfm" page in conjunction with path_info to allow for dynamic URLs. This demo does have an Application.cfc; but, if you can accept the fact that all it does is setup the variable, "application.sessions," then, I won't bother showing it.

The following api.cfm does a majority of the boiler-plate processing. Resource-specific processing will be broken out into its own CFInclude templates:

API.cfm (The Core RESTful Gateway)

  • <!---
  • This demo API is going to be kept super simple. It supports the
  • following resources:
  •  
  • GET /girls/{name}/kiss.format
  • POST /girls/{name}/french-kiss.format
  • --->
  •  
  • <!---
  • When processing this API request, we are going to deal with a lot
  • of opportunities for failure. As such, we are going to execute
  • the entire API processing in a CFTry / CFCatch such that we can
  • catch the various errors that might need to be dealt with.
  • --->
  • <cftry>
  •  
  •  
  • <!---
  • As the API request is processed, we're going to need to
  • collect information about the request and the response. This
  • might valid data, this might be a collection of errors.
  • Ultimately, this information is what will be used to
  • formulate the response to the client.
  •  
  • Username - The authorized user.
  • Password - The authorized user's password.
  • Resource - The resource URI requsted by the user.
  • Method - The type of request (get, post).
  • Data - The data returned from the API resposne.
  • Errors - A collection of errors.
  • Format - The format (json,xml) or the response.
  • Headers - The collection of additional response HTTP headers.
  • StatusCode - The HTTP status code of the response.
  • StatusText - The HTTP status text of the response.
  • MimeType - The MimeType of the response.
  • Content - The serialized version of the response.
  • Session - Session data for this user accross requests.
  • RateLimit - The max number of requests the user can make per minute.
  • --->
  • <cfset api = {
  • username = "",
  • password = "",
  • resource = "",
  • method = cgi.request_method,
  • data = "",
  • errors = [],
  • format = "json",
  • headers = {},
  • statusCode = "200",
  • statusText = "OK",
  • mimeType = "",
  • content = "",
  • session = {},
  • rateLimit = 30
  • } />
  •  
  •  
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  •  
  •  
  • <!---
  • The first thing we want to do is check to see if the user
  • is making a valid request. In order to use the API, the user
  • must be using the "PATH_INFO". That is, they must be passing
  • additional REST-resource data as the path information of
  • their request:
  •  
  • api.cfm/MY/RESOURCE/PATH.{FORMAT}
  •  
  • As you can see, the REST-resource URI is what is comging
  • after the "api.cfm" value.
  •  
  • NOTE: If you were using URL-rewriting, you wouldn't need to
  • use PATH_INFO. But, in order to make this as portable as
  • possible, I am using it to demo the API work flow.
  •  
  • NOTE: If this check fails, we will have to rely on the
  • default data format (JSON) since we cannot be sure that user
  • is event requesting a valid data format.
  • --->
  •  
  • <!---
  • Check to make sure the script name and the path info are
  • NOT the same. If they are, that means that no path info
  • was provided.
  •  
  • NOTE: This is only needed for IIS. Apache does not send the
  • requested script as the PATH_INFO when none is available.
  • --->
  • <cfif (cgi.script_name eq cgi.path_info)>
  •  
  • <!--- Set the error message. --->
  • <cfset api.errors = [
  • "No resource was requested. Please use PATH_INFO to define the desired resouce. Example: ./api.cfm/MY/RESOURCE/PATH.json."
  • ] />
  •  
  • <!--- Raise a malformed request exception. --->
  • <cfthrow type="BadRequest" />
  •  
  • </cfif>
  •  
  • <!---
  • Now that we have checked that a resource was provided, let's
  • check to see if it has a valid format. Here, we are checking
  • that the resource has the general pattern of:
  •  
  • /path/to/resource.format
  • --->
  • <cfif !reFind( "^/\w+(/[^/]+)*\.\w+$", cgi.path_info )>
  •  
  • <!--- Set the error message. --->
  • <cfset api.errors = [
  • "The resource path you requested was not formatted properly. Valid resources are in the form of [/path/to/resource.format]."
  • ] />
  •  
  • <!--- Raise a malformed request exception. --->
  • <cfthrow type="BadRequest" />
  •  
  • </cfif>
  •  
  •  
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  •  
  •  
  • <!---
  • Now that we have checked that a resource we provided and is
  • generally in the right format, we need to check to see if
  • the format that was requested is supported by this API. The
  • requested format is the last part of the resource path, after
  • the period.
  •  
  • NOTE: I am demonstrating this format in an API-wide
  • implementation. You could also do this on a per-resource
  • way in which each unique resource (and VERB) could have its
  • own set of supported formats.
  • --->
  • <cfif reFindNoCase( "\.(json|xml)$", cgi.path_info )>
  •  
  • <!---
  • The format they requests is valid, so let's extract it
  • and store it for the response.
  • --->
  • <cfset api.format = listLast( cgi.path_info, "." ) />
  •  
  • <!---
  • Also, now that we have the format, we can extract
  • the actual resource URL, which is everything minus
  • the format.
  • --->
  • <cfset api.resource = reReplace(
  • cgi.path_info,
  • "\.[^.]+$",
  • "",
  • "one"
  • ) />
  •  
  • <cfelse>
  •  
  • <!--- The requested format is not currently supported. --->
  •  
  • <!--- Set the error message. --->
  • <cfset api.errors = [
  • "The format you requested [#listLast( cgi.path_info, '.' )#] is not currently supported by this API. Only JSON and XML formats are supported."
  • ] />
  •  
  • <!--- Raise a not acceptable format exception. --->
  • <cfthrow type="NotAcceptable" />
  •  
  • </cfif>
  •  
  •  
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  •  
  •  
  • <!---
  • Now that we know that the user is making a proper API
  • request, we need to make sure that the user is authorized
  • to make this API request. For this demo, we are going to
  • require all requests GET and POST to be authorized (we're
  • not going to be authenticating against any database).
  • --->
  •  
  • <!--- Get the request data. --->
  • <cfset requestData = getHTTPRequestData() />
  •  
  • <!---
  • When we check for authorization, there are a number of
  • things that can go wrong:
  •  
  • - Authorization was not provided
  • - Authorization is not formed properly
  • - Authorization is not valid
  •  
  • As such, we are going to perform this in its own try/catch
  • block and proceed as if the best case scenario is met.
  • --->
  • <cftry>
  •  
  • <!--- Get the authorization entry from the request data. --->
  • <cfset authorization = requestData.headers.authorization />
  •  
  • <!---
  • Get the encoded credentials. This is the base64 encoded
  • part of the authentication string. We can think of this
  • as the second item in a space-delimited list:
  •  
  • Example:
  • Basic YmVuQGJlbm5hZGVsLmNvbTpJTG92ZUNvbGRGdXNpb24=
  • --->
  • <cfset credentials = toString(
  • toBinary(
  • listLast( authorization, " " )
  • )
  • ) />
  •  
  • <!---
  • At this point, our credentials should be a colon-
  • delimited list of "username:password." In this approach,
  • we are using listGetAt() rather than listFirst() and
  • listLast() to ensure that if the list is not the right
  • length, an exception will be raised.
  • --->
  • <cfset api.username = listGetAt( credentials, 1, ":" ) />
  • <cfset api.password = listGetAt( credentials, 2, ":" ) />
  •  
  •  
  • <!---
  • For this demo, we are going to be very lose with the
  • authorizations. All we are going to require is that the
  • user has some sort of username and password. We're not
  • going to authorize against any database.
  • --->
  • <cfif !(
  • len( api.username ) &&
  • len( api.password )
  • )>
  •  
  • <!---
  • The credentials are not valid. Throw an error. This
  • will get caught along with any other error raised
  • during authorization.
  • --->
  • <cfthrow type="Unauthorized" />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Catch any exceptions that were raised during
  • authentication. This could be from a malformed request
  • or from invalid / missing credentials.
  • --->
  • <cfcatch>
  •  
  • <!--- Set the error message for the response. --->
  • <cfset api.errors = [
  • "Please provide a valid username and password (for this demo, any username and password will do)."
  • ] />
  •  
  • <!---
  • Raise an unauthorization exception. We cannot
  • simply REThrow the error since we don't know how
  • it was initially triggered.
  • --->
  • <cfthrow type="Unauthroized" />
  •  
  • </cfcatch>
  •  
  • </cftry>
  •  
  •  
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  •  
  •  
  • <!---
  • Now that we have authenticated the user, let's get the user's
  • session out of the application. Since we might have some race
  • conditions here, let's lock this down to the user's username.
  •  
  • The session allows us to maintain information about the user
  • across requests to help create a better user experience.
  • --->
  • <cflock
  • name="session-#api.username#"
  • type="exclusive"
  • timeout="5">
  •  
  • <!---
  • Check to see if the session object is already created in
  • the application cache.
  •  
  • NOTE: We'd probably want to do something like this in a
  • database or other caching mechanism to limit the side
  • that the application can get.
  • --->
  • <cfif !structKeyExists( application.sessions, api.username )>
  •  
  • <!--- Create a new session of this user. --->
  • <cfset application.sessions[ api.username ] = {
  • requests = []
  • } />
  •  
  • </cfif>
  •  
  • <!---
  • At this point, we know that the session either already
  • exists or was just created. In any case, get the existing
  • session reference.
  • --->
  • <cfset api.session = application.sessions[ api.username ] />
  •  
  • </cflock>
  •  
  •  
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  •  
  •  
  • <!---
  • Now that we have the user's session, we need to check to see
  • if they are staying in their rate limit. For now, their rate
  • limit is gonna be 30 requests per minute. These requests will
  • be stored in their requests array in date/time order.
  •  
  • Since this is altering shared data, we are gonna want to lock
  • the this down down a single user.
  • --->
  • <cflock
  • name="session-#api.username#"
  • type="exclusive"
  • timeout="5">
  •  
  • <!---
  • First, we want to append this request date/time stamp to
  • the end of the requests array.
  • --->
  • <cfset arrayAppend( api.session.requests, now() ) />
  •  
  • <!---
  • Next, we want to delete any requests from the user's
  • session that were made greater than 60 seconds ago.
  • --->
  • <cfloop condition="arrayLen( api.session.requests )">
  •  
  • <!---
  • Check to see if the first item is greater than 60
  • seconds ago.
  • --->
  • <cfif (dateDiff( "s", api.session.requests[ 1 ], now() ) gt 60)>
  •  
  • <!--- Delete the first item. --->
  • <cfset arrayDeleteAt( api.session.requests, 1 ) />
  •  
  • <cfelse>
  •  
  • <!---
  • We have cleared out any requests that are beyond
  • this rate-limitting window (and therefore will
  • not be used for rate limitting calculations). As
  • such, break out of this loop.
  • --->
  • <cfbreak />
  •  
  • </cfif>
  •  
  • </cfloop>
  •  
  •  
  • <!--- Add a header for the current rate limit in effect. --->
  • <cfset api.headers[ "X-RateLimit-Limit" ] = api.rateLimit />
  •  
  • <!---
  • Add a header for the number of requests the user has
  • left in this time frame. We can calculate this by the
  • rate limit minus the number of elements in the request
  • array.
  • --->
  • <cfset api.headers[ "X-RateLimit-Remaining" ] = max(
  • (api.rateLimit - arrayLen( api.session.requests )),
  • 0
  • ) />
  •  
  • <!---
  • Now, we know that all of the requests stored within this
  • array were made within the last 60 seconds. Check to see
  • if there are more than is allowed by the rate limit.
  • --->
  • <cfif (arrayLen( api.session.requests ) gt api.rateLimit)>
  •  
  • <!---
  • The user has exceeded the allowed rate limit. Set the
  • error message.
  • --->
  • <cfset api.errors = [
  • "You have exceeded your rate limit. Please try again in a little while."
  • ] />
  •  
  • <!--- Raise a bad request error. --->
  • <cfthrow type="BadRequest" />
  •  
  • </cfif>
  •  
  • </cflock>
  •  
  •  
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  •  
  •  
  • <!---
  • At this point, we have a resource and a format. Now, we
  • just need to make sure the resource is valid and has the
  • necessary data in the request.
  • --->
  • <cfswitch expression="#listFirst( api.resource, '/' )#">
  •  
  • <!--- Girls. --->
  • <cfcase value="girls">
  • <cfinclude template="api.girls.cfm" />
  • </cfcase>
  •  
  • <!--- Unknown resource. --->
  • <cfdefaultcase>
  •  
  • <!---
  • The user has asked for a resource that doesn't exist.
  • Don't worry about setting an error message at this
  • point because this is such a generic error - we can
  • put the message in the catch.
  • --->
  • <cfthrow type="NotFound" />
  •  
  • </cfdefaultcase>
  •  
  • </cfswitch>
  •  
  •  
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  •  
  •  
  • <!---
  • If we have made it this far, then our API request has
  • been succesfully processed (everything went off without
  • raising an exception). Store the current request in
  • the session.
  •  
  • We can use the previous resource and POST information in
  • order to prevent the user from submitting two duplicate
  • requests in a row (if we want to).
  • --->
  • <cflock
  • name="session-#api.username#"
  • type="exclusive"
  • timeout="5">
  •  
  • <!--- Store the resource that the user was working with. --->
  • <cfset api.session.prevResource = api.resource />
  •  
  • <!---
  • Store the previous FORM information. We only need to
  • worry about the form data since POST is the only type
  • of method that can mutate information.
  • --->
  • <cfset api.session.prevForm = duplicate( form ) />
  •  
  • <!--- Store the previous response. --->
  • <cfset api.session.prevData = api.data />
  •  
  • </cflock>
  •  
  •  
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  •  
  •  
  • <!---
  • Catch any exceptions raised during API processing. Since we
  • were raising exceptions with explicit Types, we are going to
  • want to response to those types on a case-by-case basis.
  • --->
  •  
  •  
  • <!---
  • Catch any unauthorized errors in which the user did not
  • provide the appropirate credentials.
  • --->
  • <cfcatch type="Unauthroized">
  •  
  • <!---
  • If the current page requires authentication (status code
  • 401), then set authorization header. This just let's the
  • client know what kind of authorization is supported.
  •  
  • NOTE: If using the browser to access API directly,
  • cancel out of the first prompt box if is asking for NTLM
  • authentication (based on your server configuration).
  • --->
  • <cfset api.headers[ "WWW-Authenticate" ] = "basic realm=""API Demo""" />
  •  
  • <!--- Set the status code and response text. --->
  • <cfset api.statusCode = "401" />
  • <cfset api.statusText = "Unauthorized" />
  •  
  • <!--- Overwrite the data key with the errors. --->
  • <cfset api.data = api.errors />
  •  
  • </cfcatch>
  •  
  •  
  • <!---
  • Catch any bad requests (these are malformed requests either
  • due to resource definition or invalid request data or rate
  • limit filtering).
  • --->
  • <cfcatch type="BadRequest">
  •  
  • <!--- Set the status code and response text. --->
  • <cfset api.statusCode = "400" />
  • <cfset api.statusText = "Bad Request" />
  •  
  • <!--- Overwrite the data key with the errors. --->
  • <cfset api.data = api.errors />
  •  
  • </cfcatch>
  •  
  •  
  • <!---
  • Catch any unacceptable requests (these are requests for
  • response formats that are not currently supported by the
  • API).
  • --->
  • <cfcatch type="NotAcceptable">
  •  
  • <!--- Set the status code and response text. --->
  • <cfset api.statusCode = "406" />
  • <cfset api.statusText = "Not Acceptable" />
  •  
  • <!--- Overwrite the data key with the errors. --->
  • <cfset api.data = api.errors />
  •  
  • </cfcatch>
  •  
  •  
  • <!---
  • Catch any not found resources. This could be due to the fact
  • that the resource is simply invalid or that the requested
  • resource has been removed / deleted.
  • --->
  • <cfcatch type="NotFound">
  •  
  • <!--- Set the status code and response text. --->
  • <cfset api.statusCode = "404" />
  • <cfset api.statusText = "Not Found" />
  •  
  • <!---
  • Create the data key. This time, however, since we have a
  • resource, let's include it in on the data.
  • --->
  • <cfset api.data = {
  • resource = api.resource,
  • errors = [ "The requested resource could not be found." ]
  • } />
  •  
  • </cfcatch>
  •  
  •  
  • <!---
  • Catch any method not allowed errors. This happens when a user
  • tried to access a resource using a VERB (ex. GET) that was
  • not supported on the given resource.
  • --->
  • <cfcatch type="MethodNotAllowed">
  •  
  • <!--- Set the status code and response text. --->
  • <cfset api.statusCode = "405" />
  • <cfset api.statusText = "Method Not Allowed" />
  •  
  • <!---
  • Create the data key. This time, however, since we have
  • a resource, let's include it in on the data.
  • --->
  • <cfset api.data = {
  • resource = api.resource,
  • errors = [ "The requested resource does not support this verb [#api.method#]." ]
  • } />
  •  
  • </cfcatch>
  •  
  •  
  • <!---
  • Note that we are not going to catch any unexpected errors.
  • Really, we shouldn't get any random errors. If we do,
  • we'll just let the server handle that so everything gets
  • logged properly.
  • --->
  •  
  • </cftry>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • Now that we have processed the response, let's figure out
  • how we need to present it to the user. All responses will
  • be in text format - we just need to figure out how that is
  • going to work.
  •  
  • NOTE: Since we validated teh response format earlier in the
  • workflow, we don't have to provide any default logic at this
  • point - we know we support the given format.
  • --->
  • <cfswitch expression="#api.format#">
  •  
  • <cfcase value="json">
  •  
  • <!---
  • For JSON, we are just going to use ColdFusion's native
  • JSON serializer.
  • --->
  • <cfset api.content = serializeJSON(
  • api.data
  • ) />
  •  
  • <!--- Set the content's mime-type. --->
  • <cfset api.mimeType = "application/json" />
  •  
  • </cfcase>
  •  
  • <cfcase value="xml">
  •  
  • <!---
  • For XML, we're just going to use ColdFusion's native WDDX
  • serializer. This isn't really the type of XML we want...
  • but this will keep the demo simple.
  • --->
  • <cfwddx
  • output="api.content"
  • action="cfml2wddx"
  • input="#api.data#"
  • />
  •  
  • <!--- Set the content's mime-type. --->
  • <cfset api.mimeType = "text/xml" />
  •  
  • </cfcase>
  •  
  • </cfswitch>
  •  
  •  
  • <!---
  • At this point, we have converted the response data into a
  • response content string, no matter what. Now, we just need
  • to convert that to a binary value so we can stream it to
  • the client.
  • --->
  • <cfset responseBinary = toBinary(
  • toBase64(
  • api.content
  • )
  • ) />
  •  
  •  
  • <!---
  • Loop over any additional headers that we need to return.
  • These might be rate-limit headers, authorization headers, etc..
  • --->
  • <cfloop
  • item="headerName"
  • collection="#api.headers#">
  •  
  • <!--- Pass back the given header. --->
  • <cfheader
  • name="#headerName#"
  • value="#api.headers[ headerName ]#"
  • />
  •  
  • </cfloop>
  •  
  • <!--- Report the status code and text. --->
  • <cfheader
  • statuscode="#api.statusCode#"
  • statustext="#api.statusText#"
  • />
  •  
  • <!---
  • Tell the client how much data to expect so that it knows when
  • to close the connection with the server.
  • --->
  • <cfheader
  • name="content-length"
  • value="#arrayLen( responseBinary )#"
  • />
  •  
  •  
  • <!--- Stream the content back to the client. --->
  • <cfcontent
  • type="#api.mimeType#"
  • variable="#responseBinary#"
  • />

As you can see, this API file is basically split up into three sections: incoming processing, error handling, and output processing. If you look at the incoming processing part, you'll see that it makes heavy use of the CFThrow tag to throw typed errors. These typed errors are then handled by typed-CFCatch tags.

In this demo, I am not handling any 500 Server Errors. I don't see those as being core to the API logic. I also know that those as something that I cannot always return in a consistent format (it depends on where the unexpected error is raised). As such, I am going to let the site-wide error handler deal with the 500 server errors and keep my API logic api-specific.

Once the work flow has assessed the incoming resource request as being superficially valid, it passes the processing off to the resource-specific modules. In our case, both resources are handled by the same module:

api.girls.cfm

  • <!---
  • Now that we are in the Girls resource, let's figure out what
  • specific resource the user is looking for. Here are the resources
  • that this module currently supports:
  •  
  • GET /girls/{name}/kiss
  • POST /girls/{name}/french-kiss
  • --->
  • <cfif reFindNoCase( "^/girls/[\w-]+/kiss$", api.resource )>
  •  
  • <!--- Include action. --->
  • <cfinclude template="api.girls.kiss.cfm" />
  •  
  • <cfelseif reFindNoCase( "^/girls/[\w-]+/french-kiss$", api.resource )>
  •  
  • <!--- Include action. --->
  • <cfinclude template="api.girls.frenchkiss.cfm" />
  •  
  • <cfelse>
  •  
  • <!---
  • The requested resource was not supported in this module.
  • Throw an exception. Don't worry about adding any additional
  • error information - this is such a generic error, it can be
  • handled in a central manner.
  • --->
  • <cfthrow type="NotFound" />
  •  
  • </cfif>

All this does is further parse the incoming resource request and then pass the processing off to the appropriate resource handlers. Both kiss and french-kiss have their own handler and their own rules. Let's take a look at the kiss one first:

api.girls.kiss.cfm

  • <!---
  • This action gets a kiss from the girl defined in the given
  • resource path.
  •  
  • GET: /girls/{name}/kiss
  • --->
  •  
  • <!---
  • Make sure that user has made the supported method request.
  • This resource only responds to GET requests.
  • --->
  • <cfif (api.method neq "get")>
  •  
  • <!--- Throw a method not allowed error. --->
  • <cfthrow type="MethodNotAllowed" />
  •  
  • </cfif>
  •  
  •  
  • <!--- Get the name of the targeted girl from the resource path. --->
  • <cfset girl = listGetAt( api.resource, 2, "/" ) />
  •  
  • <!---
  • Check to see if the girl's name is valid. Imagine that we would
  • be hitting a database at this point; but, for our purposes, we
  • are only going to support a few girl names.
  • --->
  • <cfif listFindNoCase( "Sarah,Tricia,Joanna", girl )>
  •  
  • <!---
  • For our simple demo, we are just going to return a simple
  • string as our response data.
  • --->
  • <cfset api.data = "Mmmm, nice kiss from #girl#!" />
  •  
  • <cfelse>
  •  
  • <!---
  • The requeste girl is not currently supported; throw a not
  • found error. Don't worry about adding any additional error
  • information - this is such a generic error, it can be handled
  • in a central manner.
  • --->
  • <cfthrow type="NotFound" />
  •  
  • </cfif>

The logic for this resource handler is very simple. You'll notice that it sets the data response but, it does not deal with any formatting; in our demo, the response formatting is generic and is a function of the greater API request, not the individual resource. In a real-world situation, however, you might need to move formatting concerns into the resource handler if you have specialized format requirements.

Now that we've looked at the GET resource example, let's take a look at french-kiss, our POST resource example:

api.girls.frenchkiss.cfm

  • <!---
  • This action gets a kiss from the girl defined in the given
  • resource path.
  •  
  • GET: /girls/{name}/french-kiss
  • --->
  •  
  • <!---
  • Make sure that user has made the supported method request.
  • This resource only responds to POST requests.
  • --->
  • <cfif (api.method neq "post")>
  •  
  • <!--- Throw a method not allowed error. --->
  • <cfthrow type="MethodNotAllowed" />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • Now that we have confirmed that this resource is a POST resource,
  • let's configure out FORM post variables.
  • --->
  • <cfparam name="form.approach" type="string" default="" />
  •  
  •  
  • <!--- Validate the form data. --->
  • <cfif (form.approach neq "gentle")>
  •  
  • <cfset arrayAppend(
  • api.errors,
  • "Come on man, be gentle with it."
  • ) />
  •  
  • </cfif>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • Check to see if there were any problems with the form data.
  • If so, then there is something wrong with the request.
  • --->
  • <cfif arrayLen( api.errors )>
  •  
  • <!---
  • The user posted a bad request. Raise an malformed request
  • exception. The errors define above will be used when handling
  • this request error.
  • --->
  • <cfthrow type="BadRequest" />
  •  
  • </cfif>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!---
  • Now that we know we are working with valid data, let's make sure
  • the user is not accidentally double-posting this information.
  • Let's compare this post to the previous post.
  • --->
  • <cflock
  • name="session-#api.username#"
  • type="exclusive"
  • timeout="5">
  •  
  • <!---
  • Check to see if this is the same resource AND the same
  • approach as the previous successful post.
  •  
  • NOTE: We don't have to check for FORM keys since the previous
  • resource will indicate which form variables will be there.
  • --->
  • <cfif (
  • structKeyExists( api.session, "prevResource" ) &&
  • (api.session.prevResource eq api.resource) &&
  • (api.session.prevForm.approach eq form.approach)
  • )>
  •  
  • <!---
  • Since this is a duplicate of the previous POST, let's
  • just return the previous data result.
  • --->
  • <cfset api.data = api.session.prevData />
  •  
  • <!---
  • Add a custom response header to indicate that the
  • request as simply a re-response of the previous post.
  • This is not required from a functional standpoint, but
  • it's nice to give the client as much information as
  • possible.
  •  
  • NOTE: Custom headers should begin with "X-" to indicate
  • that they are custom (not part of any HTTP specification).
  • --->
  • <cfset api.headers[ "X-Duplicate-Post" ] = api.resource />
  •  
  • <!---
  • Exit out of this processing branch (allowing the rest of
  • the API request to be executed with the previous data).
  • --->
  • <cfexit method="exitTemplate" />
  •  
  • </cfif>
  •  
  • </cflock>
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <!--- Get the name of the targeted girl from the resource path. --->
  • <cfset girl = listGetAt( api.resource, 2, "/" ) />
  •  
  • <!---
  • Check to see if the girl's name is valid. Imagine that we would
  • be hitting a database at this point; but, for our purposes, we
  • are only going to support a few girl names.
  • --->
  • <cfif listFindNoCase( "Sarah,Tricia,Joanna", girl )>
  •  
  • <!---
  • For our simple demo, we are just going to return a simple
  • string as our response data.
  • --->
  • <cfset api.data = "Mmmm, nice french-kiss from #girl#!" />
  •  
  • <cfelse>
  •  
  • <!---
  • The requeste girl is not currently supported; throw a not
  • found error. Don't worry about adding any additional error
  • information - this is such a generic error, it can be handled
  • in a central manner.
  • --->
  • <cfthrow type="NotFound" />
  •  
  • </cfif>

The POST example gets a bit more interesting as there is much more logic involved; not only do we have form validation and possible errors, we also have duplicate post protection taking place.

Now that we have our API set up, let's quickly look at some code that can test our API processing. I won't go into a lot of detail on this code (this post is long enough); but essentially, we're using ColdFusion's CFHTTP tag to hit our RESTful API with a number of GET and POST requests.

  • <!--- Create a base API url. --->
  • <cfset baseAPIUrl = (
  • "http://" &
  • cgi.server_name &
  • getDirectoryFromPath( cgi.script_name ) &
  • "api.cfm"
  • ) />
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <h2>
  • GET Request
  • </h2>
  •  
  • <!--- Get the GET request. --->
  • <cfhttp
  • result="result"
  • method="get"
  • url="#baseAPIUrl#/girls/Sarah/kiss.json"
  • username="ben"
  • password="bananas!"
  • />
  •  
  • <cfdump var="#result#">
  • <br />
  • <cfdump var="#toString( result.fileContent )#">
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  • <br />
  • <hr />
  • <br />
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <h2>
  • GET Request Not Found
  • </h2>
  •  
  • <!--- Get the GET request. --->
  • <cfhttp
  • result="result"
  • method="get"
  • url="#baseAPIUrl#/girls/Julia/kiss.json"
  • username="ben"
  • password="bananas!"
  • />
  •  
  • <cfdump var="#result#">
  • <br />
  • <cfdump var="#toString( result.fileContent )#">
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  • <br />
  • <hr />
  • <br />
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <h2>
  • POST Request
  • </h2>
  •  
  • <!--- Get the POST request. --->
  • <cfhttp
  • result="result"
  • method="post"
  • url="#baseAPIUrl#/girls/Tricia/french-kiss.json"
  • username="ben"
  • password="bananas!">
  •  
  • <cfhttpparam
  • type="formfield"
  • name="approach"
  • value="gentle"
  • />
  •  
  • </cfhttp>
  •  
  • <cfdump var="#result#">
  • <br />
  • <cfdump var="#toString( result.fileContent )#">
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  • <br />
  • <hr />
  • <br />
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <h2>
  • Second POST Request (For Duplicate Check)
  • </h2>
  •  
  • <!--- Get the POST request. --->
  • <cfhttp
  • result="result"
  • method="post"
  • url="#baseAPIUrl#/girls/Tricia/french-kiss.json"
  • username="ben"
  • password="bananas!">
  •  
  • <cfhttpparam
  • type="formfield"
  • name="approach"
  • value="gentle"
  • />
  •  
  • </cfhttp>
  •  
  • <cfdump var="#result#">
  • <br />
  • <cfdump var="#toString( result.fileContent )#">
  •  
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  • <br />
  • <hr />
  • <br />
  •  
  • <!--- ----------------------------------------------------- --->
  • <!--- ----------------------------------------------------- --->
  •  
  •  
  • <h2>
  • Invalid POST Request
  • </h2>
  •  
  • <!--- Get the POST request. --->
  • <cfhttp
  • result="result"
  • method="post"
  • url="#baseAPIUrl#/girls/Tricia/french-kiss.json"
  • username="ben"
  • password="bananas!">
  •  
  • <!--- Invalid form data. --->
  • <cfhttpparam
  • type="formfield"
  • name="approach"
  • value="rough"
  • />
  •  
  • </cfhttp>
  •  
  • <cfdump var="#result#">
  • <br />
  • <cfdump var="#toString( result.fileContent )#">

When we run the above code, we get the following screen output:

 
 
 
 
 
 
RESTful API Architecture Testing With Various GET And POST Requests Over CFHTTP. 
 
 
 

Again, I want to remind you that I am not an authority on creating RESTful API architectures; I have simply taken a look an industry giant like Twitter and tried to follow their functional specs using ColdFusion. I am sure I have a lot more to learn about this. But, as I said before, the beauty of an API - and a RESTful API at that - is that you can change the underlying logic as well as the underlying technology without creating concern for your API consumers.




Reader Comments

Ben -

this is perfect. Exactly what I was hoping for after SOTR. Having missed your first presentation due to being against you in the other room (so to speak) and having to duck out shortly after this presentation due to 'organiser things', so thank you for writing it all up. :)

Reply to this Comment

@Matt,

My pleasure my good friend. Glad I was able to turn this beast into something valuable.

@Ryan,

Thanks, I'll take a look. I believe I've seen people mention that before, so it must be pretty good.

@Edward,

Cool - sounds awesome in that case. I'll turn down the opportunity to look into "some of the most innovative CF development" you've seen!

Reply to this Comment

@Dominic,

Hey my man, just watched the Google Talk. Very good presentation. There stuff seemed to be focused more directly on Class API design (seeing as it is a series on language design); but, I think pretty much everything he talked about is just as applicable to web service API design.

Anyway, great link - thanks for sharing.

Reply to this Comment

@Edward,

This is the first I've heard of cfcommons, looking into it now. Very interested, I've been a big PowerNap fan for a little while now, curious to see what work has been done.

Reply to this Comment

Great overview Ben, and a simple implementation.

Last year I was working on an internal REST API for our White Label Dating platform; I extracted the resulting framework and released it as RESTfulCF: http://github.com/timblair/restfulcf

There's a detailed readme and sample application within the project, and quick introduction at http://groups.google.com/group/cfrest/msg/66dc8009d4b6ffb5 which covers the basic principles, and a quick explanation of how this is different to Powernap (at least the pre-cfcommons version.)

Reply to this Comment

Wow . . . this is quite a lot of info in a language that is easy to digest little by little . . . thanks, Ben!

Reply to this Comment

@Tim,

Very cool, I'll have to check that out. Thanks for extracting the framework.

@Lola,

Hope it's actually interesting once you dig in :)

@Elliott,

I am not familiar with Accelerate; I'll have to look into that one as well.

@John,

Ahhh, there is it! I could swear I read it somewhere, but I couldn't find it.

@All,

This is awesome - I have so many other RESTful concepts to look into now; thanks for providing good frameworks to look into.

Reply to this Comment

Ben, one thing I find helpful in creating RESTful APIs is asking the questions "When x (where x is some action) occurs, what is being created? What is being destroyed?" Helps to identify the true resource that should be modeled.

Reply to this Comment

@Hal,

Very good point. I need some quality field experience with this. I'm itching to get a good mobile experiment going!

Reply to this Comment

@Dominic,

Again, a very interesting link, thanks! Taffy looks like a cool framework. I particularly like how he maps the CFCs to the resource URLs with metadata on the component - quite clever. I'll have to dig into the implementation a bit more to fully wrap my head around how it's working.

Reply to this Comment

It is easy to see that you are impassioned about your writing. I wish I had got your ability to write. I look forward to more updates and will be returning.

Reply to this Comment

This is great, very useful Ben.

I spotted a small typo in the api.cgm listing:
<cfthrow type="Unauthroized" />

Cheers

Stefan

Reply to this Comment

Cool dive into REST. I like how you laid the groundwork for how REST worked before diving into it.

And since everyone else has given a shout out to their favorite REST framework in the comments, I wanted to make sure anyone reading know that ColdBox MVC has had really easy REST baked into it since about the time of this post in 2010 with fancy routing, rendering data and such. And using REST with a fully-featured framework like ColdBox makes stuff like security really easy via interceptors and AOP handler advices (pre, post, around actions)

http://blog.coldbox.org/blog/coldbox-rest-enabled-urls

Reply to this Comment

Post A Comment

?
You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
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.