Day 9 - Web Services with Catalyst::Action::REST

Building RESTful Web Services with Catalyst::Action::REST.

What are we going to do today?

This article will serve as a brief introduction to the REST architectural style, specifically as it relates to web services. It will walk you through a constructing a sample application using Catalyst::Controller::REST, the source for which is available at the end of this article.

What is REST?

REST stands for Representational State Transfer. It describes an architectural style for building systems on the World Wide Web. REST sees every web application as a set of resources (such as http://dev.catalyst.perl.org) which represent a particular state of an application. When you access a resource, you are transferring it; and thus possibly changing its state.

(For a longer, much more accurate description of REST, see the links at the end of this article. The preceding paragraph owes quite a bit to the article at http://www.xfront.com/REST-Web-Services.html)

In practice, REST is a method for designing web services using three things:

Nouns, or Resources

A noun is a thing (or set of things) that describe the actors in your system. They are represented as URIs, and should be descriptive. A good example of a noun (which we will be using in our example later on) would be http://example.com/user, or http://example.com/user/adam.

Verbs, or Actions

Verbs in REST are the various actions you can apply to a Noun. These are typically the same as HTTP Methods. The four most common verbs in REST are:

PUT - which creates (or updates) the data at a resource.
GET - which retrieves the data from a resource.
POST - which updates (or creates) the data at a resource.
DELETE - which deletes the data at a resource.

Notice that they correspond quite neatly to the CRUD (Create, Retrieve, Update, Delete) acronym common in discussions of database actions.

Content Types

The final piece of the REST puzzle is Content Types. These are used to describe the format of a resource: how the various parts of our machine should process the resource. In practice, we specify the Content Type to ensure that the representation we receive of a given resource is the one we are best suited to process (text/xml for XML, text/x-yaml for YAML, text/x-json for JSON, etc.)

These three things (Nouns, Verbs, and Content Types) form what's called the REST Triangle. Once you understand the interactions among these three things, you've got 95% of everything you need to know about REST.

What's the remaining 5%? It's understanding HTTP 1.1; knowing when to return different status codes, what headers to be set, etc.

Catalyst::Action::REST

To make building RESTful web services easy, we've created Catalyst::Action::REST. It's composed of several pieces, each of which helps you to implement a portion of the REST triangle (and to help wrangle HTTP, while you're at it.)

How it helps with Nouns

Truthfully, Catalyst already helps you here. Its incredibly flexible dispatch system is perfect for creating RESTful URIs.

How it helps with Verbs

Catalyst::Action::REST helps you deal with the different verbs by extending the dispatcher. When you declare a sub like this:

   sub cat :Path('/cat') :Args(1) :ActionClass('REST') {}

we will automatically re-dispatch to subroutines that have the current method appended with an _. So, for the above example, we might have:

   sub cat_GET {
       ... returns a particular cat ...
   }

   sub cat_PUT {
       ... creates a new cat...
   }

When a reference is requested using a method you have not implemented, Catalyst::Action::REST will automatically generate a proper 405 Not Implemented response. The Allow header will contain a list of all the implemented methods. (In this case, it would contain GET and PUT.) Similarly, support for OPTIONS requests are dynamically generated as well.

How it helps with Content Types

Support for different Content Types is provided by Catalyst::Action::Serialize and Catalyst::Action::Deserialize. Together, these two Actions allow you to send serialized data back to clients (Catalyst::Action::Serialize) and to deserialize data received from them (Catalyst::Action::Deserialize). Eleven different content types are currently supported (including YAML, JSON, XML, and Data::Dumper) and adding new ones is easy.

We evaluate which content type to use by evaluating the following things on a per-request basis:

The value of the Content-Type HTTP header.

If the client provides a Content-Type header, we will honor it first.

The value of the content-type parameter.

For GET requests, you can manually set the content type with a query parameter. Calling a resource like http://example.com/user/adam?content-type=text/x-json would return a JSON structure.

Parsing the Accept HTTP header

If none of these have been provided, we will parse the Accept header, selecting the content type most preferred by the client.

How it helps with HTTP

Creating the proper HTTP responses to clients makes everyone's lives easier. Catalyst::Controller::REST provides status helpers, which can be used to easily create well-formed HTTP responses. For example, a 200 OK should include an HTTP Body that matches the requested content type. Using a status helper, generating the proper response is:

     $self->status_ok(
       $c,
       entity => {
           radiohead => "Is a good band!",
       }
     );

REST by Example

Often the best way to learn about something is by doing, so let's build a simple REST web service with Catalyst. You can follow along here, or download the source with subversion from http://dev.catalyst.perl.org/repos/Catalyst/examples/AdventREST.

Our example application will be a simple User database that tracks User ID's, Full Names, and Descriptions of users.

So let's start by determining what our Nouns will be, and what Verbs are valid for them:

Noun 1 - http://example.com/user

This noun represents all the users in our system.

Verb - GET (retrieve)

We want to be able to GET a list of users from the system.

Noun 2 - http://example.com/user/user_id

These nouns will represent individual users in our system. Each user gets a URL based on his or her User ID.

Verb - PUT (create)

We need to be able to create new users by sending a PUT request to an individual user's URL.

Verb - GET (retrieve)

Sending a GET request should return the state of the current user.

Verb - POST (update)

POST requests will be needed to update the user.

Verb - DELETE (delete)

And sadly, sometimes we need to delete a user entirely.

So, now that we have our architecture laid out, let's get down to business. To follow along with this tutorial, you will need to have installed:

Catalyst
Catalyst::Plugin::ConfigLoader
Catalyst::Action::REST
Catalyst::Model::DBIC::Schema
DBD::SQLite

Create the Application

First off, we need to create our new Catalyst application.

  $ catalyst.pl AdventREST

The remaining commands in this tutorial are from the base of this new application.

Create the Database, and the DBIx::Class Schema

We need to have somewhere to store all our lovely users, so let's start with that. A couple of directories need to get made:

   $ mkdir db lib/AdventREST/Schema

We have a simple SQL schema, which we will put in a file called db.sql.

   CREATE TABLE user (
    user_id TYPE text NOT NULL PRIMARY KEY,
    fullname TYPE text NOT NULL,
    description TYPE text NOT NULL
   ); 

Setting up our table structure is:

   $ sqlite3 db/adventrest.db < db.sql

Now we populate our DBIx::Class schema. In lib/AdventREST/Schema.pm you should have:

    #
    # AdventREST::Schema.pm
    #

    package AdventREST::Schema;
    use base qw/DBIx::Class::Schema/;

    __PACKAGE__->load_classes(qw/User/);

    1;

The "user" table from above gets its own class as well, in lib/AdventREST/Schema/User.pm.

    package AdventREST::Schema::User;

    use base qw/DBIx::Class/;
    __PACKAGE__->load_components(qw/Core/);
    __PACKAGE__->table('user');
    __PACKAGE__->add_columns(qw/user_id fullname description/);
    __PACKAGE__->set_primary_key('user_id');

    1;

To understand what this code does, refer to the DBIx::Class, DBIx::Class::Schema, and DBIx::Class::ResultSource documentation.

We still need to have a Catalyst model for our database. Catalyst::Model::DBIC::Schema comes with one we can use, so let's do that:

   $ ./script/adventrest_create.pl model DB DBIC::Schema AdventREST::Schema

Connecting our new Database model to the SQLite database we created earlier is easy. Just edit the adventrest.yml file:

    ---
    name: AdventREST
    Model::DB:
        schema_class: AdventREST::Schema
        connect_info:
            - DBI:SQLite:dbname=__path_to(db/adventrest.db)__
            - ""
            - ""

That makes our Database ready for use with Catalyst. Now, let's create our Controller.

Creating the User Controller.

Using the adventrest_create.pl command, let's create a User controller:

   $ ./script/adventrest_create.pl controller User

To make things easier, I'm going to annotate the contents of your new controller, which was created as lib/AdventREST/Controller/User.pm.

   package AdventREST::Controller::User;

   use strict;
   use warnings;
   use base 'Catalyst::Controller::REST';

Since this is a REST controller, we want to inherit from Catalyst::Controller::REST instead of the regular Catalyst::Controller. This sets things up to automatically handle Content-Type negotiation and provides the status helpers I mentioned earlier.

   sub user_list : Path('/user') :Args(0) : ActionClass('REST') { }

The sub user_list handles the http://example.com/user noun. Remember that the various actions (verbs) are dealt with in different subroutines. The actual list of users should only be generated on GET requests, so we create:

   sub user_list_GET {
       my ( $self, $c ) = @_;

       my %user_list;
       my $user_rs = $c->model('DB::User')->search;
       while ( my $user_row = $user_rs->next ) {
           $user_list{ $user_row->user_id } =
             $c->uri_for( '/user/' . $user_row->user_id )->as_string;
       }
       $self->status_ok( $c, entity => \%user_list );
   }

It starts by doing a search of our database, populating a hash (%user_list) with the user_id and its resource URI. We then pass that hash to the status_ok status helper, which returns the %user_list serialized into a supported content-type.

Since GET is the only verb we care about for http://example.com/user, let's move on to creating the http://example.com/user/adam style resources..

    sub single_user : Path('/user') : Args(1) : ActionClass('REST') {
        my ( $self, $c, $user_id ) = @_;

        $c->stash->{'user'} = $c->model('DB::User')->find($user_id);
    }

Note that this routine is different from user_list, in that it's not empty. It will be executed before any of the _METHOD subroutines are invoked, letting you pre-populate any data that is necessary in each method. In this case, we look up the user specified in our URL in the database, and stick it in the stash for later.

Since we need to create users before we can retrieve them, we'll do the single_user_POST method next.

    sub single_user_POST {
        my ( $self, $c, $user_id ) = @_;

        my $new_user_data = $c->req->data;

Methods like POST, PUT, and OPTIONS requests will all have the data they sent deserialized and put into $c->req->data.

        if ( !defined($new_user_data) ) {
            return $self->status_bad_request( $c,
                message => "You must provide a user to create or modify!" );
        }

If the client didn't supply any data, they didn't send a properly formed request.

        if ( $new_user_data->{'user_id'} ne $user_id ) {
            return $self->status_bad_request( 
                    $c,
                    message => 
                        "Cannot create or modify user "
                      . $new_user_data->{'user_id'} . " at "
                      . $c->req->uri->as_string
                      . "; the user_id does not match!" );
        }

If they did supply some data, but it isn't appropriate for the resource they specified, it's a bad request.

        foreach my $required (qw(user_id fullname description)) {
            return $self->status_bad_request( $c,
                message => "Missing required field: " . $required )
              if !exists( $new_user_data->{$required} );
        }

This makes sure that all the fields we need to update the database exist.

        my $user = $c->model('DB::User')->update_or_create(
            user_id     => $new_user_data->{'user_id'},
            fullname    => $new_user_data->{'fullname'},
            description => $new_user_data->{'description'},
        );
        my $return_entity = {
            user_id     => $user->user_id,
            fullname    => $user->fullname,
            description => $user->description,
        };

This creates a new user in the database, and prepares the data we are going to return to our client.

        if ( $c->stash->{'user'} ) {
            $self->status_ok( $c, entity => $return_entity, );
        } else {
            $self->status_created(
                $c,
                location => $c->req->uri->as_string,
                entity   => $return_entity,
            );
        }
    }

Since POST can handle both updating an existing resource and creating a new one, we need to know whether to return a 200 OK response, or a <201 CREATED>. We do that by checking to see if the user object was populated in the stash. The location argument to status_created should be the URI where the resource can be found.

For our application, PUT and POST are the same. We'll just take a little shortcut to making that so in our controller:

    *single_user_PUT = *single_user_POST;

Ok, so now we can list, create, and update users. We still need to be able to retrieve an individual user.

    sub single_user_GET {
        my ( $self, $c, $user_id ) = @_;

        my $user = $c->stash->{'user'};
        if ( defined($user) ) {
            $self->status_ok(
                $c,
                entity => {
                    user_id     => $user->user_id,
                    fullname    => $user->fullname,
                    description => $user->description,
                }
            );
        }
        else {
            $self->status_not_found( $c,
                message => "Could not find User $user_id!" );
        }
    }

This should look pretty familiar to you by now. If we found the user when the request was made, then we'll return a 200 OK along with their data. Otherwise, it's the old 404 NOT FOUND response for you.

Last thing left to be done: deleting a user.

    sub single_user_DELETE {
        my ( $self, $c, $user_id ) = @_;

        my $user = $c->stash->{'user'};
        if ( defined($user) ) {
            $user->delete;
            $self->status_ok(
                $c,
                entity => {
                    user_id     => $user->user_id,
                    fullname    => $user->fullname,
                    description => $user->description,
                }
            );
        } else {
            $self->status_not_found( $c,
                message => "Cannot delete non-existent user $user_id!" );
        }
    }

It's only a one-line difference from our _GET routine; we just delete the user if they exist.

Fire it up!

To test your new REST application, we'll just use the good old curl command. You can get a copy of curl at http://curl.haxx.se/.

First off, we start up our Catalyst test server:

   $ ./script/adventrest_server

Next, we need to put some data together to populate our User service. (If you are following along from the reference implementation, these files exist in the "data" directory.) Let's start with some yaml. Create a file called new-user.yml:

    ---
    user_id: adam
    fullname: Adam Jacob
    description: Another Catalyst Monkey

This will create a user just like me! :)

Ok, let's use curl to update our web service:

   $ curl -X PUT -H 'Content-Type: text/x-yaml' -T new-user.yml \
       http://localhost:3000/user/adam

You will get back:

    --- 
    description: Another Catalyst Monkey
    fullname: Adam Jacob
    user_id: adam

Note that the order of the fields will be (sort of) random, since we're serializing a hash. Lets get a list of all the users we have created so far:

   $ curl -X GET -H 'Content-Type: text/x-yaml' http://localhost:3000/user
   --- 
   adam: http://localhost:3000/user/adam

I love monkeys as much as the next guy, but even I balk at having it in my description! So let's change it to something else. This time, create a file called update-user.datadumper, and stick some Perl in there:

    {
        user_id => "adam",
        fullname => "Adam Jacob",
        description => "Catalyst::Action::REST rules!",
    }

Updating the user entry is:

   $ curl -X POST -H 'Content-Type: text/x-data-dumper' \
      -T update-user.datadumper \
      http://localhost:3000/user/adam

The server will respond:

    {'fullname' => 'Adam Jacob','user_id' => 'adam','description' => 'Catalyst::Action::REST rules!'}

As great as I am (and I'm pretty darn sweet) you probably don't want me in your user database. Delete!

   $ curl -X DELETE -H 'Content-Type: text/x-yaml' \
      http://localhost:3000/user/adam
    --- 
    description: Catalyst::Action::REST rules!
    fullname: Adam Jacob
    user_id: adam

Viola! Empty database.

Summary

REST is an incredibly useful way of looking at the web applications you build. Creating REST services with Catalyst tries to make implementing them as easy as possible. If you want to learn more about REST, here are some resources for you:

http://www.xfront.com/REST-Web-Services.html

A pretty concise, well-structured introduction to REST.

http://en.wikipedia.org/wiki/Representational_State_Transfer

The almighty Wikipedia has a slightly rambling, but ultimately useful entry on REST.

http://rest.blueoxen.net/cgi-bin/wiki.pl

The REST wiki provides all sorts of information about REST.

http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm

This is Roy Fielding's doctoral thesis that first coined the term REST, and lays out its fundamental principles.

Thanks for reading! Happy RESTing!

AUTHOR

Adam Jacob <adam@stalecoffee.org>