People who've been following me on Twitter know that I've been spending my weekends turning RSS Bandit into a desktop client for Google Reader and NewsGator Online. Although I was familiar with NewsGator's SOAP API, I didn't have the patience to figure out the differences in using SOAP services that had been made in Visual Studio 2008 given the last time I tried it I was using Visual Studio 2003 and there seemed to be fairly significant differences. In addition, the chance to program against a fully featured RESTful API also piqued my curiosity. For these reasons I decided to use the NewsGator REST API when working on the features for synchronizing with NewsGator Online.
After I completed work on the synchronization with NewsGator Online, it was quite clear to me that the developer(s) who designed the NewsGator REST API didn't really understand the principles behind building RESTful services. For the most part, it looks like a big chunk of the work done to create a REST API was to strip the SOAP envelopes from their existing SOAP services and then switch to URL parameters instead of using SOAP messages for requests. This isn't REST, it is POX/HTTP (Plain Old XML over HTTP). Although this approach gets you out of the interoperability and complexity tax which you get from using XSD/SOAP/WS-*, it doesn't give you all of the benefits of conforming to the Web's natural architecture.
So I thought it would be useful to take a look at some aspects of the NewsGator REST API and see what is gained/lost by making them more RESTful.
What Should a Feed Reader's REST API Look Like?
Before the NewsGator REST API can be critiqued, it is a good idea to have a mental model of what the API would look like if it was RESTful. To do this, we can follow the steps in Joe Gregorio's How to Create a REST Protocol which covers the four decisions you must make as part of the design process for your service
There are two main resources or data types in a feed reader; feeds and subscription lists. When building RESTful protocols, it pays to reuse standard XML formats for your message payloads since it increases the likelihood that your clients will have libraries and tools for dealing with your payloads. Standard formats for feeds are RSS and Atom (either is fine although I'd prefer to go with Atom) and a standard format for feed lists is OPML. In addition, there are bits of state you want to associate with feeds (e.g. what items have been read/unread/flagged/etc) and with subscription lists (e.g. username/password of authenticated feeds, etc). Since the aforementioned XML formats are extensible, this is not a problem to accommodate.
What methods to support also seems straightforward. You will want full Create, Retrieve, Update and Delete operations on the subscription list. So you will need the full complement of supporting POST, GET, PUT and DELETE on subscription lists. For feeds, you will only need to fetch them and update the user-specific associated with each feed such as if an item has been read or flagged. So you'd want to support GET and PUT/POST.
The question of what error codes to return would probably be at least 200 OK (success), 304 Not Modified (feeds haven't changed since last fetch), 400 Bad Request (invalid or missing request parameters) ,401 Unauthorized (invalid or no authentication credentials provided) and 404 Not Found (self explanatory).
This seems like a textbook example of a situation that calls for using the Atom Publishing Protocol (AtomPub). The only wrinkle would be that AtomPub requires uploading the entire document with changes when updating the state of an item or media resource. This is pretty wasteful when updating the state of an item in an RSS feed (e.g. indicating that the user has read the item) since you'd end up re-uploading all the content of the item just to change the equivalent of a flag on the item. There have been a number of proposals and workarounds proposed for this limitation of AtomPub such as the HTTP PATCH Internet Draft and Astoria's introduction of a MERGE HTTP method. In thinking about this problem, I'm going to assume that the ideal service supports partial updates of feed and subscription documents using PATCH/MERGE or something similar.
Now we have enough of a background to thoughtfully critique some of the API design choices in the NewsGator API.
When a lot of developers think of REST they do not think of the REpresentational State Transfer (REST) architectural style as described in chapter five of Roy Fieldings' Ph.D thesis. Instead they think of exposing remote procedure calls (RPC) over the Web using plain old XML payloads, without schemas and without having to use an interface definition language (i.e XML-based RPC without the encumbrance of SOAP, XSD or WSDL).
The problem with this thinking is that it often violates the key idea behind REST. In a RESTful Web service, the URIs identify the nouns (aka resources) that occur in the service and the verbs are the HTTP methods that operate on those resources. In an RPC-style service, the URLs identify verbs (aka methods) and the nouns are the parameters to these methods. The problem with the RPC style design is that it increases the complexity of the clients that interact with the system. Instead of a client simply knowing how to interact with a single resource (e.g. an RSS feed) and then signifying changes in state by the addition/removal of data in the document (e.g. adding an <is-read>true</is-read> element as an extension to indicate an item is read), it has to specifically target each of the different behaviors a system supports for that item explicitly (e.g. client needs to code against markread(), flagitem(), shareitem(), rateitem(), etc). The reduced surface area of the interface is not only a benefit to the client but to the service as well.
Below are a few examples which contrast the approach taken by the NewsGator API with a more RESTful method based on AtomPub.
1a) Modifying the name of one of the locations a user synchronizes data from using the Newsgator RESTful API
Service URL http://services.newsgator.com/ngws/svc/Location.aspx/<locationId>/changelocationnamebylocationid Method POST Request Format application/x-www-form-urlencoded Payload "locationname=<new location name>” Response No response
1b.) Modifying the name of one of the locations a user synchronizes data from using a more RESTful approach
Service URL http://services.newsgator.com/ngws/svc/Location.aspx/<locationId>/ Method POST Request Format application/xml Payload <opml xmlns:ng="http://newsgator.com/schema/opml"> <head /> <body> <outline text="<location name>" ng:isPublic="<True|False>" ng:autoAddSubs="<True|False>" /> </body> </opml> Response No response
<opml xmlns:ng="http://newsgator.com/schema/opml"> <head /> <body> <outline text="<location name>" ng:isPublic="<True|False>" ng:autoAddSubs="<True|False>" /> </body> </opml>
<opml xmlns:ng="http://newsgator.com/schema/opml">
<head />
<body> <outline text="<location name>" ng:isPublic="<True|False>" ng:autoAddSubs="<True|False>" />
</body>
</opml>
2a.) Deleting a folder using the NewsGator REST API
Service URL http://services.newsgator.com/ngws/svc/Folder.aspx/delete/ Method DELETE Request Format application/x-www-form-urlencoded Payload "fld=<NewsGator folder id>" Response No response
2b.) Deleting a folder using a more RESTful approach
Service URL http://services.newsgator.com/ngws/svc/Folder.aspx/<folder-id> Method DELETE Request Format Not applicable Payload Not applicable Response No response
3a.) Retrieve a folder or create it if it doesn't exist using the NewsGator REST API
Service URL http://services.newsgator.com/ngws/svc/Folder.aspx/getorcreate Method POST Request Format application/x-www-form-urlencoded Payload "parentid=<NewsGator folder id>&name=<folder name>&root=<MYF|MYC>" Response <opml xmlns:ng="http://newsgator.com/schema/opml"> <head> <title>getorcreate</title> </head> <body> <outline text="<folder name>" ng:id="<NewsGator folder id>" /> </body> </opml>
<opml xmlns:ng="http://newsgator.com/schema/opml"> <head> <title>getorcreate</title> </head> <body> <outline text="<folder name>" ng:id="<NewsGator folder id>" /> </body> </opml>
<head>
<title>getorcreate</title>
</head>
<body> <outline text="<folder name>" ng:id="<NewsGator folder id>" />
3b.) Retrieve a folder or create it if it doesn't exist using a more RESTful approach (the approach below supports creating multiple and/or nested folders in a single call)
Service URL http://services.newsgator.com/ngws/svc/Folder.aspx/<root>/<folder-id> Method POST Request Format application/xml Payload <opml xmlns:ng="http://newsgator.com/schema/opml"> <head /> <body> <outline text="<folder name>" /> </body> </opml> Response <opml xmlns:ng="http://newsgator.com/schema/opml"> <head> <title>getorcreate</title> </head> <body> <outline text="<folder name>" ng:id="<NewsGator folder id>" /> </body> </opml>
<opml xmlns:ng="http://newsgator.com/schema/opml"> <head /> <body> <outline text="<folder name>" /> </body> </opml>
<body> <outline text="<folder name>" />
In REST, all resources are interacted with using the same generic and uniform interface (i.e. via the HTTP methods - GET, PUT, DELETE, POST, etc). However it defeats the purpose of exposing a uniform interface if resources in the same system respond very differently when interacting with them using the same HTTP method. Consider the following examples taken from the NewsGator REST API.
1.) Deleting a folder using the NewsGator REST API
Service URL http://services.newsgator.com/ngws/svc/Folder.aspx/delete Method DELETE Request Format application/x-www-form-urlencoded Payload "fld=<NewsGator folder id>" Response No response
2.) Deleting a feed using the NewsGator REST API
Service URL http://services.newsgator.com/ngws/svc/Subscription.aspx Method DELETE Request Format application/xml Payload <opml xmlns:ng="http://newsgator.com/schema/opml"> <head> <title>delete</title> </head> <body> <outline text=”subscription title” ng:id=”<NewsGator subscription id>” ng:statusCode=”<0|1>” /> </body> </opml> Response No response
<opml xmlns:ng="http://newsgator.com/schema/opml"> <head> <title>delete</title> </head> <body> <outline text=”subscription title” ng:id=”<NewsGator subscription id>” ng:statusCode=”<0|1>” /> </body> </opml>
<title>delete</title>
<body>
<outline text=”subscription title” ng:id=”<NewsGator subscription id>”
ng:statusCode=”<0|1>” />
From the end user or application developer's perspective the above actions aren't fundamentally different. In both cases, the user is removing part of their subscription list which in turn deletes some subset of the <outline> elements in the user's subscriptions OPML file.
However the NewsGator REST API exposes this fundamentally identical task in radically different ways for deleting folders versus subscriptions. The service URL is different. The request format is different. Even the response is different. There's no reason why all three of these can't be the same for both folders and subscriptions in a user's OPML feed list. Although it may seem like I'm singling out the NewsGator REST API, I've seen lots of REST APIs that have similarly missed the point when it comes to using REST to expose a uniform interface to their service.
These aren't the only mistakes developers make when designing a REST API, they are just the most common ones. They often are a sign that the developers simply ported some old or legacy API without actually trying to make it RESTful. This is the clearly case with the NewsGator REST API which is obviously a thin veneer over the NewsGator SOAP API.
If you are going to build a RESTful API, do it right. Your developers will thank you for it.
Now Playing: Montell Jordan - This is How We Do It