Thin API Clients

I’ve sometimes seen open source libraries used as an API client for a SaaS service when a better choice would be a “thin” client written by the application developers.

Here’s an example of a “thin” API client:

require "bundler/inline"

gemfile do
  source "https://rubygems.org"
  gem "http"
end

HTTP.post(ENV["SLACK_WEBHOOK"], json: {
  text: "Hello, world!"
})

This triggers a Slack incoming webhook from Ruby.

I would rather write and maintain this code with a generic HTTP dependency than depend on a Slack-specific third-party library.

Third-party libraries have costs. They need to be managed by a package manager. They may need to be upgraded to patch security issues or resolve competing requirements in the dependency graph. They can become unmaintained. They are an additional interface for the team to learn.

In the Ruby example, I used a third-party HTTP library because I prefer its interface to the Ruby standard library’s interface. A “thinner” client would use only the language’s standard library, such as this Go version of the same program:

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
)

func main() {
	url := os.Getenv("SLACK_WEBHOOK")
	if url == "" {
		log.Fatalln("no webhook provided")
	}

	reqBody, err := json.Marshal(map[string]string{
		"text": "Hello, world!",
	})
	if err != nil {
		log.Fatalln(err)
	}

	resp, err := http.Post(url, "application/json", bytes.NewBuffer(reqBody))
	if err != nil {
		log.Fatalln(err)
	}

	respBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatalln(err)
	}

	fmt.Println(string(respBody))
}

If my only needs are a POST request with JSON body, I would write a thin client. If there is some lightweight authentication with a header token, an idempotency key, or retry logic, I would still choose to write a thin client.

I would use a third-party library if the authentication mechanism is more complex or we need to use a large surface area of the API, stitching together many endpoints.

Another “thin” variation is to choose the lightest option of open source libraries. For example, to add a new credit card to a React Native app, I’ve used the stripe-client Node package instead of alternatives that have iOS and Android dependencies. In this case, we only need to get a token from Stripe’s API and send the token to our backend for processing.

// https://dashboard.stripe.com/account/apikeys
// It is fine to publish the Stripe Publishable key,
// as it has no dangerous permissions.
const PUBLISHABLE_API_KEY = "pk_test_1234567890abcdef";

const client = require("stripe-client")(PUBLISHABLE_API_KEY);

interface StripeCard {
  address_zip: string;
  cvc?: string; // usually required
  exp_month: string; // two-digit number
  exp_year: string; // two- or four-digit number
  number: string;
}

const createCardToken = async (card: StripeCard): Promise<string | null> => {
  // https://stripe.com/docs/api/tokens/create_card
  const response = await client.createToken({ card });

  if (response && response.id) {
    return response.id;
  } else {
    return null;
  }
};

export const stripe = {
  createCardToken,
};