The other day I blogged about using Bedrock to create a RESTful API. Today I'm going to talk about setting up Apache in order to support a RESTful interface based on URIs. Last time we pulled these requirements for a REST API framework from
Michael McClennen's talk:
- Parse a URI
- Determine the operation to be performed
- Determine the result format requested by the client
- Retrieve, validate, and deserialize client data
- Retrieve data from the back end
- Serialize data to send back to client
- Set HTTP response headers
- Handle errors
Let's configure Apache to help us with #1, and #2 as we implement a hypothetical RESTful interface for a contact database.
First, we'll create an Apache
RewriteRule that maps our desired API's base URI to a Bedrock page. We add the directives below to our Virtual host configuration file.
RewriteEngine On
RewriteRule ^contacts(/.*)?$ rest-api/contacts-api.jroc/$1
Using this rule URIs that begin with
contacts will now be rewritten to point to a URI that implements our REST interface in Bedrock. The Bedrock file
contacts-api.jroc is a Bedrock script (page) that returns a document of type
application/json. By convention (and as implemented by the Bedrock handler), any Bedrock script with an extension of
.jroc will be considered to return a JSON document.
That's just another way of saying that Bedrock will set the mime-type header to
application/json for you automatically. Of course you could have named the file
contacts-api.roc and then set the
Content-Type header yourself.
<null $header.set('Content-Type', 'application/json')>
Files in the
rest-api directory are configured using Apache directives so they will be processed by the either the Bedrock
mod_perl Apache handler or the Bedrock handler implemented as a CGI.
<Directory /var/www/vhosts/myhost/htdocs/rest-api>
# Bedrock - mod-perl for .roc (if mod_perl)
<IfModule mod_perl.c>
AddHandler perl-script .roc .jroc .html
PerlHandler Apache::Bedrock
</IfModule>
Action bedrock-cgi /cgi-bin/bedrock.cgi virtual
<IfModule !mod_perl.c>
AddHandler bedrock-cgi .roc .jroc
</IfModule>
...
</Directory>
We can now use the
PATH_INFO environment variable to interpret our URI since all the stuff after
contacts/ in our URI will be considered extra path information and presented to us in that environment variable...well sort of. As it turns out, when Bedrock is configured using the CGI version,
PATH_INFO will contain the filename (
contacts-api.jroc) as well as the extra path info. For this reason, Bedrock provides an environment variable named
BEDROCK_PATH_INFO only available when the Bedrock handler is configured as a CGI script that contains only the extra path information.
PATH_INFO => (/env.roc/foo)
BEDROCK_PATH_INFO => (/foo)
Our REST API script can now grab the extra path information necessary for us to interpret the URI.
<null:path_info --default=$env.PATH_INFO $env.BEDROCK_PATH_INFO>
Under
mod_perl we'll use the
PATH_INFO variable, but as a CGI handler we'll use the value Bedrock provides as
BEDROCK_PATH_INFO. Since the
BEDROCK_PATH_INFO variable is only set when the Bedrock handler is a CGI, our
$path_info will be set using the default value under
mod_perl.
The URI gives us the context, but what about the method? Using Bedrock's
$env object that exposes environment variables to your Bedrock pages, we can examine the HTTP method sent by the client. By testing
$env.REQUEST_METHOD we can determine the HTTP method. The value will probably be either
GET,
PUT,
POST, or
DELETE, as set by the client and defined by your API. It would then be easy enough to test that value
along with the URI to determine the operation to be performed. That is to say, the operation to be performed will be a function of the HTTP method
and the URI.
<if $env.REQUEST_METHOD --eq 'GET'>
# /contacts/friends
<if $path_info --eq '/friends'>
...
<elseif $path_info --eq '/business'>
...
<else>
<raise '400'>
</if>
<elseif $env.REQUEST_METHOD --eq 'PUT'>
...
</if>
This now satisfies our first two requirements we discussed last time, namely:
- Parse a URI
- Determine the operation to be performed
Next time: Determining the result format and de-serializing our input data.