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>