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:
initializestores the JSON attributes in a hash.method_missingintercepts method calls and looks them up in the hash. If the key exists, it returns the value. If not, it behaves normally.respond_to_missing?tells Ruby that yes, the Object does respond to those methods. This is important for introspection.wrapis 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.