Rails API - my simple approach

… and check why 5600+ Rails engineers read also this

Rails API - my simple approach

I have seen people using very different techniques to build API around Rails applications. I wanted to show what I like to do in my projects. Mostly because it is a very simple solution and it does not make your models concerned with another responsibility.

Naming

First, I have a problem with the naming around API. I believe we use invalid nomenclature to describe our intentions. Let’s think about it for a moment. Imagine we have a Customer object and we need to keep it somewhere between the restarts of our application (not necessarily Rails application). So what do we do ? We use serialization to store it in a file. May it be binary format, JSON, XML or YAML:

require 'yaml'

class Customer < Struct.new(:first_name, :last_name, :email) # Or ActiveRecord::Base
  def full_name
    [first_name, last_name].join(" ")
  end
end

c = Customer.new("Robert", "Pankowecki")
text = c.to_yaml
# =>
# --- !ruby/struct:Customer
# first_name: Robert
# last_name: Pankowecki
# email:

File.open("serialized.txt", "w"){|f| f.puts(text) }

c2 = YAML.load(text)
# => #<struct Customer first_name="Robert", last_name="Pankowecki", email=nil>

c2 == c
#=> true

What is the point of serialization ?

To store the inner state of an object and use it to recreate it later.

But this is not what we usually want to achieve when building APIs. In such case we want to deliver some data to the consumer of our API. We don’t try to save the state of an object.

Rather I would say, we present it. Therefore I prefer to use the name serialization when the object is stored and processed by the same application and its inner state is stored. And the name presenter sounds good to me in cases when you talk about an object with a separate application. When you display it to others. When you show its, what I would say, external state (if such thing might exist).

You might wanna ask “well, what is the difference”? I shall answer you immediately.

The inner state and external state might often not be the same thing. In our case we store first_name and last_name separately but our clients might only be interested in full_name. There is no reason to send them {"first_name":"Robert","last_name":"Pankowecki"} when they actually need: {"full_name":"Robert Pankowecki"}.

So… What shall we do ? Bring up the presenters on stage.

Initial implementation

Presenter, for me, in API requests has a role similar to the View layer in classic requests to obtain HTML page. We want a layer whose responsibility is to build the response data. And we want it to be separated from our domain and most likely contain some presentation logic that should not be in model.

class CustomerPresenter
  attr_accessor :customer
  delegate :full_name, to: :customer

  def initialize(customer)
    @customer = customer
  end

  def as_json(*)
    {
      fullName: full_name
    }
  end
end

You look at that as_json method and you know from the first look what is being sent to your API clients. How do you use it in a controller ?

class CustomersController < ApplicationController
  respond_to :json

  def show
    customer  = Customer.find(params[:id])
    presenter = CustomerPresenter.new(customer)
    respond_with(presenter)
  end
end

As simple as that.

Presenters might have logic

Let’s say that the consumers of the API would like to display the avatar of Customer. We know the email of a customer so we might compute Gravatar url and give it the consumer. We might be tempted to write such logic in our model (and it is not that bad idea) but because it is of no use to our app, I would prefer to have a method for that in the presenter itself.

class CustomerPresenter
  attr_accessor :customer
  delegate :full_name, :email, to: :customer

  def initialize(customer)
    @customer = customer
  end

  def as_json(*)
    {
      fullName:  full_name,
      avatarUrl: gravatar_url
    }
  end

  private

  def gravatar_url
    "http://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email)}"
  end
end

Presenters might use multiple objects

Do you like Hypermedia API ? I still don’t know but let’s give it a try here just to prove my point . There is a feature that customer can be notified about promotions and other events. It is done by sending request to URL that we have available under customer_notification_url route method in our controller. We would like to send it also to the API clients of our app.

class CustomerPresenter
  attr_accessor :customer, :url_generator

  delegate :full_name, :email,          to: :customer
  delegate :customer_notification_url,  to: :url_generator

  def initialize(customer, url_generator)
    @customer      = customer,
    @url_generator = url_generator
  end

  def as_json(*)
    {
      fullName:        full_name,
      avatarUrl:       gravatar_url,
      notificationUrl: notification_url
    }
  end

  private

  def gravatar_url
    "http://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email)}"
  end

  def notification_url
    customer_notification_url(customer.id)
  end
end

And the controller:

class CustomersController < ApplicationController
  respond_to :json

  def show
    customer  = Customer.find(params[:id])
    presenter = CustomerPresenter.new(customer, self)
    respond_with(presenter)
  end
end

Tidying up the the presenter

You can simply have you presenter talk multiple dialects by including ActiveModel::Serializers :

class CustomerPresenter

  include ActiveModel::Serializers::JSON
  include ActiveModel::Serializers::Xml

  attr_accessor :customer, :url_generator

  delegate :full_name, :email,          to: :customer
  delegate :customer_notification_url,  to: :url_generator

  def initialize(customer, url_generator)
    @customer      = customer,
    @url_generator = url_generator
  end

  def attributes
    @attributes ||= {
      fullName:        full_name,
      avatarUrl:       gravatar_url,
      notificationUrl: notification_url
    }
  end

  def to_xml(options = {})
    options        ||= {}
    options[:root] ||= :customer
    super(options)
  end

  def to_json(options = {})
    options        ||= {}
    options[:root] ||= :customer
    super(options)
  end

  private

  def gravatar_url
    "http://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(email)}"
  end

  def notification_url
    customer_notification_url(customer.id)
  end
end

And embrace it in your controller by responding to multiple mime types:

class CustomersController < ApplicationController
  respond_to :json, :xml

  def show
    customer  = Customer.find(params[:id])
    presenter = CustomerPresenter.new(customer, self)
    respond_with(presenter)
  end
end

A little bit of declarativeness

I am also a big fan of decent_exposure and love how the controllers look when using it:

class CustomersController < ApplicationController
  respond_to :json, :xml

  expose(:customers) { Customer.active } # ActiveRecord scope
  expose(:customer)
  expose(:presenter) { CustomerPresenter.new(customer, self) }

  def create
    if customer.save
      respond_with(presenter, location: nil)
    else
      # ...
    end
  end

  def show
    respond_with(presenter)
  end
end

Multiple presenters

It might happen that different usecases demend different presentation. We might need a different value or additional field. I heard you like inheritance:

class Admin::CustomerPresenter < ::CustomerPresenter
  def attributes
    @admin_attributes ||= super.merge(
      admin_field: some_value
    )
  end

  private

  def some_value
    # ...
  end
end

Or maybe you prefer dynamic mixins ?

module Admin::CustomerPresenter
  def attributes
    @admin_attributes ||= super.merge(
      admin_field: some_value
    )
  end

  private

  def some_value
    # ...
  end
end

presenter = CustomerPresenter.new(...)
presenter.extend(Admin::CustomerPresenter)

Delegation ?

class Admin::CustomerPresenter
  include ActiveModel::Serializers::JSON
  include ActiveModel::Serializers::Xml

  def initialize(base_presenter)
    @base_presenter = base_presenter
  end

  def attributes
    @attributes ||= base_presenter.attributes.merge(
      admin_field: some_value
    )
  end

  def to_xml(options = {})
    options        ||= {}
    options[:root] ||= :"customer-for-admin"
    super(options)
  end

  def to_json(options = {})
    options        ||= {}
    options[:root] ||= :"customer-for-admin"
    super(options)
  end

  private

  def some_value
    # ...
  end
end

base_presenter = CustomerPresenter.new(...)
presenter      = Admin::CustomerPresenter.new(base_presenter)

It doesn’t matter which way you like most. All options are still available to you. You know how they work and what are the implications of using the solution you have chosen. Because they are part of the language that you use daily. Not yet another DSL which must implement its own syntax to let you share some parts of the code and mimic inheritance. Plain, old, simple Ruby.

Limitations

Nothing of what I showed here will help in the case where you actually need to created objects based on XML or JSON that you received. Roar might be really helpful in such situation.

Note

If you dislike my solution, feel free to use roar, rails-api or active model serializers. I think they all have their own advantages.

Conclusion

There many libraries that try to help you deliver a well crafted API representations. But maybe you do not need them and you can achieve your goals using just plain Ruby features ?

If you like this article you might be interested in our product that we would like to publish in the future. It will be full of such analysis. You can sign up below.

You might also like