Writing REST web services with Catalyst::Controller::REST

This article is a minor update to the 2006 entry about Catalyst::Controller::REST, which can be found at http://www.catalystframework.org/calendar/2006/9.

What is REST

REST means REpresentational State Transfer. The REST approach is using the HTTP verbs (GET, PUT, POST, DELETE) to interact with a web service, using the content type of the request to determine the format of the response and then mapping the URI to a resource. Look at this simple query:

    curl -X GET -H "Content-type: application/json" http://baseuri/book/1

The query asks (GET) for the book (the resource) with the id of 1, and wants the response (Content-type) in JSON.

Using Catalyst::Controller::REST

The Catalyst::Controller::REST module helps us easily create REST web services in Catalyst.

HTTP Verbs

First you declare a new resource:

    sub book : Local : ActionClass('REST') { }

Then subroutines for the methods you want to handle that resource:

    sub book_GET { }

    sub book_POST { }

Catalyst dispatches to the subroutine with the appropriate name. The book() subroutine will be executed each time you request the book resource, whatever the HTTP method is. For example, when you call GET on /book/, first Catalyst goes to the book subroutine, then the book_GET subroutine.

If an ID is required to access the book resource, check for it in book():

    sub book : Local : ActionClass('REST') {
        my ($self, $c, $id) = @_;
        if (!$id) {
            $self->status_bad_request($c, message => 'id is missing');
            $c->detach();
        }
    }

Now if $id is missing book_GET() and book_POST() will not be called.

Serialization

A nice feature of Catalyst::Controller::REST is automatic serialization and deserialization. You don't need to serialize the response, or know how the data sent to you were serialized. When a client makes a request, Catalyst::Controller::REST attempts to find the appropriate content-type for the query. It looks for:

Content-type from the HTTP request header

It tries to find the Content-type of the request in the in the HTTP header. This value can be set:

    my $req = HTTP::Request(GET => 'http://...');
    $req->header('Content-Type' => 'application/json');

Content-type from the HTTP request parameter

For GET requests, Catalyst::Controller::REST also checks the query's Content-type parameter:

    http://www.example.com/book/id?content-type=application/json

This is nice, because you can do a request for a specific content-type from your browser, without changing the content-type value of the header.

Accept-Content from the HTTP::Request

Finally if nothing is found, Catalyst::Controller::REST extracts content-type from Accept-Content in the HTTP request.

HTTP Helpers

Catalyst::Controller::REST comes with helpers to generate the appropriate HTTP response to a query. When you receive a POST query and you create a new entry, you can use the status_created helper, to generate an HTTP response with code 201. If a request returned no record, you can use status_bad_request to return a 404.

Configuration

Catalyst::Controller::REST is usable without any configuration. You can also customize many of its parts. When you

    $self->status_ok($c, entity => {foo => 'bar'});

the content of entity will be set in the 'rest' key of the stash. You can change the name of this key:

    __PACKAGE__->config('stash_key' => 'my_rest_key');

Various serializations are supported: JSON, YAML, storable, XML, etc. You might want to limit your application to a subset of these formats.

    __PACKAGE__->config(map => {
        'text/x-yaml' => 'YAML',
        'application/json' => 'JSON',
    });

It is possible to force a default serializer. If no serializer is found for a requested content-type, this one is used:

    __PACKAGE__->config('default'   => 'application/json');

Writing a simple controller

Imagine you have a nice website with a database, and you want to provide users an easy way to access data.

    package BookStore::Controller::API::REST;
    use Moose;
    BEGIN { extends 'Catalyst::Controller::REST'};

    sub book : Local : ActionClass('REST') { }

    sub book_get {
        my ( $self, $c, $id ) = @_;
        if ( !$id ) {
            $self->status_bad_request( $c, message => "id is missing" );
            $c->detach();
        }

        # do something clever
        my $book = ... 
        $self->status_ok( 
            $c,
            entity => { author => $book->author, title => $book->title } 
        );
    }

    sub book_POST {
        my ( $self, $c ) = @_;
        my $book_content = $c->req->data;
        # insert book
        $self->status_created(
            $c,
            location => $c->req->uri->as_string,
            entity   => { title => $book->title, author => $book->author }
        );
    }

    sub book_PUT {
        my ( $self, $c ) = @_;
        my $new_quantity = $c->req->data->{quantity};
        # update quantity
    }

    sub book_DELETE {
        my ( $self, $c, $id ) = @_;
        $self->status_accepted( $c, entity => { status => "deleted" } );
    }

    1;

SEE ALSO

Author

Franck Cuny <franck@lumberjaph.net>