As cool new RESTful services crop up practically each day, I find myself using or writing a lot of Ruby API wrappers. So much that I've seen some common approaches emerge, each with their own pros and cons.
I had been considering this topic for some time when Zach Inglis' feedback on my readernaut gem spurred me to write about it. Rattling around in the back of my head is Chad Fowler's post from 2007 about Facebooker v. RFacebook and writing APIs to wrap APIs, so I'd like to start there.
Chad makes the case for idiomatic consistency with the language you're supporting. Put simply, this means your wrapper should smell like language in which it is written — Ruby like Ruby, Java like Java, PHP like PHP, and so on. This usually comes into play when language conventions bleed through an API in the form of method and variable names. Take the following example JSON response from the Billboard Charts API:
// http://api.billboard.com/apisvc/chart/v1/item?id=3064446&format=json&api_key=bvk4re5h37dzvx87h7rf5dqz
{"chart":{
"issueDate": "2006-03-04",
"description":"Chart",
"chartItems":{
"firstPosition":1,
"totalReturned":15,
"totalRecords":25663,
"chartItem":[{
"songName":"Lonely Runs Both Ways",
"artistName":"Alison Krauss + Union Station",
"peek":1,
"catalogNo":"610525",
"rank":1,
"exrank":1,
"weeksOn":65,
"albumId":655684,
...
}}
Using the incredibly simple and fun HTTParty gem I can quite easily create a Ruby wrapper for this method that will return a Mash representing the data returned. However, when writing Ruby I prefer to deal with Ruby naming conventions, chart.chart_items
instead of chart.chartItems
. For this reason, when writing the Billboard wrapper gem I implemented this trick:
class Hash
# Converts all of the keys to strings, optionally formatting key name
def rubyify_keys!
keys.each{|k|
v = delete(k)
new_key = k.to_s.underscore
self[new_key] = v
v.rubyify_keys! if v.is_a?(Hash)
v.each{|p| p.rubyify_keys! if p.is_a?(Hash)} if v.is_a?(Array)
}
self
end
end
Idiomatic consistency also means making method calls feel more natural. Take this example from the Yahoo! Upcoming.com events wrapper:
# from user.rb
# Retrieve the details about a user by email
#
# +email+ (Required)
# The email of the user to look within. To run getInfoByEmail on multiple addresses, simply pass a comma-separated list of valid email addresses.
#
def self.info_by_email(email)
email = email.join(',') if email.is_a?(Array)
Mash.new(self.get('/', :query => {:method => 'user.getInfoByEmail', :email => email}.merge(Upcoming.default_options))).rsp.user
end
Two things make this method more Rubyish. First, change the method name to follow language conventions. The API method user.getInfoByEmail
becomes info_by_email
. This goes beyond just taking it from camel-casing to underscores. In this case, our domain model is User
so the user.
in the method name is redundant. I also dropped the get
because most all the methods in the API have this prefix and dropping it reduces noise.
Second, provide convenience when possible. Note line nine below:
# from user.rb
# Retrieve the details about a user by email
#
# +email+ (Required)
# The email of the user to look within. To run getInfoByEmail on multiple addresses, simply pass a comma-separated list of valid email addresses.
#
def self.info_by_email(email)
email = email.join(',') if email.is_a?(Array)
Mash.new(self.get('/', :query => {:method => 'user.getInfoByEmail', :email => email}.merge(Upcoming.default_options))).rsp.user
end
The API method supports returning info for multiple users from a comma delimited list of email addresses. Instead of making the consumer build that list and concatenate a string of delimited email addresses, we can accept either a string or an array and build the list for them if need be.
Another design decision in writing a good API wrapper is between writing a wrapper and an abstraction. Take these examples of updating a your Twitter status from popular Ruby wrappers for the API.
Twitter Auth from @mbleigh:
user.twitter.post('/statuses/update.json', 'status' => 'This is my status.')
Grackle from @hayesdavis:
client.statuses.update.json! :status=>'this status is from grackle'
Twitter from @jnunemaker:
client.update('Heeeeyyyyooo from the Twitter Gem')
Michael and Hayes lean more to the simple wrapper side of the spectrum while John provides a simple abstraction and the caller doesn't know or care about the underlying API call at all. Each approach has its advantages and disadvantages.
In my experience, simple API wrappers provide two advantages.
Service API changes require less changes in the wrapper. In all three examples, if Twitter decided to add additional parameters, the library could handle the change without an update to the wrapper. However, if Twitter decided to rename the API method (which has happened) we would have to update the Twitter gem in the last example.
You can leverage existing API documentation. In the first two examples, it's very clear which REST endpoint is in play, so it's very easy to find the method in the Twitter API docs to see what parameters are supported.
Abstraction, on the other hand, provides some advantages as well.
Simplifying a complex API. Some APIs provide a long, flat surface area. Abstracting and organizing those methods into classes is natural.
Provide a business domain. Abstraction can also provide things like caching, lazy loading of associated data, and simple methods that may make multiple calls to the server.
In either case, when creating a simple wrapper or abstraction library for an API, take advantage of the language in which your implementing.
Let's compare the simple wrappers in the first two examples. Both perform a POST
to the /statuses/update
method:
# twitter-auth
user.twitter.post('/statuses/update.json', 'status' => 'This is my status.')
# grackle
client.statuses.update.json! :status=>'this status is from grackle'
I rather like what Hayes has done with some method_missing
magic in the second example. By using dot notation, I have to worry about less string building.
Similarly, I really dig what John did in his implementation of Twitter search in the Twitter gem:
Twitter::Search.new.from('jnunemaker').to('pengwynn').hashed('ruby').fetch()
That's a very natural way of specifying parameters without having to build a params hash before or during the method call. I liked it so much I ripped it off when creating the Rimxr gem, a wrapper for the BestBuy Remix API:
# calls http://api.remix.bestbuy.com/v1/stores(area(76227,50))+products(salePrice<=3000)?apiKey=OU812
stores = client.stores({:area => ['76227', 50]}).products({:salePrice => {'$gt' => 3000}}).fetch.stores
Using the power of Ruby to support chaining methods lets us build some powerful queries quite simply.
There are many considerations in designing an API wrapper, but it's important to make the library feel as natural as possible in the language in which it's written. With many APIs, there's room for multiple successful libraries. What are your thoughts? What are some of your favorite API wrappers and what makes them special?
Engineering Director at Adobe Creative Cloud, team builder, DFW GraphQL meetup organizer, platform nerd, author, and Jesus follower.