API

We prefer to write APIs with rails-api over other libraries, such as grape because:

  • rails-api comes with Rails since version 5.0.
  • It’s easier to learn and maintain - the interface is the same as Rails.
  • Good integration with api-documentation tools, such as swagger.
  • Routing is defined in config/routes.rb file, instead of inside each endpoint like other libraries do.
  • API endpoints are tested as regular Rails controllers.
  • Possibility to reuse existing Rails helper for authentication and authorization.

Defining routes

  • Place your API into a separate namespace.
  • Use resources nesting for scoped resources.
example
namespace :api do
  resources :users, only: [:index, :show] do
    resources :posts, only: [:index, :show, :create]
  end
end

Writing endpoints

  • Just like writing regular Rails controllers, API controllers should be RESTful.
  • Have base api controller, from which other controllers will inherit.
  • Define common before_actions, methods, helpers in base api controller.
Example
# app/controllers/api/application_controller.rb
module Api
  class ApplicationController < ActionController::API
    before_action :authenticate_user!
  end
end

# app/controllers/api/users_controller.rb
module Api
  class UsersController < ApplicationController
    def index
      render json: User.all, serializer: UserSerializer
    end

    def show
      # ...
    end
  end
end

Serializing objects

  • Prefer to use ActiveModelSerializers for objects serialization.
  • Place serializes used only for API into api namespace.
Example
module Api
  class UserSerializer < ActiveModel::Serializer
    attributes :first_name, :last_name

    has_many :posts, serializer: PostSerializer
  end
end

Testing

  • Api controllers are tested in the same way as regular controllers.
  • Consider adding helper methods for simpler json comparison.
Example
# spec/support/api_helper.rb
module ApiHelper
  def json_body
    JSON.parse(body)
  end
end

# spec/controllers/api/users_controller_spec.rb
require 'rails_helper'

describe Api::UsersController do
  include ApiHelper

  describe '#index' do
    before { create(:user) }

    it 'successfully renders users' do
      get :index
      expect(response).to be_success
    end

    it 'includes user in body' do
      get :index
      expect(json_body).to eq([{ 'first_name' => 'John', 'last_name' => 'Doe' }])
    end
  end
end

Versioning

  • Do not version APIs, depend on client versions instead.
  • Maintain a backward compatibility policy.
  • Branch code depending on a backward compatibility policy.
  • When adding a new functionality create a relevant method in a backward compatibility policy and use that method when checking which functionality (new or deprecated) should be used.
  • When checking which functionality (new or deprecated) should be used, place the new functionality inside else block.
  • Remove a deprecated functionality once it takes too much effort to maintain it.
  • When removing a deprecated functionality remove relevant if blocks and backward compatibility policy’s methods.
  • Whenever a deprecated functionality is removed, the lowest supported version of the application should be incremented.

Example policy:

class BackwardsCompatibilityPolicy
  attr_reader :version, :platform

  def initialize(version, platform)
    self.version = version
    self.platform = platform
  end

  def lesser_than_version?(acceptable_version)
    (BackwardsCompatibilityPolicy.format_version(version) <=>
      BackwardsCompatibilityPolicy.format_version(acceptable_version)) < 0
  end

  # Method name should indicate the feature that is being deprecated
  def deprecated_feature_name?
    lesser_than_version?('3.1.1')
  end

  def self.format_version(version)
    version.to_s.split('.').map(&:to_i)
  end

  private

  attr_writer :version, :platform
end

Tips

  • Prefer postman over curl for debugging.

Documenting

When the documentation for the API is needed we use Swagger. Swagger allows creating a specification for all of the API resources and operations in a human and machine readable format. Swagger provides three core components: Swagger UI, Swagger Codegen, and Swagger Editor.

Swagger Editor and Specification

Swagger Editor is a tool which allows a developer to create a detailed API specification. Swagger specification describes API endpoints, parameters, responses, authentication schemes, etc. The created specification can be used by other tools (for example this specification is used when creating a visual documentation with Swagger UI or when creating a server stub with Swagger Codegen). When creating Rails API we use swagger-docs which allows defining specification details directly inside controllers using Ruby DSL. However, if the API that is being developed doesn’t need to be exposed Swagger is used only if a project requires doing so. If the API is being developed by the external team we ask them to use Swagger and create a specification file for us.

Swagger UI

Swagger UI is a tool which consumes a swagger specification file and produces an easy to read documentation. This is particularly useful when working with external teams because the created visual documentation allows developers to visualize and interact with the API without having to know any of the implementation logic.

Swagger Codegen

Swagger Codegen is a tool which generates client SDKs and server stubs out of swagger specification file. Having a stubbed server is very useful when building client applications that depend on APIs which are developed by external teams because it allows us to build client applications without any knowledge of the server side implementation. Usually when we need a server stub we generate either a Sinatra server or a Node.js server. When working with external team and the mismatch between the swagger specification and the actual API is noticed we inform the external team about the issue and ask them to update the specification as soon as possible.