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
OPTIONS
request is made, return a response with anAllow
header 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.