Faraday HTTP API Client in Rails

If you enjoy this article you may be interested in the book I am working on called Building and Deploying Crypto Trading Bots. You can find more info about it here.

Faraday is a popular Ruby HTTP client – it's great for a lot of reasons, blah, blah, blah skip the intro BS.

Add a new library directory to app for your API client. This is the preferred way to autoload and organize library based code in Rails 5+. Add faraday (HTTP client) and faraday_middleware (middleware like JSON parsing) gems to Gemfile:

# .. other gems
gem 'faraday'
gem 'faraday_middleware'
# .. other stuff
Gemfile

Create a name spaced module for API client I'm going to use an example client I built for a Deployer microservice:  app/lib/deployer/v1/client.rb

module Deployer
  module V1
    class Client
       def initialize
         @base_url = Rails.application.config.deployer_url
       end
       
       private
       # This sets up a Faraday client 
       def client
        @client ||= Faraday.new(@base_url) do |conf|
          conf.request :url_encoded
          conf.response :json
          conf.request :json
          conf.adapter Faraday.default_adapter
        end
      end
    end
  end
end
app/lib/deployer/v1/client.rb

The initialize method pulls the URL from Rails.application.config at instantiation time. The client is then configured to encode requests using url_encoded scheme converting the body of a Faraday::Request to a hash of key/value pairs into a url-encoded request body. Next the client adds support for response decoding into JSON. Finally, the default_adapter is set to make use of Net::HTTP to make requests.

(optional) You can use Rails' & Rack's default exceptions and HTTP status codes of implement your own. The advantage is you have full control of the errors, the drawback is "it is not DRY". If you have a lot of HTTP clients it can be helpful.

module Http
 module Exceptions
    class GenericError < StandardError; end
    class InternalServiceError < GenericError; end
    class BadRequest < GenericError; end
    class Unauthorized < GenericError; end
    class Forbidden < GenericError; end
    class NotFound < GenericError; end
    class UnprocessableEntity < GenericError; end
  end
 
 module StatusCodes
   OK = 200
   CREATED = 201
   ACCEPTED = 202
   BAD_REQUEST = 400
   UNAUTHORIZED = 401
   FORBIDDEN = 403
   NOT_FOUND = 404
   UNPROCESSABLE_ENTITY = 422
  end
end
app/lib/http.rb

Include these in the HTTP client:

module Deployer
  module V1
    class Client
      include Http::Exceptions
      include Http::StatusCodes
      
      # -- snipped --
    end
  end
end

Set up the request method that will communicate with the Faraday client inside your client:

private

def request(http_method:, endpoint:, params: {}) 
 response = client.send(http_method, endpoint, params) ❶
 parsed_response = response.body 
 return parsed_response.deep_symbolize_keys if response_successful?(response.status) ❷
 
 raise error_klass(response.status).new(parsed_response) ❸
end
app/lib/deployer/v1/client.rb

The request method  is the heart of the client. It takes in the keyword arguments http_method (i.e. 'get', 'post', 'patch'),  endpoint (i.e. /deploy, /terminate, etc) and an optional parameter body and forwards them to the Faraday client using Ruby's clever public_send method ❶.

Upon receiving a response, the method parses the body to JSON if it determines that the request succeeded using the response_successful? helper (to be created). When a failure is detected (e.g. not an OK, CREATED, or ACCEPTED), the method uses another helper error_klass ❸ to retrieve the appropriate error constant and raise an exception.

Let's create the response_successful? and error_klass:

private 

# -- snipped request method --

def error_klass(status)
 case status
  when BAD_REQUEST
   BadRequest
  when UNAUTHORIZED
   Unauthorized
  when FORBIDDEN
   Forbidden
  when NOT_FOUND
   NotFound
  when UNPROCESSABLE_ENTITY
   UnprocessableEntity
  else
   InternalServiceError
  end
end

def response_successful?(status)
 case status
 when OK, CREATED, ACCEPTED
   true
 else
  false
 end
end
app/lib/deployer/v1/client.rb

Finally, once you have all the plumbing set up you add your various client methods mapping to the API endpoints you are hitting.

module Deployer
 module V1
  class Client
   include Http::Exceptions
   include Http:StatusCodes
   
    def deploy(payload)
     request(
       http_method: :post,
       endpoint: "deploy",
       params: payload.to_json,
     )
    end

    def terminate(id)
      request(
        http_method: :post,
        endpoint: "terminate",
        params: { id: id }.to_json,
      )
    end

    def get(id)
      request(
       http_method: :get,
       endpoint: "deployment/#{id}",
      )
    end
    
    def initialize
      @base_url = Rails.application.config.deployer_url
    end
    
    private 
    # .. boilerplate we just set up
  end
 end
end

Finally the client usage looks like this:

client = Deployer::V1::Client.new

begin
 response = client.deploy( {application_name: 'app_name', host: 'mycoolhostingprovider.com'} )
 
 # do something with the response
rescue Http::GenericError => e
 Rails.logger.error("Failed to deploy")
end
main.rb

That's it.


Have fun!