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:

  • Authorisation;
  • Authentication;
  • Conditional redirection;
  • Caching;
  • 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!

HTTP Protocol

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:

Webmachine

Here's an example from webmachine-ruby:

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 Accept header that tolerates application/json;
  • 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.

More

The original implementation of Webmachine is in Erlang but you can find webmachine in multiple programming languages: Erlang, Ruby, Clojure, Javascript, etc ...

There's also a neat presentation by Sean Cribbs: Resources, For Real This Time (with Webmachine)

Posted on November 19, 2014