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.