DEV Community

Andrew McIntosh for FreshBooks

Posted on • Updated on

API Client Design Across Languages - Part 1

In my recent post Some Best Practices On Building An Integration, I espoused the benefits of using API owner supplied tools and libraries, and mentioned areas where a well-built SDK hides complexity from, or otherwise makes things easier for, a developer.

A colleague suggested that it might be useful to present examples of some of these areas to give some pointers for someone who needs to implement that functionality themselves, can't make use of an SDK, or simply for someone looking to build their own API client. So, this is part 1 of a deep dive into functionality in FreshBooks' (and some other API owner's) SDKs.

Basic Structure

This first post won't go too much into functionality as I think it's best to start on structure.

A RESTful API is language agnostic and clients built any number of languages must all support the same API features and resources. However, the actual design of the client and usage of the client itself can, and probably should, be different language to language. For example, a Ruby client versus a Java client will still call the same API endpoint, but the form of the methods to make that call, and the form of the returned data could look very different.

I feel it's best to build an API client in a way that is natural to the specific language it's written in. This extends from the project layout, to the client initialization, the method calls themselves, and the returned data. This makes things more intuitive and easy for a developer to use.

The language influences the design primarily in two ways: language capabilities, and common language conventions.

Capabilities

By capabilities, I'm talking about language design and features. A statically typed language usually needs a bit more structure than a dynamically typed one. For instance, an API client in a language like PHP or Python could return JSON results as associative arrays (array and dictionary respectively), as you don't have to declare the various return value's types are. It would be difficult to do the same in Java with a HashMap (possible, but it would not be clean), so you're much more likely to build data objects for the responses with all the fields included and nicely typed.

Other features play in as well. How does the language handle functions with different options? Function overloadings? Optional arguments? These all affect the design.

Conventions

Beyond what you can do with a language, there's also what you should do. You can write your Python or Ruby in a very Java-like way, but it might not feel as natural to a Ruby developer using your library. Of course conventions aren't so cut-and-dry as capabilities; there are many ways to do something and sometimes one is considered "more right" than others, but often not as well. Looking at how other libraries are implemented and getting to know a language helps informs a lot of design choices. The best advice is to try to make things clear.

FreshBook's SDKs

At the time of writing, FreshBooks has first-party Python and Node.js SDKs, and a community-supported Java one (all three are listed here). As I said, I'm going to walk through some of the differences in the design, but today I'll get started with the basics of client initialization and configuration.

First, let's talk about the configuration the FreshBooks' SDKs need to support:

  • We require the clients to be initialized with their application's unique client id for the user-agent string, so that's a required parameter.
  • To use the API requires authentication. Depending on what a developer has implemented, they'll either have a valid OAuth2 access token to initialize the client with, or they'll want to go through the authorization flow, which would require their client secret and redirect urls. Ideally the SDK supports both.
  • If they have an expired token, they may want to refresh it, which would require the refresh token to be supplied.
  • The developer may want to override some of the default settings like user-agent string, timeouts, or disabling automatic retries on failures.

Java

I'll start with the Java SDK because the features of the Java language makes it a good first example to set the others against.

Java supports function overloading, but with the number of possible options mentioned above, that would get very compicated combination-wise. You could just use nullable parameters, but that would be confusing and ugly. For example:

public FreshBooksClient(
    String clientId, String clientSecret, String redirectUri,
    String accessToken, String userAgent, Integer timeout
) {
    ...
Enter fullscreen mode Exit fullscreen mode

which could like anything like:

client = new FreshBooksClient(
    <client_id>, <client_secret>, <url>, null, null, null);
client = new FreshBooksClient(
    <client_id>, null, null, <access_token>, null);
client = new FreshBooksClient(
    <client_id>, null, null, <access_token>, null, 30);
Enter fullscreen mode Exit fullscreen mode

This is what the builder pattern is for. You can see the full code for
the client and the builder on github but essentially the client is not initialized directly. You initialize a "client builder", which has a constructor for each of the base cases ("client_id" versus "client_id, secret, url") and different methods for the various options, and the builder returns a client.

private FreshBooksClient(FreshBooksClientBuilder builder) {
    ...
}

public FreshBooksClientBuilder(
    String clientId, 
    String clientSecret, 
    String redirectUri
) {
    ...
}

public FreshBooksClientBuilder(String clientId) {
    ...
}

public FreshBooksClientBuilder withAccessToken(
    String accessToken
) {
    ...
}

public FreshBooksClientBuilder withReadTimeout(
    int timeout
) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

Which allows you to instantiate the client in the various differing ways cleanly:

client = new FreshBooksClient.FreshBooksClientBuilder(
        <client_id>, <client_secret>, <url>)
    .build();
client = new FreshBooksClient.FreshBooksClientBuilder(
        <client_id>)
    .withAccessToken(<valid token>)
    .build();
client = new FreshBooksClient.FreshBooksClientBuilder(
        <client_id>)
    .withAccessToken(<valid token>)
    .withReadTimeout(30)
    .build();
Enter fullscreen mode Exit fullscreen mode

This requires much more structure in the client, but allows much cleaner usage.

Python

By comparison, Python allows for a much more concise implementation. Python is an object-oriented language and you could implement a builder pattern, but as python also supports named parameters, and there actually aren't too many options for the client, we can get away with something much simpler and more in the pythonic style (again, full code on github).

def __init__(
    self, 
    client_id: str, 
    client_secret: Optional[str] = None, 
    redirect_uri: Optional[str] = None,
    access_token: Optional[str] = None, 
    refresh_token: Optional[str] = None,
    user_agent: Optional[str] = None, 
    timeout: Optional[int] = DEFAULT_TIMEOUT,
    auto_retry: bool = True
):
Enter fullscreen mode Exit fullscreen mode

which allows for:

client = Client(
    <client_id>, 
    client_secret=<client_secret>, 
    redirect_uri=<url>
)
client = Client(
    <client_id>, 
    access_token=<valid token>
)
client = Client(
    <client_id>, 
    access_token=<valid token>, 
    timeout=30
)
Enter fullscreen mode Exit fullscreen mode

As you can see, the language features of Python can lead to a very different implementation and usage than Java.

Node.js

FreshBooks' Node.js SDK is written in TypeScript. Again, there are different ways to go about implementation, but we took a fairly common javascript pattern and passed a configuration object as a parameter. The Stripe Node.js Library does something similar (in general Stripe is a great place to look for any "how have others"-type API questions.)

export interface Options {
    clientSecret?: string
    redirectUri?: string
    accessToken?: string
    refreshToken?: string
    apiUrl?: string
    retryOptions?: IAxiosRetryConfig
    userAgent?: string
}

constructor(clientId: string, options: Options = {}) {
    const defaultRetry = {
        retries: 10,
        retryDelay: axiosRetry.exponentialDelay,
        retryCondition: APIClient.isNetworkRateLimitOrIdempotentRequestError,
    }
    const {
        clientSecret,
        redirectUri,
        accessToken,
        refreshToken,
        apiUrl = process.env.FRESHBOOKS_API_URL || API_BASE_URL,
        retryOptions = defaultRetry,
    } = options
Enter fullscreen mode Exit fullscreen mode

with initialization looking like:

client = new Client(<client_id>, {
    clientSecret: <client_secret>
    redirectUri:  <url>
})

client = new Client(<client_id>, {
    accessToken: <valid token>,
})
Enter fullscreen mode Exit fullscreen mode

This also happens to be a fairly common pattern in PHP, thus a possible future FreshBooks PHP SDK would likely look similar. auth0's PHP SDK has an example of this.

Up Next

I hope you found it interesting seeing the different ways a client for the same API can look language-to-language. As I said, I'll dive a bit more into functionality differences next time, but feel free to dig around the projects and if you have any questions, please reach out.

Top comments (0)