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.

Leave a Reply

Your email address will not be published. Required fields are marked *