O'Reilly logo

RESTful Web Services by Sam Ruby, Leonard Richardson

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Resource Design

In Chapters 5 and 6 I had a lot of leeway in turning my imaginary data set into resources. The idea for my map service came from the Google Maps application with its image tiles, but I took it off in another direction. I added user accounts, custom places, and other features not found in any existing map service.

This chapter works differently. I’m focusing on translating the ideas of del.icio.us into the Resource-Oriented Architecture. There are lots of ways of exposing a data set of tagged bookmarks, but I’m focusing on the ones del.icio.us actually uses. Let’s start by taking a look at what the del.icio.us web service has to offer.

The del.icio.us web service is a REST-RPC hybrid service, described in English prose at http://del.icio.us/help/api/. The web service itself is rooted at https://api.del.icio.us/v1/. The service exposes three RPC-style APIs, rooted at the relative URIs posts/, tags/, and bundles/. Beneath these URIs the web service exposes a total of twelve RPC functions that can be invoked through HTTP GET. I need to define RESTful resources that can expose at least the functionality of these three APIs:

First, the posts/ API, which lets the user fetch and manage her bookmark posts to del.icio.us:

  • posts/get: Search your posts by tag or date, or search for a specific bookmarked URI.

  • posts/recent: Fetch the n most recent posts by the authenticated user. The client may apply a tag filter: “fetch the n most recent posts that the authenticated user tagged with tag t”.

  • posts/dates: Fetch the number of posts by the authenticated user for each day: perhaps five posts on the 12th, two on the 15th, and so on. The client may apply a tag filter here, too.

  • posts/all: Fetch all posts for the authenticated user, ever. The client may apply a tag filter.

  • posts/update: Check when the authenticated user last posted a bookmark. Clients are supposed to check this before deciding to call the expensive posts/all.

  • posts/add: Create a bookmark for a URI. The client must specify a short description. It may choose to specify a long description, a set of tags, and a timestamp. A bookmark may be public or private (the default is public). A client may not bookmark the same URI more than once: calling posts/add again overwrites the old post with new information.

  • posts/delete: Deletes a user’s post for a particular URI.

Second, the tags/ API, which lets the authenticated user manage her tags separately from the bookmarks that use the tags:

  • tags/get: Fetch a list of tags used by the authenticated user.

  • tags/rename: Rename one of the authenticated user’s tags. All posts tagged with the old name will now be tagged with the new name instead.

Finally, the bundles API, which lets the authenticated user group similar tags together.

  • tags/bundles/all: Fetch the user’s bundles. The resulting document lists the bundles, and each bundle lists the tags it contains.

  • tags/bundles/set: Group several tags together into a (possibly new) bundle.

  • tags/bundles/delete: Delete a bundle.

That’s the web service. As I mentioned in Chapter 2, the service only gives you access to your own bookmarks and tags. The del.icio.us web site has social features as well, and I’m going to steal some of those features for my design.

Here are some interesting “functions” exposed by the del.icio.us web site but not the web service:

  • /{username}: Fetch any user’s bookmarks.

  • /{username}/{tag}: Fetch any user’s bookmarks, applying a tag filter.

  • /tag/{tag-name}: Fetch bookmarks tagged with a particular tag, from all users.

  • /url/{URI-MD5}: Fetch the list of users who have bookmarked a particular URI. The {URI-MD5} happens to be the MD5 hash of the URI, but from the average client’s point of view that’s not important: it’s an opaque string of bytes that somehow identifies a URI within the del.icio.us system.

  • /recent: Fetch the most recently posted bookmarks, from all users. The del.icio.us home page also shows this information.

Now that I know what the service has to do, arranging the features into resources is like working a logic puzzle. I want to expose as few kinds of resources as possible. But one kind of resource can only convey one concept, so sometimes I need to split a single feature across two kinds of resource. On the other hand, sometimes I can combine multiple RPC functions into one kind of resource, a resource that responds to several methods of HTTP’s uniform interface.

REST in Rails

I’m not designing these resources in a vacuum: I’m going to implement them in a Rails application. It’s worth taking a brief look at how RESTful applications work in Rails. Unlike some other frameworks, Rails doesn’t let you define your resources directly. Instead, it divides up an application’s functionality into controllers: it’s the controllers that expose the resources. The first path variable in a request URI is used to route Rails to the appropriate controller class. For instance, in the URI /weblogs/4 the “weblogs” designates the controller: probably a class called WeblogController. The “4” designates the database ID of a particular weblog.

In previous versions of Rails, programmers defined RPC-style methods on controllers: methods like rename and delete. To rename a weblog you’d send a GET or an overloaded POST request to /weblogs/4/rename. Rails applications, like most web applications, were REST-RPC hybrids.

In Rails 1.2, programmers define special controller methods that correspond to the methods of HTTP’s uniform interface. For instance, sending a GET to /weblogs triggers the WeblogController’s index method, which is supposed to retrieve a list of the weblogs. Sending a POST to the same URI triggers the WeblogController#create method, which creates a subordinate resource beneath /weblogs: say, a weblog with a URI of /weblogs/4. The Rails controller exposes a resource—“the list of weblogs”—that responds to GET and POST. As you’d expect, when you POST to the “list” resource you get a subordinate resource: a new weblog.

The subordinate resource also supports the uniform interface. If you wanted to rename a weblog in an RPC-style service, you might POST a new name to /weblogs/4/rename. Under a RESTful regime, you PUT a new name to /weblogs/4, triggering the WeblogController#update method. To delete a weblog, you send a DELETE request to its URI, triggering the controller’s WeblogController#destroy method. There’s no need to expose an RPC-style URI /weblogs/4/delete, because HTTP’s uniform interface already knows about deleting.

These two resources, a list and an item in the list, show up all the time. Every database table is a list that contains items. Anything that can be represented as an RSS or Atom feed is a list that contains items. Rails defines a RESTful architecture that makes a simplifying assumption: every resource you expose can be made to fit one of these two patterns. This makes things easy most of the time, but the cost is aggravation when you try to use Rails controllers to expose resources that don’t fit this simple model.

I’m going to define my resources in terms of Rails controllers. These controllers impose constraints on my URI structure and my use of the uniform interface, and I need to design with those constraints in mind. By the time I’m done designing the controllers, I’ll know which resources the controllers expose, which URIs they answer to, and which methods of the uniform interface correspond to which RPC functions from the del.icio.us service. Basically, I’ll have completed steps 2 through 4 of the 9-step procedure from the Turning Requirements into Read/Write Resources” section in Chapter 6: “Split the data set into resources,” “Name the resources with URIs,” and “Expose a subset of the uniform interface.” In Chapter 12 I give a variant of the service design procedure specifically for Rails services.

I’ll only be accessing my Rails application from my local machine. The root URI will be http://localhost:3000/v1. When I give a relative URI below, like /users, understand that I’m talking about http://localhost:3000/v1/users. I only ever plan to write one version of this service, but I’m versioning the URIs, just in case. (When and how to version is discussed in Chapter 8).

The User Controller

Now I’m going to go back to that big list of RPC functions I found in the del.icio.us API and web site, and try to tease some Rails controllers out of it. One obvious controller is one that exposes information about user accounts. In Rails, this would be a class called UsersController. As soon as I say that, a lot of decisions are made for me. Rails sets up a path of least resistance that looks like this:

The user controller exposes a one-off “user list” resource, at the URI /users. It also exposes a resource for every user on the system, at a URI that incorporates the user’s database ID: /users/52 and the like. These resources expose some subset of HTTP’s uniform interface. Which subset? Rails defines this with a programming-language interface in the superclass of all controller classes: ActionController::Base. Table 7-1 shows how the two interfaces line up.

Table 7-1. How Rails wants my UsersController to look

OperationHTTP actionRails method
List the usersGET /usersUsersController#index
Create a userPOST /usersUsersController#create
View a userGET /users/52UsersController#show
Modify a userPUT /users/52UsersController#update
Delete a userDELETE /users/52UsersController#destroy

So if I want to let clients create new user accounts, I implement UsersController#create, and my “user list” resource starts calling that method in response to POST requests.

The path of least resistance is pretty good but I have a couple problems with it. First, I don’t want to let clients fetch the list of users, because del.icio.us doesn’t have that feature. (Presumably the del.icio.us administrative interface does have a feature like this.) That’s fine: I don’t have to expose GET on every resource, and I don’t have to define index in every controller. My user list resource, at the URI /users, will only expose the POST method, for creating new users. My user list is a featureless container for user account resources, and the only thing a client can do with it is create a new account. This incorporates functionality like that at https://secure.del.icio.us/register, where you can use your web browser to sign up for a del.icio.us account.

Another problem is that URIs like /users/52 look ugly. They certainly don’t look like http://del.icio.us/leonardr, the URI to my corresponding page on del.icio.us. This URI format is the Rails default because every object in a Rails application’s database can be uniquely identified by its table (“users”) and its ID (“52”). This URI might go away (if user 52 DELETEs her account), but it will never change, because database unique IDs don’t change.

I’d rather expose readable URIs that might change occasionally than permanent URIs that don’t say anything, so I’m going to identify a user using elements of its resource state. I happen to know that users have unique names, so I’m going to expose my “user” resources at URIs like /users/leonardr. Each resource of this type will expose the methods GET, PUT, and DELETE. This incorporates the functionality of the del.icio.us web site’s /{username} “function.” It also incorporates the pages on the web site (I didn’t mention these earlier) that let you edit and delete your own del.icio.us account.

To expose this RESTful interface, I just need to implement four special methods on UsersController. The create method implements POST on the “user list” resource at /users. The other three methods implement HTTP methods on the “user” resources at /users/{username}: show implements GET, update implements PUT, and destroy implements DELETE.

The Bookmarks Controller

Each user account has a number of subordinate resources associated with it: the user’s bookmarks. I’m going to expose these resources through a second controller class, rooted beneath the “user account” resource.

The base URI of this controller will be /users/{username}/bookmarks. Like the users controller, the bookmarks controller exposes two types of resource: a one-off resource for the list of a user’s bookmarks, and one resource for each individual bookmark.

Rails wants to expose an individual bookmark under the URI /users/{username}/bookmarks/{database-id}. I don’t like this any more than I like /users/{database-id}. I’d like the URI to a bookmark to have some visible relationship to the URI that got bookmarked.

My original plan was to incorporate the target URI in the URI to the bookmark. That way if I bookmarked http://www.oreilly.com/, the bookmark resource would be available at /v1/users/leonardr/bookmarks/http://www.oreilly.com/. Lots of services work this way, including the W3C’s HTML validator. Looking at one of these URIs you can easily tell who bookmarked what. Rails didn’t like this URI format, though, and after trying some hacks I decided to get back on Rails’s path of least resistance. Instead of embedding external URIs in my resource URIs, I’m going to put the URI through a one-way hash function and embed the hashed string instead.

If you go to http://del.icio.us/url/55020a5384313579a5f11e75c1818b89 in your web browser, you’ll see the list of people who’ve bookmarked http://www.oreilly.com/. There’s no obvious connection between the URI and its MD5 hash, but if you know the former you can calculate the latter. It’s certainly better than a totally opaque database ID. And since it’s a single alphanumeric string, Rails handles it with ease. My bookmark resources will have URIs like /v1/users/leonardr/bookmarks/55020a5384313579a5f11e75c1818b89. That URI identifies the time I bookmarked http://www.oreilly.com/ (see Example 7-2).

Example 7-2. Calculating an MD5 hash in Ruby

require 'digest/md5'
# => "55020a5384313579a5f11e75c1818b89"

When a user is first created it has no bookmarks. A client creates bookmarks by sending a POST request to its own “bookmark list” resource, just as it might create a user account by sending a POST to the “user list” resource. This takes care of the posts/add and posts/delete functions from the del.icio.us API.

Unlike with the list of users, I do want to let clients fetch the list of a user’s bookmarks. This means /users/{username}/bookmarks will respond to GET. The individual bookmarks will respond to GET, PUT, and DELETE. This means the BookmarksController: index, create, show, update, and delete.

The “bookmark list” resource incorporates some of the functionality from the del.icio.us API functions posts/get, posts/recent, and posts/all.

The User Tags Controller

Bookmarks aren’t the only type of resource that conceptually fits “beneath” a user account. There’s also the user’s tag vocabulary. I’m not talking about tags in general here: I’m asking questions about which tags a particular user likes to use. These questions are handled by the user tags controller.

This controller is rooted at /users/{username}/tags. That’s the “user tag list” resource. It’s an algorithmic resource, generated from the tags a user uses to talk about her bookmarks. This resource corresponds roughly to the del.icio.us tags/get function. It’s a read-only resource: a user can’t modify her vocabulary directly, only by changing the way she uses tags in bookmarks.

The resources at /users/{username}/tags/{tag} talk about the user’s use of a specific tag. My representation will show which bookmarks a user has filed under a particular tag. This class of resource corresponds to the /{username}/{tag} “function” from the web site. It also incorporates some stuff of the del.icio.us API functions posts/get, posts/recent, and posts/all.

The “tag” resources are also algorithmic, but they’re not strictly read-only. A user can’t delete a tag except by removing it from all of her bookmarks, but I do want to let users rename tags. (Tag deletion is a plausible feature, but I’m not implementing it because, again, del.icio.us doesn’t have it.) So each user-tag resource will expose PUT for clients who want to rename that tag.

Instead of PUT, I could have used overloaded POST to define a one-off “rename” method like the del.icio.us API’s tag/rename. I didn’t, because that’s RPC-style thinking. The PUT method suffices to convey any state change, whether it’s a rename or something else. There’s a subtle difference between renaming the tag and changing its state so the name is different, but it’s the difference between an RPC-style interface and a uniform, RESTful one. It’s less work to program a computer to understand a generic “change the state” than to program it to understand “rename a tag.”

The Calendar Controller

A user’s posting history—her calendar— is handled by one more controller that lives “underneath” a user account resource. The posting history is another algorithmically generated, read-only resource: you can’t change your posting history except by posting. The controller’s root URI is /users/{username}/calendar, and it corresponds to the del.icio.us API’s posts/dates function.

I’ll also expose a variety of subresources, one for each tag in a user’s vocabulary. These resources will give a user’s posting history when only one tag is considered. These resources correspond to the del.icio.us API’s posts/dates function with a tag filter applied. Both kinds of resource, posting history and filtered posting history, will expose only GET.

The URI Controller

I mentioned earlier that URIs in a social bookmarking system have emergent properties. The URI controller gives access to some of those properties. It’s rooted at /uris/, and it exposes URIs as resources independent from the users who bookmark them.

I’m not exposing this controller’s root URI as a resource, though I could. The logical thing to put there would be a huge list of all URIs known to the application. But again, the site I’m taking for my model doesn’t have any feature like that. Instead, I’m exposing a series of resources at /uris/{URI-MD5}: one resource for each URI known to the application. The URI format is the same as /users/{username}/bookmarks/{URI-MD5} in the user bookmark controller: calculate the MD5 hash of the target URI and stick it onto the end of the controller’s base URI.

These resources expose the application’s knowledge about a specific URI, such as which users have bookmarked it. This corresponds to the /url/{URI-MD5} “function” on the del.icio.us web site.

The Recent Bookmarks Controller

My last implemented controller reveals another emergent property of the URIs. In this case the property is newness: which URIs were most recently posted.

This controller is rooted at /recent. The top-level “list” resource lists all the recently posted bookmarks. This corresponds to the /recent “function” on the del.icio.us web site.

The sub-resources at /recent/{tag} expose the list of recently posted bookmarks that were tagged with a particular tag. For instance, a client can GET /recent/recipes to find recently posted URIs that were tagged with “recipes”. This corresponds to the /tag/{tag-name} function on the del.icio.us web site.

The Bundles Controller

Again, I’m not going to implement this controller, but I want to design it so you can see I’m not cheating. This controller is rooted at /user/{username}/bundles/. An alternative is /user/{username}/tags/bundles/, but that would prevent any user from having a tag named “bundles”. A client can send a GET request to the appropriate URI to get any user’s “bundle list”. A client can POST to its own bundle list to create a new bundle. This takes care of tags/bundles/all and part of tags/bundles/set.

The sub-resources at /user/{username}/bundles/{bundle} expose the individual bundles by name. These respond to GET (to see which tags are in a particular bundle), PUT (to modify the tags associated with a bundle), and DELETE (to delete a bundle). This takes care of tags/bundles/delete and the rest of tags/bundles/set.

The Leftovers

What’s left? I’ve covered almost all the functionality of the original del.icio.us API, but I haven’t placed the posts/update function. This function is designed to let a client avoid calling posts/all when there’s no new data there. Why bother? Because the posts/all function is extremely expensive on the server side. A del.icio.us client is supposed to keep track of the last time it called posts/all, and check that time against the “return value” of posts/update before calling the expensive function again.

There’s already a solution for this built into HTTP: conditional GET. I cover it briefly in Conditional GET later in this chapter and I’ll cover it in more detail in Chapter 8, but in this chapter you’ll see it implemented. By implementing conditional GET, I can give the time- and bandwidth-saving benefits of posts/update to most of the resources I’m exposing, not just the single most expensive one.

Remodeling the REST Way

I’ve taken an RPC-style web service that was only RESTful in certain places and by accident, and turned it into a set of fully RESTful resources. I’d like to take a break now and illustrate how the two services line up with each other. Tables 7-2 through 7-6 show every social bookmarking operation I implemented, the HTTP request you’d send to invoke that operation on my RESTful web service, and how you’d invoke the corresponding operation on del.icio.us itself.

Table 7-2. Service comparison: user accounts

OperationOn my serviceOn del.icio.us
Create a user accountPOST /usersPOST /register (via web site)
View a user accountGET /users/{username}GET /users/{username} (via web site)
Modify a user accountPUT /users/{username}Various, via web site
Delete a user accountDELETE /users/{username}POST /settings/{username}/profile/delete (via web site)

Table 7-3. Service comparison: bookmark management

OperationOn my serviceOn del.icio.us
Post a bookmarkPOST /users/{username}/bookmarksGET /posts/add
Fetch a bookmarkGET /users/{username}/bookmarks/{URI-MD5}GET /posts/get
Modify a bookmarkPUT /users/{username}/bookmarks/{URI-MD5}GET /posts/add
Delete a bookmarkDELETE /users/{username}/bookmarks/{URI-MD5}GET /posts/delete
See when the user last posted a bookmarkUse conditional HTTP GETGET /posts/update
Fetch a user’s posting historyGET /users/{username}/calendarGET /posts/dates (your history only)
Fetch a user’s posting history, filtered by tagGET /users/{username}/calendar/{tag}GET /posts/dates with query string (your history only)

Table 7-4. Service comparison: finding bookmarks

OperationOn my serviceOn del.icio.us
Fetch a user’s recent bookmarksGET /users/{username}/bookmarks with query stringGET /posts/recent (your bookmarks only)
Fetch all of a user’s bookmarksGET /users/{username}/bookmarksGET /posts/all (your bookmarks only)
Search a user’s bookmarks by dateGET /users/{username}/bookmarks with query stringGET /posts/get with query string (your bookmarks only)
Fetch a user’s bookmarks tagged with a certain tagGET /users/{username}/bookmarks/{tag}GET /posts/get with query string (your bookmarks only)

Table 7-5. Service comparison: social features

OperationOn my serviceOn del.icio.us
See recently posted bookmarksGET /recentGET /recent (via web site)
See recently posted bookmarks for a certain tagGET /recent/{tag}GET /tag/{tag} (via web site)
See which users have bookmarked a certain URIGET /uris/{URI-MD5}GET /url/{URI-MD5} (via web site)

Table 7-6. Service comparison: tags and tag bundles

OperationOn my serviceOn del.icio.us
Fetch a user’s tag vocabularyGET /users/{username}/tagsGET /tags/get (your tags only)
Rename a tagPUT /users/{username}/tags/{tag}GET /tags/rename
Fetch the list of a user’s tag bundlesGET /users/{username}/bundlesGET /tags/bundles/all (your bundles only)
Group tags into a bundlePOST /users/{username}/bundlesGET /tags/bundles/set
Fetch a bundleGET /users/{username}/bundles/{bundle}N/A
Modify a bundlePUT /users/{username}/bundles/{bundle}GET /tags/bundles/set
Delete a bundleDELETE /users/{username}/bundles/{bundle}GET /tags/bundles/delete

I think you’ll agree that the RESTful service is more self-consistent, even accounting for the fact that some of the del.icio.us features come from the web service and some from the web site. Table 7-6 is probably the best for a straight-up comparison. There you can distinctly see the main advantage of my RESTful service: its use of the HTTP method to remove the operation name from the URI. This lets the URI identify an object in the object-oriented sense. By varying the HTTP method you can perform different operations on the object. Instead of having to understand some number of arbitrarily-named functions, you can understand a single class (in the object-oriented sense) whose instances expose a standardized interface.

My service also lifts various restrictions found in the del.icio.us web service. Most notably, you can see other peoples’ public bookmarks. Now, sometimes restrictions are the accidental consequences of bad design, but sometimes they exist for a reason. If I were deploying this service commercially it might turn out that I want to add those limits back in. I might not want user A to have unlimited access to user B’s bookmark list. I don’t have to change my design to add these limits. I just have to change the authorization component of my service. I make it so that authenticating as userA doesn’t authorize you to fetch userB’s public bookmarks, any more than it authorizes you to delete userB’s account. Or if bandwidth is the problem, I might limit how often any user can perform certain operations. I haven’t changed my resources at all: I’ve just added additional rules about when operations on those resources will succeed.

Implementation: The routes.rb File

Ready for some more code? I’ve split my data set into Rails controllers, and each Rails controller has divided its data set further into one or two kinds of resources. Rails has also made decisions about what my URIs will look like. I vetoed some of these decisions (like /users/52, which I changed to /users/leonardr), but most of them I’m going to let stand.

I’ll implement the controllers as Ruby classes, but what about the URIs? I need some way of mapping path fragments like bookmarks/ to controller classes like BookmarksController. In a Rails application, this is the job of the routes.rb file. Example 7-3 is a routes.rb that sets up URIs for the six controllers I’ll implement later in the chapter.

Example 7-3. The routes.rb file

# service/config/routes.rb
ActionController::Routing::Routes.draw do |map|
  base = '/v1'

  ## The first controller I define is the UsersController. The call to
  ## map.resources sets it up so that all HTTP requests to /v1/users
  ## or /v1/users/{username} are routed to the UsersController class.

  # /v1/users => UsersController
  map.resources :users, :path_prefix => base

  ## Now I'm going to define a number of controllers beneath the
  ## UsersController. They will respond to requests for URIs that start out
  ## with /v1/users/{username}, and then have some extra stuff.
  user_base = base + '/users/:username'

  # /v1/users/{username}/bookmarks => BookmarksController
  map.resources :bookmarks, :path_prefix => user_base

  # /v1/users/{username}/tags => TagsController
  map.resources :tags, :path_prefix => user_base

  # /v1/users/{username}/calendar => CalendarController
  map.resources :calendar, :path_prefix => user_base

  ## Finally, two more controllers that are rooted beneath /v1.

  # /v1/recent => RecentController
  map.resources :recent, :path_prefix => base

  # /v1/uris => UrisController
  map.resources :uris, :path_prefix => base

Now I’m committed to defining six controller classes. The code in Example 7-3 determines the class names by tying into Rails’ naming conventions. My six classes are called UsersController, BookmarksController, TagsController, CalendarController, RecentController, and UrisController. Each class controls one or two kinds of resources. Each controller implements a specially-named Ruby method for each HTTP method the resources expose.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required