As developers interacting with APIs is a day to day occurrence. Thankfully things have begun to standardize around the RESTful pattern for most APIs. For most APIs a developer can usually find a client library for their language of choice. Here at WebVolta we work with Ruby and with it being a fairly popular language we can usually find libraries for most APIs that we use.
So what do you do when you can’t find one? Well working in Ruby there are many choices out there for us. We can go bare bones and craft our requests using the Net::HTTP libraries that are part of the core Ruby libraries. If that’s a bit too low level for you, there are libraries like HTTParty and Typhoeus. Lastly you can go really high level and use something like RestClient which abstracts a few more things.
What if you are prototyping though and all you really need is a quick and simple solution that abstracts just enough and doesn’t require you to code too much?
I ran across this exact scenario the other day and came up with a simple solution that leverages Ruby’s method_missing
and uses HTTParty
to crafts requests quickly while still being easy to understand.
Here’s my code:
class SimpleClient
include HTTParty
base_uri 'https://api.example.com/v1'
attr_accessor :token
def initialize(token = 'some_default_token')
self.token = token
end
private
def method_missing(endpoint, method = :get, **opts)
path = [endpoint.to_s, opts.delete(:path)].compact.join(?/)
response = self.class.send(method, "/#{path}", **opts, headers: headers)
if response.nil? && response.code >= 300
raise ArgumentError, "Request failed with code #{response.code}"
else
convert_to_openstruct(response.parsed_response || '{}')
end
end
def convert_to_openstruct(response)
if response['data'].present? && response['data'].is_a?(Array)
response['data'] = response['data'].map { |row| RecursiveOpenStruct.new(row) }
end
RecursiveOpenStruct.new(parsed)
end
def headers
{
'Content-Type' => 'application/json',
'Authorization' => "Token #{token}"
}
end
end
The code should be pretty easy to follow but I’ll quickly explain what’s going on.
For this API that I used this on I needed to be able to set a token in the header so I made it where you could define a default or pass one in to the initializer at line 8. What method_missing
is doing is taking the method name and converting that into the resource type for the API (the first argument). The second argument is optional and is the HTTP verb for the request (ie. :get
, :post
, :put
, :patch
, :delete
). And the last argument for method_missing
slurps up the remaining parameters into an options hash.
The options hash are mostly options that HTTParty
understands with the exception of the :path
option which is used to build onto the URI for the request.
The reest of the code looks to make sure we had a successful request and if not it raises an error, otherwise, a RecursiveOpenStruct is built out of the resposnse and returned. For the specific use case I created this for all responses came wrapped in a data
root node, which may or may not be the case for you.
Below you will find some examples of what using this client would look like.
client = SimpleClient.new
client.users(path: 1)
#=> Sends a :get request to https://api.example.com/v1/users/1
client.users(query: { filter: { client_id: 1 } })
#=> Sends a :get request to https://api.example.com/v1/users?filter[client_id]=1
client.products(:post, body: { name: 'Washing Detergent' }.to_json)
#=> Sends a :post request to https://api.example.com/v1/products with a JSON body of '{ "name": "Washing Detergent" }'
client.products(:put, path: 1, body: { name: 'Marbles' }.to_json)
#=> Sends a :put request to https://api.example.com/v1/products/1 with a JSON body of '{ "name": "Marbles" }'
As you can see in a few lines of code we have created a client that is very adaptable and produces code that is very readable and easy to follow.
Feel free to extend the ideas here and contact us with any feedback you may have.