Building RESTful Services with Zend Framework

As a followup to my previous post, I now turn to RESTful web services. I originally encountered the term when attending php|tropics in 2005, where George Schlossnaggle likened it to simple GET and POST requests. Since then, the architectural style — and developer understanding of the architectural style — has improved a bit, and a more solid definition can be made.

At its heart, REST simply dictates that a given resource have a unique address, and that you interact with that resource using HTTP verbs. The standard verbs utilized are:

  • GET: retrieve a list of resources, or, if an identifier is present, view a single resource
  • POST: create a new resource with the data provided in the POST
  • PUT: update an existing resource as specified by an identifier, using the PUT data
  • DELETE: delete an existing resource as specified by an identifier

The standard URL structure used is as follows:

  • /resource - GET (list) and POST operations
  • /resource/{identifier} - GET (view), PUT, and DELETE operations

What the REST paradigm provides you is a simple, standard way to structure your CRUD (Create-Read-Update-Delete) applications. Due to the large number of REST clients available, it also means that if you follow the rules, you get a ton of interoperability with those clients.

As of Zend Framework 1.9.0, it's trivially easy to create RESTful routes for your MVC application, as well as to handle the various REST actions via action controllers.

Zend_Rest_Route allows you to define RESTful controllers at several levels:

  • You can make it the default route, meaning that unless you have additional routes, all controllers will be considered REST controllers.
  • You can specify modules that contain RESTful controllers.
  • You can specify specific controllers per module that are RESTful

As examples:

$front = Zend_Controller_Front::getInstance();
$router = $front->getRouter();

// Specifying all controllers as RESTful:
$restRoute = new Zend_Rest_Route($front);
$router->addRoute('default', $restRoute);

// Specifying the "api" module only as RESTful:
$restRoute = new Zend_Rest_Route($front, array(), array(
    'api',
));
$router->addRoute('rest', $restRoute);

// Specifying the "api" module as RESTful, and the "task" controller of the
// "backlog" module as RESTful:
$restRoute = new Zend_Rest_Route($front, array(), array(
    'api',
    'backlog' => array('task'),
));
$router->addRoute('rest', $restRoute);

To define a RESTful action controller, you can either extend Zend_Rest_Controller, or simply define the following methods in a standard controller extending Zend_Controller_Action (you'll need to define them regardless):

// Or extend Zend_Rest_Controller
class RestController extends Zend_Controller_Action
{
    // Handle GET and return a list of resources
    public function indexAction() {}

    // Handle GET and return a specific resource item
    public function getAction() {}

    // Handle POST requests to create a new resource item
    public function postAction() {}

    // Handle PUT requests to update a specific resource item
    public function putAction() {}

    // Handle DELETE requests to delete a specific item
    public function deleteAction() {}
}

For those methods that operate on individual resources (getAction(), putAction(), and deleteAction()), you can test for the identifier using the following:

if (!$id = $this->_getParam('id', false)) {
    // report error, redirect, etc.
}

Responding is an art

Many developers are either unaware of or ignore the part of the specification that dictates what the response should look like.

For instance, in classic REST, after performing a POST to create a new item, you should do the following:

  • Set the HTTP response code to 201, indicating "Created"
  • Set the Location header to point to the canonical URI for the newly created item: /team/31
  • Provide a representation of the newly created item

Note that there's no redirect, which flies in the face of standard web development (where GET-POST-Redirect is the typical format). This is a common "gotcha" moment.

Similarly, with PUT requests, you simply indicate an HTTP 200 status when successful, and show a representation of the updated item. DELETE requests should return an HTTP 204 status (indicating success - no content), with no body content.

Note: when building RESTful HTML applications, you may want to still do GET-POST-Redirect to prevent caching issues. The above applies to RESTful web services, which typically use XML or JSON for transactions, and have smart clients for interacting with the service.

I'll be writing another article soon showing some tips and tricks for interacting with HTTP headers, both from the request and for the response, as it's a subject lengthy enough for a post of its own. In the meantime, start playing with Zend_Rest_Route and standardizing on it for your CRUD operations!