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
Flexible & Robust
Good APIs consider the many ways that developers might want to interact with them, and allow a variety of inputs, without breaking.
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.
Good APIs allow for high reuse through configuration.
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.
So how we can apply some of these qualities to our code?
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.
Constructors and Factories
However we rarely see this “naked” instantiation syntax in the APIs of our most popular tools.
Similarly, outside of the context of UI:
All of the above serve the same purpose – instantiation – but avoid the direct use of the
- Dynamic Return Types
By abstracting instantiation behind a factory function, our primary “public” API entry point now becomes simply:
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:
Let’s look at some of the other benefits to using factory functions as API entry points.
What if the API consumer has already attached a datepicker to the provided input element?
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.
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.
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:
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.
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:
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.
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”.
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:
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() to be one of the most useful and underused pieces of functionality when it comes to writing robust runtime code.
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
We can implement this kind of functionality very easily using
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.
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
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:
Now let's try that again:
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.
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.
To implement this kind of structure, we simply need to define additional sealed configuration classes for each nested configuration object:
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.
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.
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.
Underscored Member Names
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.
Another popular approach makes use of “closures” (the private namespaces created when functions are invoked), in this case our constructor function:
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.
Secondly, we create the facade:
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
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
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.
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.
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.