Zend Framework has a very neat action helper called ContextSwitch. With this helper you can easily output data from your actions in other formats besides HTML. For instance, when you create an AJAX-callback action, you’d like to have the output in JSON format, or simple API actions might output its content in XML. The ContextSwitch makes this task easy for you. But what if you are writing an API and a client wants its output in JSON instead of XML? Or maybe it wants HTML. You could use the ContextSwitch helper for this, but do you want to add a ?format=xml behind your URI’s or might there a better way?
Some REST theory
First, lets look at some “bad” ways to return data in a specific format:
If you have 3 different representations of the same data (say: json, html and xml) you’ll end up with 3 different URI’s all pointing to the same resource.
Another problem: what kind of data do we return? The blogpost itself? The blogpost including user comments? Maybe even more data? But what about clients who only want to fetch the titles. It would be a waste of bandwidth if we must return all user comments to them. So let’s introduce another parameter:
Using these methods to control our results will end up in a big URI mess. So we must find a better way to achieve the same goals.
Meet HTTP’s Accept header
By using the Accept-header in the HTTP fields during a request, we let the client decide what kind of data, format and even version our API needs to return.
GET /api/blog/article/12345 HTTP/1.1 Host: www.enrise.com Accept: application/vnd.enrise.article+xml; version: 1.0;
Many more things go on in this simple request than meets the eye. First of all, the URI is clean again. No extra information about HOW the data should be returned is given, we only state WHAT needs to be returned. We just want to return information about blog article 12345. It’s the Accept-header which gives the server information about how the client likes the data to be returned.
We can divide this header into 3 different parts: the first part defines the format we return. Our “format” starts with “application/vnd” which is a vendor-specific format. If you define your own format you should start with this string to avoid problems with existing formats.
Adding “Accept: application/vnd.enrise.article+xml” would return the main article data in XML format, but we could just as easily return it in JSON format by issuing an “Accept: application/vnd.enrise.article+json”.
It’s even possible to return other types of information about the article. For instance, we can return the comments that users have made about this article:
GET /api/blog/article/12345 HTTP/1.1 Host: www.enrise.com Accept: application/vnd.enrise.comments+html; version: 1.2;
You see what we did here? First off all, we did not change the URI. We are still requesting information about article resource 12345. Secondly, we use a different Accept-format, in which we request vnd.enrise.comments. This could return a list of user comments. Third, we like the information to be returned in plain HTML. And last, our client wants to use version 1.2 of this comments format. It might be that version 1.2 has added user images which version 1.0 hasn’t. Older clients who connect to the API will use version 1.0 so for them we don’t return the user images. This means both new and old clients can use the same API without any backward compatibility breaks.
So with 1 single Accept line, we achieve:
- clean URI’s
- API versioning
- Context switching (xml, json, html, etc.)
- Format switching
Caching with Vary
One important aspect not to forget is that we must issue a “Vary” header back to the client. It’s not so much that clients like your browser use this field, but it’s needed for proxy and caching servers that might be between the client and the REST-server. Since we always use the same URL, but many variations of the data can be returned (depending on the Accept-header), we must tell caching and proxy servers which HTTP-headers will trigger those different formats. By issuing a “Vary: Accept” header, we tell those system that they not only must check the URI, but also the Accept-header to decide whether or not it must return cached data. Otherwise a client might end up with cached XML data while it requested the same data, but in JSON format because the caching server though the URI was the same, so would be the data.
Expanding the concept even further
How about returning user data depending on language? What if we have a single article in two different languages: English and Dutch. Depending on the Accept-Language HTTP header, we could return either the Dutch text, or the English text, or maybe even both.
In part 2 we will put this theory into practice and create some simple helpers and plugins that you can use for your own REST services. So stay tuned for the next part…