API on Rails

Who am I?

What is API and why do we need it?

Goals

  • Flexible
  • Consistent
  • Fast

API types

REST API

GET        /v1/users.json          users#index
GET        /v1/users/:id.json      users#show
POST       /v1/users.json          users#create
PATCH|PUT  /v1/users/:id.json      users#update
DELETE     /v1/users/:id.json      users#destroy

Authentication

Signing requests

Gem 'api-auth'

canonical_string = [
  http_method, content_type, content_MD5, request_URI, timestamp
].join('.')

Canonical string is then used to create the signature which is a Base64 encoded SHA1 HMAC, using the client's private secret key.

This signature is then added as the Authorization HTTP header in the form:

Authorization = APIAuth #{client_access_id}:#{canonical_string}

API action(grape)

# app/api/v1/users_api.rb
module V1
  class UsersApi < Grape::API
    namespace :users do
      desc 'Returns a list of users for company.', {
        entity: Entities::User,
        is_array: true
      }
      params do
        requires :company_id, type: Integer
      end
      get do
        company = Company.find(params[:company_id])
        scope = company.users
        scope = scope.includes(:projects) if params[:include_projects]
        last_updated_at = User.last_updated_at(scope)

        cache("users", last_updated_at, { parent: "company-#{params[:company_id]}" }) do
          present(
            scope, with: Entities::User, include_projects: params[:include_projects]
          ).as_json
        end
      end
    end
  end
end

API action(rails-api)

# app/controllers/api/v1/users_controller.rb

module Api::V1
  class UsersController < ApiController

    # GET /v1/users
    def index
      scope = company.users
      scope = scope.includes(:projects) if params[:include_projects]

      render json:  if params[:inclide_projects]
                      scope.as_json(include: :projects)
                    else
                      scope
                    end
    end


    private def company
      @company ||= Company.find(params[:company_id])
    end
  end
end

Serializers (grape)

# app/api/v1/entities/user.rb
module V1
  module Entities
    class User < Grape::Entity
      root 'result', 'result'

      expose :id, documentation: { type: "Integer"}
      expose :first_name, documentation: { type: "String", desc: "First name" }
      expose :last_name, documentation: { type: "String", desc: "Last name" }
      
      expose :projects, if: { include_projects: :true }, using: V1::Entities::Project
      expose :avatar

      private def avatar
        self.object.avatar.file.scale.url
      end
    end
  end
end

Serializers (jbuilder)

# app/views/api/v1/users/index.jbuilder

json.users do
  json.id user.id
  json.first_name user.first_name
  json.last_name user.last_name

  json.array! user.projects, partial: 'projects/project', as: :project
end

Versions

Postman

RSpec

require 'rails_helper'

describe "users api" do
  let(:basic_headers){{"Accept-Version" => 'v1'}}

  let(:response_body) { JSON.parse response.body }

  context "GET #index" do
    let(:company) { create(:company) }
    let!(:correct_user) { create(:user, company: company) }
    let!(:incorrect_user) { create(:user) }

    it "returns users for given company" do
      get '/api/users', params: { company_id: company.id }, headers: basic_headers

      expect(response.status).to eq(200)
      expect(response_body["result"].length).to eq 1
      expect(response_body["result"][0]["id"]).to eq correct_user.id
    end
  end
end
Made with Slides.com