How to Create an API Wrapper of an External Service in Rails

   Back to list

Introduction

Have you ever had the problem when you wanted to use an external API that didn’t have a ready-made library and you needed to write everything from scratch? You weren’t sure how everything should be split and separated in order to write readable and clear code?

If you answered yes to any of these questions, this article is for you. I’ll show you how to write code that is responsible for connecting with the external API and how to split everything in order to write clear and maintainable code. It doesn’t matter if you use Rails or plain Ruby code, this code is flexible, so you can use it wherever you want.

Let’s add a structure

Your API wrappers should be kept in the app/apis folder. In the Rails application there is a folder for models, views, controllers – so why not add another one, for APIs?

Let’s assume that there is no ready-made gem for Github API and we need to write their API wrapper. Start with creating a github_api folder in the apis folder.

The next step is adding another folder, a version of our API, and in this case, we’ll use Github API v3, so you should create a v3 folder. This is really important; in this case, when there are v4 and v4 APIs, you’ll just create another folder and everything will be separated.

The next step is to create a file called client.rb – in this file you’ll keep the whole logic!
It’ll be the heart of our wrapper!

External gems

In order to build a wrapper, we’ll need an HTTP client gem and a gem which is able to parse and decode JSON files. We’ll use two gems:

  • Faraday – a gem used for HTTP requests, which is quite powerful and really easy to use.
  • Oj – a gem used for JSON parsing, it’s written in C, so it’s really fast, much faster than original JSON gem.

If you use the Ruby/Rails app, add these two following gems into your Gemfile. If you don’t have a Gemfile, simply require these libraries in your client.rb file:

require ‘oj’
require ‘faraday’

API Client

You should start with reading API docs, checking how to make a request, what the endpoint is named if the API requires any API token, and what the required parameters are. Everything else we can find here – in the official docs.

Basically, each API client should look pretty similar and should have a client variable on which you’ll base all endpoints. During the initialization process, you should think about if you need to pass an API token and how your response should be parsed.

Now, when you have learned more about Github API and have read about it in the docs, you can then write some code inside your client class. Let’s start with the client method and request method. Your client method will be responsible for the initialization of the Faraday client, a request method, and will send a request based on a passed HTTP method, endpoint, and parameters. It should also parse a JSON response.

A simple version of the client should look like this:

module GithubAPI
  module V3
    class Client
      API_ENDPOINT = 'https://api.github.com'.freeze

      attr_reader :oauth_token

      def initialize(oauth_token = nil)
        @oauth_token = oauth_token
      end

      def user_repos(username)
        request(
          http_method: :get,
          endpoint: "users/#{username}/repos"
        )
      end

      def user_orgs(username)
        request(
          http_method: :get,
          endpoint: "users/#{username}/orgs"
        )
      end

      private

      def client
        @_client ||= Faraday.new(API_ENDPOINT) do |client|
          client.request :url_encoded
          client.adapter Faraday.default_adapter
          client.headers['Authorization'] = "token #{oauth_token}" if oauth_token.present?
        end
      end

      def request(http_method:, endpoint:, params: {})
        response = client.public_send(http_method, endpoint, params)
        Oj.load(response.body)
      end
    end
  end
end

You should only allow calling HTTP endpoint methods (like user_repos), each of these methods should just call the request private method with proper parameters.

In this case, you can use your API wrapper like this:

github_client = GithubAPI::V3::Client.new('myApiKey')
user_repos = github_client.user_repos('piotrjaworski')
user_orgs = github_client.user_orgs('piotrjaworski')

Error handling

If you’re observant, you probably noticed that we didn’t handle any of the exceptions. What happens when the Github API returns with an error or we reach a limit of 60 requests per hour without an API token? The client will crash!

What you should do is check the HTTP response status. If it’s 200 (ok), it means that an external API (in this case Github) has returned a successful response. In any other case, you should handle client errors.
You must handle exceptions and send an error message.

A Faraday response variable has access to a response’s status variable, which keeps an HTTP status of the response. A good practice would be to create a separate exception for each client error, like for 404 – NotFoundError and return it to the client.

You can write something like this:

module GithubAPI
  module V3
    class Client
      GithubAPIError = Class.new(StandardError)
      BadRequestError = Class.new(GithubAPIError)
      UnauthorizedError = Class.new(GithubAPIError)
      ForbiddenError = Class.new(GithubAPIError)
      ApiRequestsQuotaReachedError = Class.new(GithubAPIError)
      NotFoundError = Class.new(GithubAPIError)
      UnprocessableEntityError = Class.new(GithubAPIError)
      ApiError = Class.new(GithubAPIError)
      
      HTTP_OK_CODE = 200

      HTTP_BAD_REQUEST_CODE = 400
      HTTP_UNAUTHORIZED_CODE = 401
      HTTP_FORBIDDEN_CODE = 403
      HTTP_NOT_FOUND_CODE = 404
      HTTP_UNPROCESSABLE_ENTITY_CODE = 429
    
      ...
      
      private
      
      def request(http_method:, endpoint:, params: {})
        response = client.public_send(http_method, endpoint, params)
        parsed_response = Oj.load(response.body)

        return parsed_response if response_successful?

        raise error_class, "Code: #{response.status}, response: #{response.body}"
      end
      
      def error_class
        case response.status
        when HTTP_BAD_REQUEST_CODE
          BadRequestError
        when HTTP_UNAUTHORIZED_CODE
          UnauthorizedError
        when HTTP_FORBIDDEN_CODE
          return ApiRequestsQuotaReachedError if api_requests_quota_reached?
          ForbiddenError
        when HTTP_NOT_FOUND_CODE
          NotFoundError
        when HTTP_UNPROCESSABLE_ENTITY_CODE
          UnprocessableEntityError
        else
          ApiError
        end
      end
      
      def response_successful?
        response.status == HTTP_OK_CODE
      end

      def api_requests_quota_reached?
        response.body.match?(API_REQUSTS_QUOTA_REACHED_MESSAGE)
      end
      
      ...
    end
  end
end

What you’re doing here is checking if a response’s status is 200, if it’s 200, just return a parsed JSON. If something has gone wrong, throw an exception with a response error message. Everything is clear and readable!

Another thing which we need to handle is the Github API request quota limit.
In this case, Github API will also return a 403 HTTP status code, but you can check the JSON response body and see if you have reached the limit.
Remember, without a token, you can only make 60 requests per hour!

Small cleanups

As you’ve probably noticed, when you create any other API wrapper, you’ll duplicate the HTTP status codes and exceptions. You should definitely move all exception classes and HTTP status code constants to a separate module. Let’s do it!

Create an HttpStatusCodes module under the app/apis folder – it can be reused for all providers!

module HttpStatusCodes
  HTTP_OK_CODE = 200

  HTTP_BAD_REQUEST_CODE = 400
  HTTP_UNAUTHORIZED_CODE = 401
  HTTP_FORBIDDEN_CODE = 403
  HTTP_NOT_FOUND_CODE = 404
  HTTP_UNPROCESSABLE_ENTITY_CODE = 429
end

Also, create an ApiExceptions module under the same folder, for exceptions.

module ApiExceptions
  APIExceptionError = Class.new(APIExceptionError)
  BadRequestError = Class.new(APIExceptionError)
  UnauthorizedError = Class.new(APIExceptionError)
  ForbiddenError = Class.new(APIExceptionError)
  ApiRequestsQuotaReachedError = Class.new(APIExceptionError)
  NotFoundError = Class.new(APIExceptionError)
  UnprocessableEntityError = Class.new(APIExceptionError)
  ApiError = Class.new(APIExceptionError)
end

As you can see, each exception class inherits from the APIExceptionError class. You may be wondering why – if you want to customize any exception messages or similar, you can just modify the parent class, in this case, the APIExceptionError class, without needing to touch any other exception class like NotFoundError or ForbiddenError.

The final implementation of the client class should look like this:

module GithubApi
  module V3
    class Client
      include HttpStatusCodes
      include ApiExceptions

      API_ENDPOINT = 'https://api.github.com'.freeze
      API_REQUSTS_QUOTA_REACHED_MESSAGE = 'API rate limit exceeded'.freeze

      attr_reader :oauth_token

      def initialize(oauth_token = nil)
        @oauth_token = oauth_token
      end

      def user_repos(username)
        request(
          http_method: :get,
          endpoint: "users/#{username}/repos"
        )
      end
      
      def user_orgs(username)
        request(
          http_method: :get,
          endpoint: "users/#{username}/orgs"
        )
      end

      private

      def client
        @_client ||= Faraday.new(API_ENDPOINT) do |client|
          client.request :url_encoded
          client.adapter Faraday.default_adapter
          client.headers['Authorization'] = "token #{oauth_token}" if oauth_token.present?
        end
      end

      def request(http_method:, endpoint:, params: {})
        response = client.public_send(http_method, endpoint, params)
        parsed_response = Oj.load(response.body)

        return parsed_response if response_successful?

        raise error_class, "Code: #{response.status}, response: #{response.body}"
      end

      def error_class
        case response.status
        when HTTP_BAD_REQUEST_CODE
          BadRequestError
        when HTTP_UNAUTHORIZED_CODE
          UnauthorizedError
        when HTTP_FORBIDDEN_CODE
          return ApiRequestsQuotaReachedError if api_requests_quota_reached?
          ForbiddenError
        when HTTP_NOT_FOUND_CODE
          NotFoundError
        when HTTP_UNPROCESSABLE_ENTITY_CODE
          UnprocessableEntityError
        else
          ApiError
        end
      end

      def response_successful?
        response.status == HTTP_OK_CODE
      end

      def api_requests_quota_reached?
        response.body.match?(API_REQUSTS_QUOTA_REACHED_MESSAGE)
      end
    end
  end
end

Summary

As you can see, in a few steps you have been able to write a readable Github API wrapper using Faraday. It’s ready for any extensions and new endpoints, all you need is to create a new method with a valid endpoint name and parameters. It’s pretty simple!

The current implementation of the wrapper can even be moved to a Ruby gem, and in this case, our application will have less code and the API wrapper logic will be separated. Just use the library directly in the code without using an implementation!

Send this to a friend