Webmachine - REST done right
Webmachine describes resources on top of HTTP to provide RESTful APIs. This is pretty much what every modern web framework states, right?
After writing some good amount of controllers on MVC frameworks you realise that it’s complicated to keep a clean controller without using callbacks/middlewares to handle things like:
- Conditional redirection;
- Content representation.
On Rails code, for example, It’s common to find before_filters (before_action on Rails 4) describing behaviour that should happen before handling the resource itself. Here’s an example of discourse’s ApplicationController:
class ApplicationController < ActionController::Base # ... before_filter :set_current_user_for_logs before_filter :set_locale before_filter :set_mobile_view before_filter :inject_preview_style before_filter :disable_customization before_filter :block_if_readonly_mode before_filter :authorize_mini_profiler before_filter :preload_json before_filter :check_xhr before_filter :redirect_to_login_if_required # ... end
These callbacks will run doing assorted things on pretty much every controller on the app. Just by looking on the name of the callbacks you can notice some redirection, authorisation, authentication and content representation logic. One important thing is that the order here matters and you will probably hit lots of issues if you change the order of the before_filters. There’s also the problem that unit tests are complex as you need to test each callback indirectly.
Could it be less complicated? Of course!
Webmachine describes the request handling as a state machine where HTTP status codes are end states. It implements the HTTP protocol as a collection of functions that will describe the flow into this state machine. Below you can see the initial state and some transitions:
Here’s an example from
class OrderResource < Webmachine::Resource def allowed_methods ["GET"] end def content_types_provided [["application/json", :to_json]] end def to_json order.to_json end private def order @order ||= Order.new(params) end def id request.path_info[:id] end end
This simple resource you will get automatically the following status:
- 405 Method Not Allowed if it’s not a GET;
- 406 Not Acceptable if the request does not specify an
Acceptheader that tolerates
- 200 OK if everything is fine with and the correct content-type header will be provided.
Now let’s handle when the order does not exist adding the
resource_exists? method to the resource:
class OrderResource < Webmachine::Resource def allowed_methods ["GET"] end def content_types_provided [["application/json", :to_json]] end def resource_exists? order end def to_json order.to_json end private def order @order ||= Order.new(params) end def id request.path_info[:id] end end
Now 404 Not Found will be given if
resource_exists? returns falsy value.
Let’s now add authorisation and authentication:
class OrderResource < Webmachine::Resource include Webmachine::Resource::Authentication def allowed_methods ["GET"] end def content_types_provided [["application/json", :to_json]] end def resource_exists? order end def is_authorized?(authorization_header) basic_auth(authorization_header, "My Application") do |username, password| @user = User.auth!(username, password) !@user.nil? end end def forbidden? order.allow?(@user) end def to_json order.to_json end private def order @order ||= Order.new(params) end def id request.path_info[:id] end end
And again easily we will get:
- 401 Unauthorized if
is_authorized?returns falsy value;
- 403 Forbidden if
forbidden?returns falsy value
This is, in my opinion, a scalable way of describing your resources as the code will be as complex as the resources are. The framework already takes care of the nuances of the HTTP protocol keeping the code clean and extremely simple to unit test.