Kyle Conroy

Writing API Clients in Go

The humble API client. A deceptively simple piece of software. Make a request and parse a response. Repeat.

With the rise of public web services, API clients have become the main integration point. However, many of the Go clients I encounter are obtuse. Here's my guide for writing boring API clients that age well.

We'll use the Dropbox Paper API as our example. First, start with a client struct.

type client struct {
    // unexported fields
}

Each API method should be represented as an exported method on the client. We'll use the document download method as our example.

func (c *client) DownloadDoc() {}

The method should have two arguments: a context and a method-specific argument struct. It may feel awkward passing around structs instead of positional arguments, but structs allow you to make additions to methods without making breaking changes to the methods signatures.

type DocExport struct {
    DocID  string
    Format ExportFormat
}

func (c *client) DownloadDoc(ctx context.Context,
    in *DocExport) {
    // implemetation
}

Take special care in avoiding stringly-typed APIs. If a struct field is limited to a set of values, create a new type that encapsulates those values. In this case, the Format field can only have two values: markdown and html.

type ExportFormat string
const (
    ExportFormatMarkdown ExportFormat = "markdown"
    ExportFormatHTML                  = "html"
)

As an added bonus, these types work natively with the encoding/json package.

The method should return two results: a method-specific return value struct and an error.

type DocExportResult struct {
    Owner    string
    Title    string
    Revision int64
    MIME     string
    Content  []byte
}

func (c *client) DownloadDoc(ctx context.Context,
    in *DocExport) (*DocExportResult, error) {
    // implemetation
}

You may have noticed that our client struct isn't exported. Instead of exporting your concrete type, export an interface. Using an interface makes it easier for downstream consumers to mock out calls to your package.

type Client interface {
    DownloadDoc(context.Context, *DocExport) (
        *DocExportResult, error)
}

For increased flexibility, allow users to pass in their own http.Client, as many use cases require a customized HTTP transport.

func NewClient(token string, hclient *http.Client) Client {
    // implementation
}

This isn't the only way to design API clients in Go. Have another style you like more? I'd love to know about it.