REST. What is it, and how can it be used to design better web applications?
A presentation at RailsConf did me a great service by first pointing out all the things REST is not. It isn’t CRUD. It isn’t pretty URLs. It is neither a protocol nor an architecture, but it can play a role in your implementation of all of the above. REST itself though, is less concrete than all of that. It is a theoretical framework, a way of thinking about designing distributed software systems. For me, the first step in absorbing its principles is to forget about the database and focus on the fundamentals. This article will start there, then drill down to show how these ideas can help organize the development of your Rails applications.
(Noon: Rest From Work (After Millet) by Vincent Van Gogh)
REST encourages a focus on resources. A resource is anything that can be named, and your system can have as many resources and corresponding names as you want. Conversely, there is a limited set of operations defined on those resources. Unlike objects in object-oriented programming languages, which support very diverse, rich interfaces, resources in a RESTful system are relatively uniform. So how can a sophisticated API be developed if REST requires a fixed and limited number of operations that resources can support? The answer: add more resources!
Lets play with an example. Say I’m writing a web application that has a collection of shapes that can be moved to new locations. In an object oriented program, I might have a move method defined on each shape, but in this RESTful API, I have assigned consistent semantics to a limited set of operations supported by the HTTP protocol. I can show
PUT, and destroy
DELETE my resources. Nowhere in the HTTP specification is the a
MOVESHAPE method defined. A naiive approach to remedying this limitation is to shoehorn this operation into the protocol by abusing URLs.
I call this abuse because URL stands for uniform resource locator, and it’s very hard to see this imperative-style command encoding as much of a resource. This is an API decision that fights the nature of the protocol it uses. How can the API go with the flow?
There are many potential solutions. Let me outline two, the first very simple, the second more sophisticated.
The first solution is to recognize that a move operation is just a change to the location of an object. If we expose this location as a resource and allow it to be updated, we’ll implement movement in a natural way without contorting URLs to name things that aren’t actually resources:
So we combine one of the four standard operations:
Update, represented by an HTTP
With a resource:
Updating the location of the shape will naturally equate to moving it.
But what if we want the movement to be relative to the objects current position, so that the client can say that they want a shape to move 5 pixels up and 10 pixels to the right without needing to know the objects current position or do any computation? To solve that problem, we apply a technique I learned doing computational semantics: reification.
Reification means that we give solid form or objecthood to something formerly fleeting or ephemeral. Anything can be reified. The fact that I am named Nathan Sobo can be thought of as my NathanSoboness, which is an (albeit abstract) conceptual object. Here we’ll apply the technique in a more conservative fashion, and say that shapes are associated with a history of movements. This movement history is a collection, which is itself a resource.
Now say we want to move the shape. We combine the above resource locator with one of our standard operations, create, implemented as an HTTP POST. By posting a new movement to a shapes history, we cause the shape to move.
Now we’re working with HTTP rather than around it.
So how does this transfer to the design of Rails applications? Embracing resource oriented application development means you’ll be writing more controllers with fewer, more consistent methods. Lets work through a potential implementation of the shape API in Rails. It will all start in the routes file, with
Lets say we want to expose both the relative and absolute means of moving a shape. First we’ll start with a shapes resource.
This represents the collection of all shapes in our system. It assumes the existince of a corresponding ShapesController and will set up a series of routes and url-generating methods to help reference the actions therein. Note the controller and its standard complement of methods below.
class ShapesController < ActionController::Base def index end def show end def create end def edit end def update end def destroy end end
But with map.resources, the actions on the controller do not play a critical role in the url. They merely name the operations to which a given pairing of HTTP request method and URL will map.
/shapes will execute index.
/shapes will execute create.
/shapes/:id will execute show.
/shapes/:id will execute update, and
/shapes/:id will execute destroy. So even though there are 5 actions, there are really only two url patterns, one referencing the resource that is the collection of all shapes, and another referencing resources that are members of that collection. We can reference these URLs with automatically defined methods:
shapes_url ---> /shapes shape_url(@square) or shape_url(@square.id) ---> /shapes/1
By pairing these with the correct HTTP method, we can access every operation we need.
Now lets add relative movement:
map.resources :shapes do |shape| shape.resources :relative_movements, :name_prefix => 'shape_' end class RelativeMovementController < ActionController::Base ... end
This again assumes the existence of a RelativeMovementController with all of the standard methods defined on it. Except the resources supported by this controller are nested within shape resources, so the URL patterns look like this:
Because of the
:name_prefix supplied (which will no longer be needed at some release of Rails in the future), we can refer to these URLs with helper methods that look like this:
shape_relative_movements_url(@triangle) ---> /shapes/1/relative_movements shape_relative_movement_url(@triangle, 4) ---> /shapes/1/relative_movements/4
All of the same rules about HTTP method choice allow access to the
Now a cool twist: Singleton resources. Lets add the nested position resource to shapes.
map.resources :shapes do |shape| shape.resources :relative_movements, :name_prefix => 'shape_' shape.resource :position, :name_prefix => 'shape_' end
And a corresponding controller, this time with a different complement of methods:
class PositionController < ActionController::Base def show end def edit end def update end end
Because position is a singleton resource nested inside of shape, this controller is designed to deal with a single resource rather than a collection of them, so there is no need for an index action. The HTTP verb / URL combination mappings are also different. So PUTting to /shapes/1/position will invoke the update action.
None of these changes are Earth-shattering, but the simple act of focusing on resources is a force that will organize your application. Rather than growing a hodgepodge of actions on ever fattening controllers, you’ll instead create a greater number of controllers that are more circumscribed in their responsibilities.
What does this say about your model? Not much. I used to think that it was important to have a controller for every model object, and I no longer do. Controllers are responsible for supporting the exposure of resources to a remote API. This collection of resources is, in a sense, your remote client’s model. Whether your resources map precisely onto your underlying data model is your business. For example, you might expose resources that have no direct correspondence in the model layer. Or you might have model objects that you don’t choose to expose as resources.
But regardless, REST finally provides an organizing principle for the controller layer. Even if you don’t plan on exposing a RESTful API as a service, thinking in terms of resources will help you build more consistent applications and help you make fewer decisions.
I realize that this article has by no means covered every aspect of REST, but I hope it fills a gap that I felt as I was learning all of this.