Álvaro Fernández-Alonso Araluce

Álvaro Fernández-Alonso Araluce

Published in ruby, gems, open-source, tutorial, building-a-gem-serie, api, design-patterns · · 7 min read

Building an API Wrapper from Scratch

Third article in the series about building Ruby gems. We already know what a gem is and how it’s structured. Now let’s fill it with code. This is the article where we build something real.


The real need

I had a project that needed to integrate with Calendly. I could have written the HTTP calls directly in the application code. It would have worked. But I saw Chris Oliver working live on a fork of a gem called Vultr and realized the pattern he was using was clean, simple, and eaasy to replicate.

I thought: “I can do the same with Calendly.”

And I did. With net/http, no external dependencies, in just a few classes. The pattern I’m going to show you is Client → Resource → Object. It’s the same one many API wrappers in the Ruby ecosystem use. And it’s simpler than it looks.

The Client → Resource → Object pattern

Before writing a single line of code, let’s understand what each piece does:

  • Client — The entry point. Stores the token, knows how to connect to the API.
  • Resource — Knows how to speak HTTP. Does GET, POST, DELETE and handles responses.
  • Object — Wraps the JSON response into something that feels like Ruby.

This is how it looks from the outside:

client = MyGem::Client.new(token: "...")

# Client → Resource → Object
client.events.list(user: "UUID")
#       ↑        ↑       ↑
#    Resource  method  parameter
#              ↓
#     [#<MyGem::Event>, #<MyGem::Event>, ...]
#                ↑
#             Objects

Three layers. Each with a clear responsibility. Let’s build them.

The Client: your entry point

The Client is the simplest of the three classes. It just needs to store the configuration and know which Resources are available:

module MyGem
  class Client
    BASE_URL = "https://api.example.com"

    attr_reader :token

    def initialize(token:)
      @token = token
    end

    def events
      EventsResource.new(self)
    end

    def users
      UsersResource.new(self)
    end
  end
end

That’s it. The Client doesn’t do HTTP. It doesn’t parse JSON. It just knows who you are (token) and gives you access to the Resources.

Notice something: each Resource receives self — the Client itself. This way, the Resource has access to the token and configuration without having to pass each piece of data separately.

In Calendlyr I went one step further with method_missing to avoid writing a method for each Resource:

def method_missing(method_name, *args, &block)
  resource_name = method_name.to_s.split("_").collect(&:capitalize).join + "Resource"
  if Calendlyr.const_defined?(resource_name)
    Calendlyr.const_get(resource_name).new(self)
  else
    super
  end
end

When you call client.events, it checks if EventsResource exists. If it does, it instantiates it. If not, it delegates to normal behavior. This way, every time I add a new Resource, the Client finds it automatically without touching a single line.

Is this necessary? Not really. Explicit methods work perfectly fine. But in a wrapper with over ten Resources, it saves repetition.

The Resource: the one that speaks HTTP

This is where the HTTP call logic lives. We start with a base class:

require "net/http"
require "json"
require "uri"

module MyGem
  class Resource
    attr_reader :client

    def initialize(client)
      @client = client
    end

    private

    def get_request(url, params: {})
      response = request(url, Net::HTTP::Get, params: params)
      handle_response(response)
    end

    def post_request(url, body:)
      response = request(url, Net::HTTP::Post, body: body)
      handle_response(response)
    end

    def request(url, req_type, body: {}, params: {})
      uri = URI("#{Client::BASE_URL}/#{url}")

      if params.any?
        uri.query = URI.encode_www_form(params)
      end

      http = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl = true

      request = req_type.new(uri)
      request["Content-Type"] = "application/json"
      request["Authorization"] = "Bearer #{client.token}"
      request.body = body.to_json if body.any?

      http.request(request)
    end

    def handle_response(response)
      body = response.body.to_s
      JSON.parse(body.empty? ? "{}" : body)
    end
  end
end

This uses only net/http. No Faraday, no HTTParty, no nothing. Ruby brings everything you need to do HTTP.

The request method builds the request, and handle_response parses the JSON. A concrete Resource looks like this:

module MyGem
  class EventsResource < Resource
    def list(**params)
      response = get_request("scheduled_events", params: params)
      response["collection"].map { |attrs| Event.new(attrs) }
    end

    def retrieve(uuid:)
      response = get_request("scheduled_events/#{uuid}")
      Event.new(response["resource"])
    end
  end
end

Each method maps to an API endpoint. list does a GET to /scheduled_events, retrieve does a GET to /scheduled_events/:uuid. The JSON response gets turned into Objects. Clean.

The Object: JSON that feels like Ruby

This is my favorite part and where I made the most important design decision in the entire gem.

When I started Calendlyr, I needed to convert the API’s JSON response into something that could be used with dot notation in Ruby. The obvious solution was OpenStruct:

require "ostruct"

data = {"name" => "30 Minute Meeting", "status" => "active"}
event = OpenStruct.new(data)
event.name    #=> "30 Minute Meeting"
event.status  #=> "active"

And that’s exactly what I used. It worked for years. The key point is that OpenStruct doesn’t need you to define attributes. If the API adds a new field tomorrow, your Object returns it without you having to change anything.

This is huge. It means your gem doesn’t break when the API evolves.

Over time, Ruby deprecated OpenStruct. So I wrote my own version using method_missing:

module MyGem
  class Object
    def initialize(attributes = {})
      @attributes = attributes.each_with_object({}) do |(key, value), hash|
        hash[key.to_s] = wrap(value)
      end
    end

    def method_missing(name, *args, &block)
      return @attributes[name.to_s] if args.empty? && block.nil? && @attributes.key?(name.to_s)
      super
    end

    def respond_to_missing?(name, include_private = false)
      @attributes.key?(name.to_s) || super
    end

    private

    def wrap(value)
      case value
      when Hash then self.class.new(value)
      when Array then value.map { |item| wrap(item) }
      else value
      end
    end
  end
end

Here’s what it does:

  1. initialize stores the JSON attributes in a hash.
  2. method_missing intercepts method calls and looks them up in the hash. If the key exists, it returns the value. If not, it behaves normally.
  3. respond_to_missing? tells Ruby that yes, the Object does respond to those methods. This is important for introspection.
  4. wrap is the magic touch: if a value is a Hash, it converts it into another Object. If it’s an Array, it maps each element. This way nested objects also get dot notation.
data = {
  "name" => "30 Minute Meeting",
  "location" => { "type" => "zoom", "join_url" => "https://..." }
}

event = MyGem::Object.new(data)
event.name                  #=> "30 Minute Meeting"
event.location.type         #=> "zoom"
event.location.join_url     #=> "https://..."

Nested objects, without defining a single attribute. The API can change its structure tomorrow and your gem keeps working.

Typed objects: the best of both worlds

The base Object is generic. But sometimes you want to add specific behavior to certain resources. For that, you simply inherit:

module MyGem
  class Event < Object
    def cancel(reason: nil)
      client.events.cancel(uuid: uuid, reason: reason)
    end
  end
end

Event inherits all the magic from Object (dot notation, automatic wrapping) and adds its own methods on top. You can do:

event = client.events.retrieve(uuid: "ABC123")
event.name        #=> dot notation for free, comes from the Object base
event.cancel      #=> Event-specific method

In Calendlyr I have typed objects for each API resource — Event, User, EventType, etc. — but the base class does the heavy lifting. The typed ones just add convenience methods.

The decision that saves you years of maintenance

I want this to stick because it’s the most important decision in the entire article:

Don’t define fixed attributes in your Objects.

If the Calendly API adds a new_field to the response tomorrow, Calendlyr returns it automatically. No gem update. No new version. Nothing.

This isn’t theory. Calendlyr has been running in production for 5 years and the Calendly API has changed many times. The gem kept working because the Objects don’t assume which fields exist.

The first version with OpenStruct did this. When Ruby deprecated it, I wrote my own Object that does exactly the same thing. The logic didn’t change. Just the mechanism.

Zero dependencies: a conscious decision

Everything we’ve seen uses net/http, json, and uri. All three ship with Ruby.

Could you use Faraday? Yes. HTTParty? Sure. But every dependency you add is a point of failure. An incompatible version, a breaking change, an abandoned gem.

With net/http you don’t have that problem. It’s standard Ruby. If Ruby works, your gem works.

Is net/http the prettiest API in the world? No. But for an API wrapper it’s more than enough. And the code you’ve seen proves you don’t need much to have something clean and functional.

Putting it all together

At the end of the day, an API wrapper is three classes cooperating:

Client (configuration + access to Resources)
  └── Resource (HTTP + endpoint mapping)
       └── Object (JSON → Ruby with dot notation)

The Client knows who you are. The Resource knows how to talk to the API. The Object knows how to turn JSON into something useful. Each one does one thing and does it well.

In Calendlyr, this foundation allowed me to cover the entire Calendly API v2 with 13 Resources, over 40 endpoints, auto-pagination, error handling, and logging. All built on top of these three classes.


In the next article we’ll talk about testing. How to test a gem that makes HTTP calls without making a single real request. Minitest, WebMock, and fixtures. Easier than it sounds.