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
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.
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.