By Patrick Kunka

JavaScript APIs come in many shapes and forms. These are the interfaces we use when we interact with our widgets, libraries and frameworks.

As a huge part of the day-to-day developer experience, the APIs of our tools can either expedite our work, or cause friction and frustration.

We might not give much thought to them, or even think of them as APIs in the traditional sense, but when we choose a tool or inherit one in an existing project, we enter an unwritten contract with that software based on its exported interface, which we must then learn to work with. In the current front-end landscape where we’re regularly faced with hundreds of options to choose from for any given piece of functionality, a good API can make a world of difference in terms of how quickly we can pick up, use and understand that tool. A bad API can mean a quick visit to the command line for an npm uninstall.

Some of the most popular and well-known JavaScript tools are celebrated for having user-friendly APIs that are fun and effortless to use. You might have particular tools in mind, but the ones that come to my mind all have certain traits and qualities in common:

Flexible & Robust

Good APIs consider the many ways that developers might want to interact with them, and allow a variety of inputs, without breaking.

Helpful

Good APIs anticipate mistakes and handle exceptions with meaningful error messages.

Consistent and Well-named

Good APIs establish patterns and implement them consistently across entry points.

Configurable

Good APIs allow for high reuse through configuration.

Established Standards

Good APIs keep the barrier to entry low by following best practices and established patterns, and don’t try to be clever or expect users to learn proprietary or unusual syntaxes.

While you might have no intention of ever authoring a JavaScript widget, library or framework, these qualities can improve any piece of code, particularly if you intend for other members of your team to make use of it at some point, or even if you’re just writing a basic helper function that you might need to come back to in 6-months for the next project.

So how we can apply some of these qualities to our code?

For the following examples, I’ll use a datepicker as a generic example of a JavaScript widget with an API. We'll instantiate our datepicker by passing a reference to an input element in the DOM. When the user focuses on the input, the datepicker UI will open. When a date is selected from the calendar, the UI will close and the input will be populated with a value.

This falls into the category of what we might call “DOM libraries” - small pieces of dynamic UI with which we decorate our websites and applications to enhance user experience.

const input = document.querySelector('input[name="date"]');

const picker = new Datepicker(input);

Constructors and Factories

As we'll need discrete self-contained instances – each holding its own state – an object-oriented approach using class instantiation (also known as “constructor” functions in previous JavaScript versions) is the typical way to implement such functionality.

However we rarely see this “naked” instantiation syntax in the APIs of our most popular tools.

jQuery

$('.my-element').datepicker();

React components

const picker = React.createComponent(Datepicker);

Further abstracted with JSX

<Datepicker/>

Similarly, outside of the context of UI:

Handlebars

const hbs = Handlebars.create();

Express

const app = express();

All of the above serve the same purpose – instantiation – but avoid the direct use of the new keyword. These are all examples of "factory functions". Abstracting away the “newing up” of classes is not only more succinct and less confusing to JavaScript newcomers, but also provides numerous other benefits.

  • Caching
  • Dynamic Return Types
  • Asynchronicity

By abstracting instantiation behind a factory function, our primary “public” API entry point now becomes simply:

const picker = datepicker(el);

There appears to be something of an ongoing religious war between proponents of factory functions and proponents of classes in JavaScript, but in my humble opinion there’s no reason why we can’t benefit from both simultaneously. Like it or not, classes are now officially part of the JavaScript language (and arguably always have been) and provide a neatly organized way of encapsulating related, stateful functionality. They’re also a widely understood paradigm across many object-oriented programming languages.

We can take advantage of the benefits of classes in our internal code, while exporting a more simple and robust factory function to the outside world. In its most simple form, this pattern looks like this:

Abstracting class instantiation behind a factory function

import Datepicker from './Datepicker';

export default function datepicker(input) {
    return new Datepicker(input);
}

Let’s look at some of the other benefits to using factory functions as API entry points.

Caching

What if the API consumer has already attached a datepicker to the provided input element?

const cache = [];

export default function datepicker(input) {
    let instance;
    
    for (let i = 0; (instance = cache[i]); i++) {
        if (instance.input === input) return instance;
    }

    instance = new Datepicker(input);

    cache.push(instance);

    return instance;
}

By holding all existing datepicker instances in a cache array, we can check if a datepicker has already been instantiated on a given element before creating a new instance. If match is found in the cache, we return it, otherwise we create a new instance and add it to the cache before returning it. When we destroy an instance, we also remove it from the cache.

Dynamic Return Types

What if the consumer passes an array-like element collection to the factory function instead of a single element? In this case, we might want to return an array of datepicker instances rather than a single instance.

import Datepicker from './Datepicker';

export default function datepicker(inputs) {
    if (Array.isArray(inputs) || typeof inputs.length === 'number') {
        // Return many
        
        return Array.from(inputs).map(input => new Datepicker(input));
    }
    
    // Return one
    
    return new Datepicker(inputs);
}

If we forced consumers to interface directly with a constructor function, they would be limited to obtaining only a single instance of that class and nothing else.

It should be noted that mixing return types creates unpredictability and is therefore something to avoid in our internal code, but when writing public APIs, the flexibility afforded by a dynamic language like JavaScript can often be a good thing.

However, if we did want to make our public API more predictable, we could expose an additional static factory method specifically for dealing with collections:

import Datepicker from './Datepicker';

function datepicker(input) {
    return new Datepicker(input);
}

datepicker.collection = inputs => {
    return Array.from(inputs).map(input => new Datepicker(input, config));
}

export default datepicker;

Multiple factory functions

datepicker(input) // {Datepicker}
datepicker.collection(inputs) // {Array.<Datepicker>}

We now export two factory functions for different use-cases.

This a common pattern in many APIs, where the most frequently used functionality can be called with the simplest and most succinct syntax, and more specialized functionality is available via additional public methods.

Because all functions in JavaScript are objects, we can add arbitrary properties and methods to them as in the above example, essentially extending the “namespace” around our primary factory API. This is coincidently how static methods are implemented in ES6 classes, and can also provide a useful means for encapsulating things like constants, helper functions and other parts of the public API.

Asynchronicity

Finally, let’s consider asynchronicity. Imagine that instantiating our datepicker requires the querying of an external HTTP API to obtain an accurate value for the current time, independent of the time or locale of the local machine.

By using a factory function, we could easily combine instantiation and an API query into a single promise-returning function, resolving with a reference to the new instance:

export default function datepicker(input) {
    const instance = new Datepicker(input);

    return instance.getRemoteTime()
        .then(() => instance);
}

Calling an asynchronous factory function

datepicker(input)
    .then(picker => console.log('ready!'));

Configuration

With the primary API entry point syntax established, the next step is to ensure that our datepicker is configurable and therefore reusable across a wide range of use-cases.

As an example, let’s say we can pass an optional boolean to our datepicker to dictate whether or not we want the UI to automatically close whenever a user selects a date. We might also want to specify the duration for a fade-in animation when the datepicker opens. So that’s already two additional parameters on top of the input element reference, and we can imagine there could be many more depending on how configurable we want the datepicker to be.

Our first attempt might involve simply passing more parameters to the factory, but there’s a number of problems with this approach that most developers will have already experienced at some point. Firstly, It’s just not scaleable, and remembering the order of parameters will make the API brittle, with optional parameters particularly unpleasant to work with.

Using parameters for configuration leads to brittle code

const picker = datepicker(input, false, 300);

Additionally, if someone comes across the above in your code, it’s not obvious what these parameters are doing without referring to the documentation.

A very common alternative is to pass a “configuration object”.

const picker = datepicker(input, {
    animationDuration: 300,
    closeOnSelect: false
});

This is much better already. Our parameters are now named and therefore self-documenting, and optional parameters can be omitted, falling back to default values defined in our internal code. Additionally, by not passing the input element reference as part of the configuration object, we create visual separation between an initial mandatory parameter and an optional configuration object.

But this approach brings others problems. Firstly these named properties are now part of our public API, so we need to take care to name and organize them appropriately and consistently, as well as ensuring that any user provided values for these properties are permitted and validated.

The following pattern is an approach I’ve found to be particularly helpful and allows us to build these kind of configuration interfaces quickly and robustly.

Firstly we declare a class to hold our default configuration options:

A configuration class, holding default values

class Config {
    constructor() {
        this.closeOnSelect      = true;
        this.animationDuration  = 200;
        this.animationEasing    = 'ease';

        Object.seal(this);
    }
}

Notice how similar properties are named or prefixed consistently, and defaults are set according to the most likely user settings, meaning that the most number of users will require the least amount of configuration.

Also, rather than leaving properties uninitialized, or simply setting all empty values to null, we ensure that each property is initialized with a value matching the expected type for that property, making validation of user defined values a breeze. For example, if animationDuration isn’t a number, we can immediately throw a TypeError without having to check if it was passed by the user or not. The whole class definition also serves as a very useful reference point in our code should we need to remind ourselves which configuration options have been implemented.

Finally note the use of Object.seal() at the end of the constructor. Part of JavaScript since ES5, I’ve found Object.seal() to be one of the most useful and underused pieces of functionality when it comes to writing robust runtime code.

By calling seal() on this, we ensure that no additional properties can be added to the resulting object, which gives us a considerable amount of validation out of the box, as an exception will be thrown if a user attempts to pass a configuration option not defined in the Config class.

We can implement this kind of functionality very easily using Object.assign():

Merging user-defined configuration values with defaults

class Datepicker() {
    constructor(input, config={}) {
        this.input  = null;
        this.config = new Config();

        this.init(input, config);
    }

    init(input, config) {
        this.input = input;

        Object.assign(this.config, config);
    }
}

export default function datepicker(input, config) {
    return Datepicker(input, config);
};

Within our Datepicker class, we have a config property, initialized with an instance of the Config class. When we call the init() method within the class, we use Object.assign() to shallow merge the user provided configuration values into the instance’s config object, which will then become available to all other class methods for the lifecycle of the datepicker.

datepicker(input, {
    hideOnSelection: false
});

// TypeError: Can't add property hideOnSelection, object is not extensible

In the above example, the user accidentally passed hideOnSelection instead of hideOnSelect. Because our configuration object is sealed, a TypeError was immediately thrown, which would no doubt save the user valuable debugging time trying to figure out why their UI isn’t behaving as expected.

While static typing tools like TypeScript can give us this kind of robustness and safety in our IDE or at compile-time, when writing “public” APIs we have to anticipate errors at runtime. We should also be conscious to cater to the lowest common denominator in terms of developer expertise. For example, we shouldn’t take it for granted that our users are using an editor with intellisense, or understand the difference between 'false' and false.

Returning to the error message above – it’s already a great improvement over silently allowing the user to add invalid properties, but at this point the best a user can do is go running to the documentation and see what they’ve done wrong. Can we provide something more helpful?

Let’s look at our init() method again:

init(input, config) {
    this.input = input;

    try {
        Object.assign(this.config, config);
    } catch(err) {
        handleMergeError(err, this.config);
    }
}

function handleMergeError(err, target) {
    const re = /property "?(\w*)"?[,:] object/i;

    let matches = null;

    if (!(err instanceof TypeError) || !(matches = re.exec(err.message))) throw err;

    const keys = Reflect.ownKeys(target);
    const invalid = matches[1].toLowerCase();

    const bestMatch = keys.reduce((bestMatch, key) => {
        let i = 0;

        while (
            i < invalid.length &&
            invalid.charAt(i) === key.charAt(i).toLowerCase()
        ) i++;

        return i > bestMatch.length ? key : bestMatch;
    }, '');

    const suggestion = bestMatch ? `. Did you mean "${bestMatch}"?` : '';

    throw new TypeError(`Invalid configuration option "${matches[1]}"${suggestion}`);
}

Now let's try that again:

datepicker(input, {
    hideOnSelection: true
});

// TypeError: Invalid configuration option “hideOnSelection”. Did you mean “hideOnSelect”?

Much better! While this might seem fairly crude, it will catch a large proportion of user errors including typos, missing words or plurals, incorrect casings and so on. Small pieces of functionality like this can greatly improve the experience of working with an API and expedite development.

Note the regular expression used to match the error message. As different browsers throw slightly different error messages here, we need an expression flexible enough to catch all variations while always capturing the offending property name.

Of course we can’t guarantee that the browser vendors won’t change these error messages in the future, but the error handler ensures that the original error is always thrown should the expression fail to find a match.

Complex Configuration

As we add more and more options to our configuration object, we might find that our configuration API starts to become disorganized and overwhelming. In these situations, I’ve found that breaking configuration objects down into distinct organized “domains” can greatly improve usability.

Splitting configuration into domains

const picker = datepicker(el, {
    animation: {
        duration: 150
    },
    behavior: {
        closeOnSelect: false
    }
});

To implement this kind of structure, we simply need to define additional sealed configuration classes for each nested configuration object:

class ConfigAnimation {
    constructor() {
        this.duration = 300;
        this.easing   = 'ease';

        Object.seal(this);
    }
}

class ConfigBehavior {
    constructor() {
        this.closeOnSelect: true

        Object.seal(this);
    }
}

class ConfigRoot {
    constructor() {
        this.animation = new ConfigAnimation();
        this.behavior  = new ConfigBehavior();

        Object.seal(this);
    }
}

Note that we’ll now need to use a recursive merge or extend in our configure method, as Object.assign() will only perform a shallow merge. Many libraries such as jQuery and lodash provide recursive merge functions that we can use in this scenario if we don’t fancy writing our own.

_.merge(this.config, config);

However, if we want to maintain our helpful custom error messages, we’ll want to write our own recursive extend so we can pinpoint exactly where the error occurs and interrogate the appropriate target object for the closest matching property name, which may be a nested object.

Take a look at the full datepicker implementation on GitHub for a working example of a recursive extend with error handling.

Lean Public APIs via Public and Private Members

We’ve already looked at the primary “entry point” to the datepicker's API (the factory function) now let’s look at secondary entry point – the Datepicker instance that is returned.

As with many UI libraries, we can maintain a reference to the returned instance to perform API methods on at a later time.

const picker = datepicker(input);

picker.setValue('2017-05-22');

We can see that we’ll need a public setValue() method in our Datepicker class. Looking back at our configuration functionality, we already have other internal methods in the class such as init(), and you can imagine others such as an event handler or render function.

We need to be able to differentiate between the methods we want to expose publicly, and those that are only used internally and therefore don’t concern the consumer. This differentiation is described as “public” and “private”. In many languages, the concept of public and private members is baked in, and members marked as private are not accessible outside of the class instance.

In JavaScript however, we don’t (yet) have such magic, but the benefits of hiding your implementation should not be overlooked. Keeping internal functionality private creates a better, cleaner API by presenting users with only what concerns them, and avoids accidental tampering of internal state and so on. It also allows us to safely refactor our internal implementation without worrying about breaking external integrations, and makes code more testable. Our public API therefore becomes the contract established between the software and those developers integrating it, based on the understanding that it will behave in a certain way and (ideally) won’t change between versions.

Over the years there have been numerous creative approaches devised to implement private members in JavaScript. Let’s start by looking at a couple of the more well-known ones.

Underscored Member Names
class MyClass {
    constructor() {
        this.myPublicProp   = false;
        this._myPrivateProp = false;

        Object.seal(this);
    }

    myPublicMethod() {
        // Do something public
    }

    _myPrivateMethod() {
        // Do something private
    }
}

This approach allows us to maintain nice constructor/prototype separation, but provides nothing more than a visual warning to users to avoid touching certain properties and methods, then hopes for the best. It also does little to add clarity to your API as should a user inspect the instance in their console, they will still have to trawl through the full collection of public and private members in order to find what they’re looking for.

Closures

Another popular approach makes use of “closures” (the private namespaces created when functions are invoked), in this case our constructor function:

class MyClass {
    constructor() {
        let _myPrivateProp = false;

        this.myPublicProp = false;

        function _myPrivateMethod() {
            // Do something private
        };

        this.myPublicMethod() {
            // Do something public
        }

        Object.seal(this);
    }
}

While this approach does have the benefit of actually making private members completely inaccessible outside of the class (essentially variables defined within the closure), it requires that we break the established constructor/prototype pattern in favor of something far less organized.

Our private functions also need to be explicitly bound to this in order to access public instance properties when called implicitly, so we end up with a strange mixture of syntaxes and patterns. As functionality is added we also end up with a large amount of variables floating around within the constructor and a lack of differentiation between temporary function variables and the so-called private properties of the class. If we follow this approach to its natural conclusion, we will inevitably end up asking the question "why use classes at all?".

Personally I’ve found that this approach can quickly lead to spaghetti code, as the reliance on closures makes it hard to untangle and compose functionality – if we want to work with class-oriented code.

Over the last few years, I’ve experimented with both of these approaches and found neither to be particularly useful in terms of creating lean APIs, particularly when inheritance and extensibility is added to the mix.

A New Solution: Facades

The solution I’ve arrived at after much trial and error builds upon ideas from both of above, but introduces the idea of a “facade”.

Like its name suggests, the facade is simply something that sits between the consumer and implementation, and cherry-picks which methods and properties the consumer should be able to interface with.

The first step involves defining our class as if all methods and properties are public. We get to keep our constructor/prototype pattern, and can use a consistent calling syntax throughout our code.

I typically like to use JSDoc comments to denote private and public methods for the benefit of anyone reading the code, and of course to ensure that private methods are not included in any generated documentation.

The datepicker implementation, with various public and private members

class _Datepicker {
    constructor(input, config={}) {
        this.input  = null;
        this.value  = '';
        this.config = new Config();

        this.init(input, config);
    }

    /**
     * @private
     */

    init(input, config) {
        this.input = input;

        Object.assign(this.config, config);
    }

    /**
     * @private
     */

    handleClick(e) {
        // ...
    }

    /**
     * @private
     */

    render() {
        // ...
    }

    /**
     * @public
     */

    getValue() {
        return this.value;
    }

    /**
     * @public
     */

    setValue(newValue) {
        this.value = newValue;
    }
}

Secondly, we create the facade:

The datepicker facade

class Datepicker {
    constructor() {
        const _ = new _Datepicker(...arguments);

        this.getValue = _.getValue.bind(_);
        this.setValue = _.setValue.bind(_);
    }
}

As you can see, the facade is simply another class which sits in front of our implementation, instantiates the underlying implementation and exposes only its public methods – again using a constructor function’s closure to keep things private. Anything not exposed by the facade is then inaccessible. Additionally, because all methods are explicitly bound to the implementation within the closure, methods can be called implicitly without breaking the context of this.

It’s clean and self-documenting, and also provides a visual reference point for the public API, both for our own purposes, and also for any developer who might inspect the instance while working on integration. As both the facade and the underlying implementation are classes, both can be extended using inheritance if necessary.

In addition to methods, we may also want to expose properties as part of our public API. Let’s refactor our getValue() and setValue() methods into a single virtual value property using getters and setters. We’ll also add a property that returns a reference to the input element that the datepicker was instantiated on, as well as several other public methods we might expect to see on a typical UI widget.

A facade exposing methods and properties

class Datepicker {
    constructor() {
        const _ = new _Datepicker(...arguments);

        this.open    = _.open.bind(_);
        this.close   = _.close.bind(_);
        this.destroy = _.destroy.bind(_); 

        Object.defineProperties(this, {
            value: {
                get: _.getValue.bind(_),
                set: _.setValue.bind(_)
            },
            input: {
                get: () => _.input
            }
        });
    }
}

Rather than using the ES6 class get/set syntax, ES5's Object.defineProperties() allows us to define getters and setters within our constructor closure and therefore gain access to the private implementation. As in the case of the input property above, we can omit a set() method as an easy means of making the property read-only.

The beauty of this approach is that the code within the underlying implementation is never affected and doesn’t need to be changed in anyway. We still maintain consistent and cleanly separated code internally, and delegate the task of picking and choosing the public API solely to the facade.

When using modules, I typically define both the implementation and the facade in the same module, exporting only the facade to ensure that our implementation remains completely hidden outside from the outside.

class Datepicker {  … } // Facade

class _Datepicker { … } // Implementation

export default Datepicker;

I’ve found that declaring the facade first at the top of the file provides a helpful visual introduction to the class’s public API for anyone viewing the code, as well as indicating that a facade is in use. You could however, just as easily break the facade and implementation into separate modules.

All of the techniques discussed in this post can be applied to our code in many different ways, but all help to make the software we write and the APIs we export easier to use by improving the developer experience. The examples above really just scratch the surface of what’s possible and many of these patterns can be further genericized and used to make our own lives as developers easier regardless of whether we’re writing APIs or not.

Be sure to checkout the full datepicker implementation on GitHub for a working real-world example of what I’ve coined as the “Factory > Facade > Implementation” pattern, which I've also used with MixItUp and numerous other tools I’ve built over the past few years.

Have a question about this article? Leave a comment.

Code can be added to comments via permitted Disqus HTML tags.