Designing an API Client


Abstractions are hard. They may bleed underlying complexity. They can compound the nastiness they were built to sequester. They might ineffectively guide the developer as she formulates how to model her world in code. The expert engineer often thinks to herself a few keystrokes away from another npm install, “this may be way more trouble than it’s worth.”

And so maybe against better judgment, we tread into the world of abstraction building by creating a set of client libraries for our API.

This post aims to highlight a few design decisions that we think make the technical economics of adding yet another dependency, yet another potential point-of-failure, work out. While we’ve also open-sourced our ruby and python implementations, here we’ll focus on our node client.

Just The Pipes

Button’s bread and butter is connecting apps in meaningful ways. Our goal is to present the useful services of one app inside another. A key part of our ecosystem then is knowing when a user takes advantage of this connection and buys something in a different app than they started. This is generally accomplished by the app offering the service telling us an order occurred via our HTTP Orders API.

If I’m sitting down to report orders back to Button, there’s going to be a whole litany of things I can just not be bothered to do. I’m probably not going to build a thorough class hierarchy for an Order. I’m probably not going to do a whole lot of payload validation, i.e. checking that my dates are ISO8601 compliant. After all, HTTP 400s exist for a reason. I’m probably not going to pull in an awful lot of third-party dependencies.

What I am going to do is bury a bunch of HTTP boilerplate (things like HTTP verbs, Basic Auth, payload unmarshalling, …) up inside a few handy functions such as getOrder, createOrder, and deleteOrder. I am going make sure they expose as clean and congruent an interface to my application code as possible. They’ll return platform-idiomatic types (objects, in the case of node) and match the asynchronous customs of the surrounding code (be it node- style callbacks, promises, or otherwise).

What this ends up looking like is just the pipes, just the ability to conveniently express intent over a wire. Because our partners would all at a bare minimum have to implement these pipes themselves, we see it as a worthy opportunity to provide abstraction.

Those inclined for something more heavyweight are of course invited to build on top.

The Pudding

These ideas are incarnated in a simple interface. To abstract authentication and network calls, create an object bound to an organization’s API key:

// Find your API key at 
// https://app.usebutton.com/settings/organization
//
var client = require('@button/button-client-node')('sk-XXX'); 

Fetching a resource is then as simple as grabbing the orders key and calling get on it:

client.orders.get('btnorder-XXX', (err, response) => { 
  // ... 
}); 

To be compatible with Promises, we have an interface that accepts your own promise implementation for use in all API calls:

// or ES6 Promises, or Q, or then.js, or...
//
var Promise = require('bluebird'); 

var client = require('@button/button-client-node')('sk-XXX', { 
  promise: resolver => new Promise(resolver) 
});

client.orders.update('btnorder-XXX', { 
  total: 60 
}).then((result) => { 
  // ...
}, (reason) => { 
  // ... 
}); 

This keeps us dependency-free and keeps you from having to cast promises to your preferred implementation.

Shoutouts

Defining our clients as simple communicators keeps bytes down and transitive dependencies out. Zero transitive dependencies in turn lets us target a wide range of versions for each supported platform and stay fully in control of and accountable for the code that we ship. It means we can hide the boring boilerplate and annoying gotchas of talking to a server away while also not obligating you into a more abstruse or rigid interface than is absolutely required. So shoutout to our homies pybutton.client, Button::Client, and button-client-node.

We hope you’ll pip install pybutton, npm install @button/button-client-node, and gem install button with confidence.