RESTful APIs with ZF2, Part 3
In my previous posts, I covered basics of JSON hypermedia APIs using Hypermedia Application Language (HAL), and methods for reporting errors, including API-Problem and vnd.error.
In this post, I'll be covering documenting your API -- techniques you can use to indicate what HTTP operations are allowed, as well as convey the full documentation on what endpoints are available, what they accept, and what you can expect them to return.
While I will continue covering general aspects of RESTful APIs in this post, I will also finally introduce several ZF2-specific techniques.
Why Document?
If you're asking this question, you've either never consumed software, or your software is perfect and self-documenting. I frankly don't believe either one.
In the case of APIs, those consuming the API need to know how to use it.
- What endpoints are available? Which operations are available for each endpoint?
- What does each endpoint expect as a payload during the request?
- What can you expect as a payload in return?
- How will errors be communicated?
While the promise of hypermedia APIs is that each response tells you the next steps available, you still, somewhere along the way, need more information - what payloads look like, which HTTP verbs should be used, and more. If you're not documenting your API, you're "doing it wrong."
Where Should Documentation Live?
This is the much bigger question.
Of the questions I raised above, detailing what should be documented, there
are two specific types. When discussing what operations are available,
we have a technical solution in the form of the OPTIONS
method and its counterpart, the Allow header. Everything
else falls under end-user documentation.
OPTIONS
The HTTP specification details the OPTIONS method as
idempotent, non-cacheable, and for use in detailing what operations
are available for the given resource specified by the request URI. It
makes specific mention of the Allow header, but does not
limit what is returned for requests made via this method.
The Allow header details the allowed HTTP methods for the
given resource.
Used in combination, you make an OPTIONS request to a URI,
and it should return a response containing an Allow header;
from that header value, you then know what other HTTP methods can be made
to that URI.
What this tells us is that our RESTful endpoint should do the following:
-
When an
OPTIONSrequest is made, return a response with anAllowheader that has a list of the available HTTP methods allowed. - For any HTTP method we do not allow, we should return a "405 Not Allowed" response.
These are fairly easy to accomplish in ZF2. (See? I promised I'd get to some ZF2 code in this post!)
When creating RESTful endpoints in ZF2, I recommend using
Zend\Mvc\Controller\AbstractRestfulController. This controller
contains an options() method which you can use to respond to
an OPTIONS request. As with any ZF2 controller, returning
a response object will prevent rendering and bubble out immediately so
that the response is returned.
namespace My\Controller;
use Zend\Mvc\Controller\AbstractRestfulController;
class FooController extends AbstractRestfulController
{
public function options()
{
$response = $this->getResponse();
$headers = $response->getHeaders();
// If you want to vary based on whether this is a collection or an
// individual item in that collection, check if an identifier from
// the route is present
if ($this->params()->fromRoute('id', false)) {
// Allow viewing, partial updating, replacement, and deletion
// on individual items
$headers->addHeaderLine('Allow', implode(',', array(
'GET',
'PATCH',
'PUT',
'DELETE',
)));
return $response;
}
// Allow only retrieval and creation on collections
$headers->addHeaderLine('Allow', implode(',', array(
'GET',
'POST',
)));
return $response;
}
}
The next trick is returning the 405 response if an invalid option is used. For this, you can create a listener in your controller, and wire it to listen at higher-than-default priority. As an example:
namespace My\Controller;
use Zend\EventManager\EventManagerInterface;
use Zend\Mvc\Controller\AbstractRestfulController;
class FooController extends AbstractRestfulController
{
protected $allowedCollectionMethods = array(
'GET',
'POST',
);
protected $allowedResourceMethods = array(
'GET',
'PATCH',
'PUT',
'DELETE',
);
public function setEventManager(EventManagerInterface $events)
{
parent::setEventManager($events);
$events->attach('dispatch', array($this, 'checkOptions'), 10);
}
public function checkOptions($e)
{
$matches = $e->getRouteMatch();
$response = $e->getResponse();
$request = $e->getRequest();
$method = $request->getMethod();
// test if we matched an individual resource, and then test
// if we allow the particular request method
if ($matches->getParam('id', false)) {
if (!in_array($method, $this->allowedResourceMethods)) {
$response->setStatusCode(405);
return $response;
}
return;
}
// We matched a collection; test if we allow the particular request
// method
if (!in_array($method, $this->allowedCollectionMethods)) {
$response->setStatusCode(405);
return $response;
}
}
}
Note that I moved the allowed methods into properties; if I did the above,
I'd refactor the options() method to use those properties as
well to ensure they are kept in sync.
Also note that in the case of an invalid method, I return a response object. This ensures that nothing else needs to execute in the controller; I discover the problem and return early.
End-User Documentation
Now that we have the technical solution out of the way, we're still left with the bulk of the work left to accomplish: providing end-user documentation detailing the various payloads, errors, etc.
I've seen two compelling approaches to this problem. The first builds on
the OPTIONS method, and the other uses a hypermedia link in
every response to point to documentation.
The OPTIONS solution is this: use the
body of an OPTIONS response to provide documentation.
(Keith Casey gave an excellent short
presentation about this at REST Fest 2012).
The OPTIONS method allows for you to return a body in the
response, and also allows for content negotiation. The theory, then, is
that you return media-type-specific documentation that details the
methods allowed, and what they specifically accept in the body. While
there is no standard for this at this time, the first article I linked
suggested including a description, the parameters expected, and one or more
example request bodies for each HTTP method allowed; you'd likely also
want to detail the responses that can be expected.
{
"POST": {
"description": "Create a new status",
"parameters": {
"type": {
"type": "string",
"description": "Status type -- text, image, or url; defaults to text",
"required": false
},
"text": {
"type": "string",
"description": "Status text; required for text types, optional for others",
"required": false
},
"image_url": {
"type": "string",
"description": "URL of image for image types; required for image types",
"required": false
},
"link_url": {
"type": "string",
"description": "URL of image for link types; required for link types",
"required": false
}
},
"responses": [
{
"describedBy": "http://example.com/problems/invalid-status",
"title": "Submitted status was invalid",
"detail": "Missing text field required for text type"
},
{
"id": "abcdef123456",
"type": "text",
"text": "This is a status update",
"timestamp": "2013-02-22T10:06:05+0:00"
}
],
"examples": [
{
"text": "This is a status update"
},
{
"type": "image",
"text": "This is the image caption",
"image_url": "http://example.com/favicon.ico"
},
{
"type": "link",
"text": "This is a description of the link",
"link_url": "http://example.com/"
},
]
}
}
If you were to use this methodology, you would alter the
options() method such that it does not return a response
object, but instead return a view model with the documentation.
namespace My\Controller;
use Zend\Mvc\Controller\AbstractRestfulController;
class FooController extends AbstractRestfulController
{
protected $viewModelMap = array(/* ... */);
public function options()
{
$response = $this->getResponse();
$headers = $response->getHeaders();
// Get a view model based on Accept types
$model = $this->acceptableViewModelSelector($this->viewModelMap);
// If you want to vary based on whether this is a collection or an
// individual item in that collection, check if an identifier from
// the route is present
if ($this->params()->fromRoute('id', false)) {
// Still set the Allow header
$headers->addHeaderLine('Allow', implode(
',',
$this->allowedResourceMethods
));
// Set documentation specification as variables
$model->setVariables($this->getResourceDocumentationSpec());
return $model;
}
// Allow only retrieval and creation on collections
$headers->addHeaderLine('Allow', implode(
',',
$this->allowedCollectionMethods
));
$model->setVariables($this->getCollectionDocumentationSpec());
return $model;
}
}
I purposely didn't provide the implementations of the
getResourceDocumentationSpec() and
getCollectionDocumentationSpec() methods, as that will likely
be highly specific to your application. Another possibility is to use
your view engine for this, and specify a template file that has the
fully-populated information. This would require a custom renderer when
using JSON or XML, but is a pretty easy solution.
However, there's one cautionary tale to tell, something I
already mentioned: OPTIONS, per the specification, is
non-cacheable. What this means is that everytime somebody makes an
OPTIONS request, any cache control headers you provide will be
ignored, which means hitting the server for each and every request to the
documentation. Considering documentation is static, this is problematic;
it has even prompted blog
posts urging you not to use OPTIONS for documentation.
Which brings us to the second solution for end-user documentation: a static page referenced via a hypermedia link.
This solution is insanely easy: you simply provide a Link
header in your response, and provide a describedby reference
pointing to the documentation page:
Link: <http://example.com/api/documentation.md>; rel="describedby"
With ZF2, this is trivially easy to accomplish: create a route and endpoint
for your documentation, and then a listener on your controller that adds
the Link header to your response.
The latter, adding the link header, might look like this:
namespace My\Controller;
use Zend\EventManager\EventManagerInterface;
use Zend\Mvc\Controller\AbstractRestfulController;
class FooController extends AbstractRestfulController
{
public function setEventManager(EventManagerInterface $events)
{
parent::setEventManager($events);
$events->attach('dispatch', array($this, 'injectLinkHeader'), 20);
}
public function injectLinkHeader($e)
{
$response = $e->getResponse();
$headers = $response->getHeaders();
$headers->addHeaderLine('Link', sprintf(
'<%s>; rel="describedby"',
$this->url('documentation-route-name')
));
}
}
If you want to ensure you get a fully qualified URL that includes the schema, hostname, and port, there are a number of ways to do that as well; the above gives you the basic idea.
Now, for the route and endpoint, there are tools that will help you simplify that task as well, in the form of a couple of ZF2 modules: PhlySimplePage and Soflomo\Prototype. (Disclosure: I'm the author of PhlySimplePage.)
Both essentially allow you to specify a route and the corresponding
template name to use, which means all you need to do is provide a little
configuration, and a view template. Soflomo\Prototype has
slightly simpler configuration, so I'll demonstrate it here:
return array(
'soflomo_prototype' => array(
'documentation-route-name' => array(
'route' => '/api/documentation',
'template' => 'api/documentation',
),
),
'view_manager' => array(
'template_map' => array(
'api/documentation' => __DIR__ . '/../view/api/documentation.phtml',
),
),
);
I personally have been using the Link header solution, as it's
so simple to implement. It does not write the documentation for you,
but thinking about it early and implementing it helps ensure you at least
start writing the documentation, and, if you open source your project,
you may find you have users who will write the documentation for you if
they know where it lives.
Conclusions
Document your API, or either nobody will use it, or all you're hear are complaints from your users about having to guess constantly about how to use it. Include the following information:
- What endpoint(s) is (are) available.
- Which operations are available for each endpoint.
- What payloads are expected by the endpoint.
- What payloads can a user expect in return.
- What media types may be used for requests.
- What media types may be expected in responses.
Additionally, make sure that you do the OPTIONS/Allow
dance; don't just accept any request method, and report the standard
405 response for methods that you will not allow. Make sure you differentiate
these for collections versus individual resources, as you likely may
allow replacing or updating an individual resource, but likely will not
want to do the same for a whole collection!
Next time
So far, I've covered the basics of RESTful JSON APIS, specifically recommending Hypermedia Application Language (HAL) for providing hypermedia linking and relations. I've covered error reporting, and provided two potential formats (API-Problem and vnd.error) for use with your APIs. Now, in this article, I've shown a bit about documenting your API both for machine consumption as well as end-users. What's left?
In upcoming parts, I'll talk about ZF2's AbstractRestfulController
in more detail, as well as how to perform some basic content negotiation.
I've also had requests about how one might deal with API versioning, and will
attempt to demonstrate some techniques for doing that as well. Finally,
expect to see a post showing how I've tied all of this together in a
general-purpose ZF2 module so that you can ignore all of these posts and simply
start writing APIs.
Updates
Note: I'll update this post with links to the other posts in the series as I publish them.
blog comments powered by Disqus