Writing good API wrappers
31 May 2015Both in my day job and in my year of commits, I spend a lot of time thinking about APIs.
For the uninitiated, an Application Program Interface (API) is an avenue for one piece of software to speak to another. This could be a remote, web-based, HTTP API. Or, the API might be an internal interface for one portion of a software system to talk to another. Designing an API that is simple and maintainable is crucial if it is intended to be used and quickly adopted by colleagues or 3rd party developers.
Being one of those 3rd party developers, I find myself using and writing wrappers to these APIs. Here, I will explain what makes a good API wrapper. Let’s postulate about some example wrapper designs.
The Barely Abstractor
The mantra of this design is: “I will take away that annoying HTTP element and you do literally all the rest.”
We can assume that a library called api_requester
exists to wrap our very important 3rd party remote API (Which we will also assume is a RESTful API).
If we wanted to retrieve an object our code might look something like:
require 'api_requester'
params = { object_id: 1 }
headers = { 'Content-Type' => 'application/json' }
object_wanted = APIWrapper.get(
'relative/path/to/object/',
params,
headers)
# => { big: :hash, of: :attributes}
To post an object via this wrapper:
require 'api_requester'
params = {
object_id: 1,
object_name: :foo,
object_type: :bar,
object_description: 'I am an object'
}
headers = { 'Content-Type' => 'application/json' }
APIWrapper.post(
'relative/path/to/object/',
params,
headers)
# => { big: :hash, of: :attributes}
So, you get the idea. That idea is verbosity. However, this approach is not all negative.
Pros:
- Resilient to API changes
A big problem with wrapping APIs outside of your own control is endpoint churn, change, and deprecation. With such a verbose wrapper, the consumer is in complete control of the request. From URL changes to parameter addition and deletion, a consumer of `api_requester` is able to adapt without updating their library (just their own source code).
- Transparent
Since `api_requester` does attempt to abstract out the intricacies of our 3rd party API, the consumer of `api_requester` knows exactly how the underlying API works. Understanding the nuances of the API being "wrapped" might influence the consumer's system architecture positively.
Cons:
- No abstraction
Without abstraction, a consumer of `api_requester` must be one with the 3rd party API's documentation. To ensure competent use, the consumer must become familiar with all possible endpoints and usage patterns. This nearly completely defeats the purpose of providing a wrapper.
- Does not minimize 3rd party surface area
One of the main purposes of wrapping a 3rd party API is to minimize its overall surface area. Making small, distinct interaction points is important for the consumer of `api_requester`. Fewer moving pieces means fewer points of failure. It is probably not necessary for every single end point and function to be exposed by `api_requester`.
The Over-abstraction contraption
Unlike our api_requester
, the mindset behind this pattern is: “Make sure no one can actually understand what is going on behind the scenes”.
Wrapping the same RESTful 3rd party API, api_contraption
, will be our next library. Its code might be used in the following way:
require 'api_contraption'
object_wanted = APIWrapper.fetch_an_object
# => <Object @variable=:thing, @other_variable=:other_thing>
To post an object via this wrapper:
require 'api_contraption'
object_wanted = APIWrapper.fetch_an_object
object_wanted.update_object_attribute!
# => <Object @variable=:updated, @other_variable=:also_magically_updated>
As demonstrated, this library is nearly 100% magic. APIWrapper
exposes arbitrary methods like fetch_an_object
and returns a magical object with instance variables set.
Pros:
- Actual abstraction
Consumers of `api_contraption` do not need to understand the underlying API's full functionality. The wrapper has provided (hopefully) a small handful of useful methods and classes to expose the heart of the API it wraps.
Cons:
- Inflexible
Whenever the underlying API needs to change a response or request contract, the `api_contraption` has to change. This can be a very tiring exercise for its consumers. However, if the API being wrapped is very stable, this inflexibility might not be noticed as greatly.
- Makes the underlying API a black box
While the `api_requester` above did not do enough abstraction, this `api_contraption` does too much. It prevents developers from discovering useful features of the 3rd party API that might be helpful to them. Granted, a consumer could refer to the service's own documentation for feature discovery but who says that they should be forced to?
So which is better?
The answer is neither. A desirable solution exists somewhere between these two examples. An API wrapper should be terse yet flexible, simple yet sophisticated. Making a consumer of your wrapper upgrade with every API change is not scalable and will drive people away from it. At the same time, if a consumer cannot see the value your library gives them, why would they bother to use it?
A good abstraction, a positive value add piece of software can be defined by a few key features:
- Flexibility
- Usefulness
- Readability
Each of our example libraries had one or two of these features but not all three. To make up for what was lacking let’s try and make a hybrid of the two: an api_wrapper
.
The middle ground
Requesting an Object
require 'api_wrapper'
object_wanted = APIWrapper::DesiredObject.find(123)
#=> <DesiredObject @id=123, @name='The One', @author='The Architect'>
Updating an object
require 'api_wrapper'
object_wanted = APIWrapper::DesiredObject.find(123)
object_wanted.update!(author: 'Mr. Smith')
#=> <DesiredObject @id=123, @name='The One', @author='Mr. Smith'>
At first glance it might not seem like all too much is different between this solution and the previous two. However, a few key differences are present.
-
We no longer have a generic superclass
APIWrapper
to interact with, it has become a namespace. With this namespace, clients can freely inherit their own objects from ourDesiredObject
class and make modifications as they see fit. -
Abstraction is still very much in play. URL structures, HTTP payloads and other small minutia about the request is abstracted away from our consumers and they are given very logical methods
find
andupdate
. These methods enable our consumer to interact with their resources in a familiar and pleasant way.
I am not saying this solution is perfect, but it is objectively better than the other two. A lot of factors go into making a great API wrapper. Adhering to the three ideal attributes: Flexibility, Usefulness and Readability will at least point your projects in the right direction.