Tuesday, December 30, 2014

RESTful APIs with Bedrock (part IV)

To recap our 8 requirements of a web development framework for creating our RESTful API:
  1. Parse a URI
  2. Determine the operation to be performed
  3. Determine the result format requested by the client
  4. Retrieve, validate, and de-serialize client data
  5. Retrieve data from the back end
  6. Serialize data to send back to client
  7. Set HTTP response headers
  8. Handle errors

In yesterday's blog we talked about using Apache directives to help us create meaningful URIs and using Bedrock to interpret the extra path information and HTTP method in order to satisfy requirements 1 & 2.

Today we'll examine #3 and #4 and touch on #6.

Determine the result format requested by the client

During Michael McClennen's REST talk he discussed ways for the client to specify the format desired for the result from the server.  Some APIs choose to implement this via a value passed in the query string, however others have pointed out that a more RESTful approach might be to look at the Accept headers.  In practice, it seems some developers prefer to use the former method due to the difficulties in setting Accept headers when testing their APIs using a browser.  True one can test a REST API in a variety of ways (curl for example), not just using a browser, so setting Accept headers should not be too much of a lift.  Apparently it is though.

If one wants to lift, then one can, by going ahead and examining the Accept headers.  Apache sets the HTTP_ACCEPT environment variable for you or if you're using mod_perl for example, you might inspect the headers using the Apache2::RequestRec object's headers_in() method.

Nothing ever seems to be that simple and it isn't really.  If you want to implement a robust REST API you might want to read this article and learn more about how a client and server might negotiate return content format in a slightly more structured manner.

For our Bedrock REST API we'll take the easy way out for now and insist on sending back JSON documents to the client.  Of course, it's not that hard to test our Accept headers in Bedrock since the HTTP_ACCEPT environment variable is available to us in the $env object.  Here's a sloppy (but effective) attempt at negotiating content with the client:

<if $env.HTTP_ACCEPT --re 'json'>
... return JSON
  <null $header.set('Content-Type', 'application/json')>
  <var --json $result>
<elseif $env.HTTP_ACCEPT --re 'xml'>
...return XML
  <null $header.set('Content-Type', 'application/xml')>
  <var --xml $result>
</if>

If you decide it makes more sense to simply look at query strings, you can always design your API to include some kind of format identifier and then inspect that in your Bedrock API.

<if $input.format --eq 'json'>
... return JSON
  <null $header.set('Content-Type', 'application/json')>
  <var --json $result>
<elseif $input.format --eq 'xml'>
...return XML
  <null $header.set('Content-Type', 'application/xml')>
  <var --xml $result>
</if>

In any event, you can use Bedrock's built-in serializers for JSON or XML applied to the <var> tag which outputs your result set.  What about de-serializing the input?

Retrieve, validate, and de-serialize client data

POST or PUT data sent by the client as input to your API is available to you in serialized form as a scalar of the $input object.  For POST methods, inspect the $input.POSTDATA scalar or for PUT methods inspect the $input.PUTDATA scalar.  Query string data is available as members of the $input object.

<if $env.REQUEST_METHOD --eq 'PUT'>
  <null:in $input.PUTDATA>
<elseif $env.REQUEST_METOD --eq 'POST'>
  <null:in $input.POSTDATA>
</if>

You can de-serialize the input using appropriate de-serializer option.

<null:in --json $input.POSTDATA>
<null:in --xml $input.POSTDATA>

Data validation can be done using any of several techniques that employ assertions or lookups.

<hash:valid_values red r green g blue b>

<unless $valid_values.get($input.color)>
  <raise '400|invalid color value'>
</unless>

Validating the request can involve returning an error code and possibly further information in the body of the response to help the client understand what went wrong.  More on that in a future blog...for now we've satisfied requirements #3, #4 and #6.

1 comment:

Note: Only a member of this blog may post a comment.