Github

Rails APIs With Mutations & Serializers

It's rare that a Rails app does not have some kind of JSON API, either for internal AJAX requests or to expose its data to a mobile app or 3rd party. When building such APIs I rely on 2 libraries to make the process a whole lot smoother: Mutations to handle the business logic, and ActiveModel Serializers to handle the JSON output.

Mutations

Mutations wrap up the input validation and business logic for each piece of functionality of your app into its own class. These functions traditionally exist in your models and controllers, in a combination of controller actions, model validations, and strong parameter filtering.

In addition to the levels of indirection this causes, it makes things harder to test. Worse still, code re-use is almost impossible and you can't ever access that code in anything other than a controller (like say, a rake task or the console).

A mutation handles all of these functions in one place. It is a plain old Ruby class, so you can test it by sending in a Hash and examining the outputs. And you can call it from anywhere... A controller in the front-end of your web app, a JSON API, or from the command line. Here is an example:

class UserSignup < Mutations::Command

  required do
    string :email
    string :password
    boolean :trial
  end

  optional do
    string :first_name
    string :last_name

    hash :address do
      string :address_1
      string :address_2
      string :city
      string :state
      string :zip_code
      string :country
    end
  end

  def execute
    user = User.create!(inputs)
    UserMailer.welcome_email(user).deliver_later

    if address_present?
      FulfillmentCompany.send_free_tshirt!(address)
    end

    return user
  end
end

As you can see, the bulk of the work happens in the #execute method. However, I think a huge part of the benefit comes from the input filtering. You can guarantee that all your required inputs have been set, and they are automatically typed for you. Inputs can be any Ruby type, including ActiveRecord models. Modules can be included for code re-use.

ActiveModel Serializers

Serializers are Ruby classes which take a given model instance and convert it to a JSON string. Here is an example for a user account:

class UserSerializer < ActiveModel::Serializer
  attributes :id, :username, :email, :first_name, :last_name, :full_name
  belongs_to :account
  has_many :logins

  def full_name
    "#{object.first_name} #{object.last_name}"
  end
end

UserSerializer will automatically be called anytime you try and render a User model as JSON. You can also manually specify a serializer.

In the example above you can see that it supports computed fields and relationships as well. This serializer would output:

{
  "id": 123,
  "username": "myuser",
  "email": "email@domain.com",
  "first_name": "John",
  "last_name": "Smith",
  "full_name": "John Smith",
  "account": {
    "id": "567",
    "company": "Company Name, Inc.",
    "active": true
  },
  "logins": [
    { "logged_in_at": "2015-06-09 12:45:32", "ip": "127.0.0.1" },
    { "logged_in_at": "2015-06-07 10:07:55", "ip": "127.0.0.1" }
  ]
}

Useful Helpers

I always end up with something like this in either my ApplicationController or some kind of base API controller:

class ApplicationController < ActionController::Base
  skip_before_action :verify_authenticity_token


  protected

  def render_mutation(mutation, mutation_params)
    outcome = mutation.run(mutation_params)

    if outcome.success?
      render json: outcome.result
    else
      render json: { errors: outcome.errors.symbolic }, status: 422
    end
  end
end

The render_mutation method is used like this:

class WhateverController < ApplicationController

  def index
    render_mutation(MyMutation, params.merge(user: current_user))
  end
end

This method will run your mutation, output the result as JSON (through your serializer if you have one defined), and if anything goes wrong it will output a developer-friendly error message in the same format every time.

Clients consuming your API can check for errors through the HTTP response code. If it is above or equal to 400, then the errors key will tell you everything you need to know in order to fix the error. For example:

{
  "errors": {
    "email": "required"
  }
}