Use exceptions in Rails to create RESTful endpoints
We have recently started to use a pattern on my project that I am currently in love with. While it is probably too early to call this a "Do This or Question Why You Do Not", I can see it rapidly becoming that way.
The typical Rails controller
The Rails controller that we typically see is something along the lines of the code below. In this code we are using some straight forward patterns of accessing objects from a database, showing them (through a template), checking to see if our new Post object is valid and handle the good and bad cases in an if statement. This is very basic Rails code that any Rails developer would be able to easily recognize and use.
def show
@post = Post.find(params[:id])
end
def create
post = Post.new(params[:post])
if post.save
flash[:success] = t(:post_created)
redirect_to post
else
flash[:error] = post.errors.full_messages
render 'new'
end
end
Rails controllers for API endpoints
However, this is not entirely RESTful and can even hurt in the world of creating APIs. Let us assume, for the moment, that we are creating these same endpoints for a single page Javascript client. The code would look similar to the following (I'm assuming a templating language like rabl):
def show
@post = Post.find(params[:id])
end
def create
@post = Post.new(params[:post])
unless @post.save
head :unprocessable_entity
end
end
Here we are doing much the same work and are a little more restful. We are returning 200 and 422 and 500 error responses. This is not quite restful, though. We should really handle the situation in show where an invalid id is being requested.
def show @post = Post.where(params[:id]).first head :not_found unless @post end
And, now, we are actually going to populate these posts with images from Gravatar. We need to fetch some links.
def show @post = Post.where(params[:id]).first @gravatar = GravatarService.fetch_for(@post.user) head :not_found unless @post end
But, it is possible for the Gravatar service to time out or explode in some expected way that returns a 500 error. That 500 error is actually right in its class (Server Error) but wrong in its specificity (it is probably a 503 Service Unavailable error). So, we need to be a little better about handling that error
def show
@post = Post.where(params[:id]).first
begin
@gravatar = GravatarService.fetch_for(@post.user)
rescue GravatarServiceError
head :service_unavailable
return
end
head :not_found unless @post
end
And, now we're off to the races. Our clean PostsController#show and PostsController#create has turned ugly.
Use Exceptions
So, we're going to use exceptions to clean this up correctly. Here's how I would write those in the new pattern
def show
@post = Post.find(params[:id])
@gravatar = GravatarService.fetch_for(@post.user)
rescue Mongoid::Errors::DocumentNotFound # Or AR equivalent
head :not_found
rescue GravatarServiceError
head :service_unavailable
end
def create
post = Post.new(params[:post])
if post.save
head :created
else
head :unprocessable_entity
end
end
And, if I really had my druthers (which I do on my project), we'd be happier to use a Service layer instead of AR.new/AR.create.
def create PostService.create(params[:post]) head :created rescue PostServiceError head :unprocessable_entity end
So, we are now looking down the barrel of a very RESTful API endpoint. It is very clean and describes exactly the response that we expect based on how the server is behaving. If we cannot create a new Post (for whatever reason.. we don't know what it is here), we return a response of 422 Unprocessable Entity to the client.
Again, I am really liking how clean this is. However, I can see some issues with it: we are currently using this for an API-only area of our application. It looks to be working wonderfully for us. However, if we extend this to all parts of the application I wonder about spaghetti exceptions or introducing exceptions in one part of the code that effects multiple endpoints.
If we maintain this in only the API part of the application, we have a conflict of styles if I'm using a service in both parts of the application. One will not expect the service to throw exceptions and the other will expect it.
All in all, though, this seems to be a great start to correctly handling many different return codes from an HTTP resource. If we're trying to be more RESTful with out resources, we need a way to handle different kinds of errors at various points in the process and exceptions are looking to be a good way to solve it.