DSL for Rails API calls

Vinicius Negrisolo Vinicius Negrisolo Ruby Rails

My goal is to encapsulate a API with service classes using a DSL similar to ActiveRecord. This way we can use methods like where, find_by, etc. In order to achieve that I created a class to abstract a json API using the gem faraday and the result is very cool.

Motivation

The gem faraday is well known and used by Ruby on Rails community. However, for every request it’s necessary to define all parameters again. I would like to use default values and override them if needed.

The DSL offered by the gem faraday it’s a little bit inconsistent. Some request attributes are defined via regular methods and other via setters methods, such as in the following example:

request.url path
request.params = query

Another reason is the API implementation abstraction using a well known DSL by Rails developers, like ActiveRecord.

DSL - Domain Specific Language

My intention was to create a service class where I could use it this way:

user = Github::UserService.find_by(access_token: 'my-access-token')

So I started creating the class Api.

Class: Api

The Api Class represents an instance of a flexible json API.

Its instance reuses the same Faraday connection instance by memoizing.

Also it defines default values for:

  • timeout
  • open_timeout
  • headers['Accept']
  • headers['Content-Type']

Another important point was the API abstraction that now receives keyword arguments. This allows a cleaner and more explicitly interface than the one offered by faraday.

class Api
  def initialize(url:, timeout: 5, open_timeout: 2, mime_type: 'application/json')
    @url          = url
    @timeout      = timeout
    @open_timeout = open_timeout
    @mime_type    = mime_type
  end

  def call(path:, method: :get, query: nil, body: nil, headers: {})
    request = request(path, query: query, body: body, headers: headers)
    response = connection.send(method, &request)
    Response.new(response)
  end

  private

  def request(path, query: nil, body: nil, headers: {})
    lambda do |request|
      request.url path
      request.params = query if query
      request.body = body.to_json if body
      request.options.timeout = @timeout
      request.options.open_timeout = @open_timeout
      request.headers['Accept'] = @mime_type
      request.headers['Content-Type'] = @mime_type
      headers.each { |header, value| request.headers[header] = value }
    end
  end

  def connection
    @connection ||= Faraday.new(url: @url) do |faraday|
      faraday.request :url_encoded
      faraday.adapter Faraday.default_adapter
    end
  end
end

Class: ApiResponse

Every API response is encapsulated by ApiResponse.

Its main goal is to parse the json response.

class ApiResponse
  attr_reader :response

  def initialize(raw_response)
    @response = JSON.parse(raw_response.body)

    if @response.is_a?(Hash) && @response['error']
      raise Api::Error.new(@response)
    end
  end
end

Class: ApiError

Another responsibility of ApiResponse class is to raise an ApiError in case of the API returns any error in its content.

This treatment could be via content or via http code, etc.

class ApiError < StandardError
  attr_reader :response

  def initialize(response)
    @response = response
  end
end

API initializer

In order to instantiate an API I created this initializer:

GITHUB_API = Api.new(url: Rails.configuration.github['api_url'])

I’m using the Rails method config_for for defining the configuration. I wrote about that on: Configuring a Rails App.

The setup is done.

Service Class: Github::UserService

Finally, the Service Class has as its main goal to create a DSL similar to ActiveRecord and then abstract the API complexity.

module Github::UserService
  extend self

  def find_by(access_token:)
    GITHUB_API.call(
      path: 'user',
      headers: { 'Authorization' => "token #{access_token}" }
    )
  end
end

Conclusion

A Api Class allowed us the reuse of well spread patterns into Rails community, such as convention over configuration, ActiveRecord pattern, using Hashes over json Strings, error treatment, etc.

All these brings more agility on application development and maintenance, because new developers understand the project faster.


Read also:

First Ember JS Application JavaScript Ember

Ember JS is a Javascript framework for ambitious Web Applications. This means that you can build great applications, with tons of user interactions in a very efficient way. In this post I’ll tell how to create a simple EmberJS Web Application.

Configuring a Rails Application Ruby Rails

There are a lot to consider when configuring a Rails application, such as variables organization, environments, security credentials, etc. Among so many different ways to do that I’m going to show my preferred way using what Rails has to offer with config_for.