Go-inspired HTTP clients in JavaScript
APIs, particulary HTTP APIs, are the liveblood of most software applications. Building APIs is a pretty decent enterprise, as well as using them. In this post I’ll talk about a pattern I’ve established for building integrations into external APIs. Before I proceed, I should say that sometimes the external APIs developers are generous to develop SDKs to go with it. There’s a lot of languages, and maintaining SDKs in all of them might be tedious. I use JavaScript (via TypeScript), primarily, and there’s almost always an SDK for the APIs I want to use. So why reinvent the wheel? For a couple of reasons:
- It keeps dependencies to a minimum. SDKs are built for the entirety of the API. In my experience, we often use less than 20% of an API’s offering, and in a very specific way.
- If coding style (enforced either by team or programming language) means anything to you (and your team), cranking out your own integration might be your best bet at keeping a consistent style.
- Typing: TypeScript adoption is still in its infancy. The APIs SDK might not ship with type declarations.
I take a lot of inspiration from the way Go’s HTTP tools are built. Specifically, the idea that the client is configured and ready to take a request, perform it, and return a result. I deviate slightly: the client is configured for a specific API, takes a request, performs it, and set the response on the original request object. Let me explain the different parts and the choice I made.
The client
At the core of it, the client configures a generic
HTTP client to be very specific to the API at
hand: a base URL, authentication/authorization
scheme, and content type. Client has a method
called do
, which performs the request. Before
firing off the request though, it ensures that
necessary authentication/authorization is
acquired. In the example client above,
authentication is a different call, with the
result permanently added to the default headers.
Some times it’s a query parameter. At other times
it’s a certain property that should be set on all
payloads that are sent to the API. In short, the
client should know how to take care of
authentication/authorization so that individual
requests are not burdened.
Go also calls its HTTP clients executor method
Do
. As you can tell, the inspiration isn’t
restricted to design philosophy.
Requests
Requests are classes that implement an interface
defined by the client, for the benefit of its do
method. A request should have a body
property,
which can be used as the request’s body or query
parameters. A method
property for the HTTP method
to use, and to
property, which specifies the
endpoint path for the request. If a request has all
three properties then the client can successfully
attempt an API call.
The chance to keep a consistent coding style, or rather, variable/attribute naming convention happens here. In most JavaScript projects I’ve worked on, variables names are spelled using camel-case. Unfortunately, this convention is thrown out of the door when dealing with external input coming into our system. While the JSON data exchange format has its roots in JavaScript, the snake-case (and sometimes dash-case, or whatever it’s called) has become the de facto way to spell attribute names. It leads to code like this:
createCharge({
user_name: 'User Name',
auth_token: 'authToken',
amount_usd: 100
})
.then(data => {
if (data.charge_status === 'created') {
console.log(`${data.charge_id} for ${data.amount_usd} has been created.`)
}
})
.catch(console.error)
This problem isn’t solved by SDKs either. For example, Stripe’s Node SDK sticks to the snake-case spelling style. In the case of Stripe though, I’m willing to budge. It’s a complicated API.
The design of the request class is such that the
params can be named and spelled differently than
what is eventually assembled into the body. This
gives us an opportunity to get rid of all
snake-case spellings. In the example above, I use
lodash’s snakecase
module to convert attribute
names. When the JSON response is received, before
setting it on the request, the keys are
transformed (at all levels) into camel-case using,
once again, lodash’s camelcase
module, and a
weak or strong type enforced. It depends on what
you need the type for. The example above becomes:
createCharge({
userName: 'User Name',
authToken: 'authToken',
amountUsd: 100
})
.then(data => {
if (data.chargeStatus === 'created') {
console.log(`${data.chargeId} for ${data.amountUsd} has been created.`)
}
})
.catch(console.error)
Very consistent with our camel case convention.
Metrics
As time goes on, knowing how things fail, how long
they take to complete or fail, will become a
business requirement. We had a similar request for
the external APIs we use. In our case it was easy
to add instrumentation. All the code went in the
client’s do
method. Since actual requests were
instances of classes, instance.constructor.name
gave us a good name we could use in our metrics.
There’s quite a few good opportunities that open
up with this design philosophy.
The Bad
That said, there are quite a few pitalls to be aware of. The first and most important is that, it might be a laborious task. And depending on commitment (both short and long term to both the API and the code), might not be worth it. Sometimes the work required cannot be automated away. Some APIs have crazy naming conventions. In my experience, a few of them have mixed snake-case, camel-case, and whateverthisstyleiscalled. Dealing with them might not be an exciting enterprise.
P.S. Both specimens here are first and public examples of when we started to develop SDKs using the pattern described here. There has been a few improvements since they were first made. For example, where it’s up to me, I no longer use axios and instead opt for request.
Thank you.