API Patterns for Your Open Source JavaScript Plugin

So many choices. Bryan shares how you can make sure you've chosen an API that grows with your project.

I recently refactored my open-source JavaScript project for its 1.0 release. The code had all been jammed into a global function, which did the job, but was quickly becoming insufficient as I added functionality. I needed something better.

As I prepared to refactor, I looked up a number of other successful JavaScript plugins to see how their APIs were designed, taking notes as I went. In this post, I want to show you the patterns I found and the strengths and weaknesses of each approach.

Setting Boundaries

Before we get started, here’s the criteria I used:

  1. API patterns must be for frontend JavaScript only. Node projects are a different animal.

  2. Must be a “plugin.” We’re talking about open source projects that add a bit of functionality to a codebase. I’m not collecting design patterns for full-on applications (though there are some great resources out there if that’s your interest).

I did not discount certain project sizes, but rather looked at projects of all sizes: large, medium, and small. I’m interested in what approaches are being used for all these situations.

To compare the approaches, I’m going to use a fictitious project called ✨ rainbowSparkles.js ✨.

Also, this is an exploration of APIs, not an exhaustive list of design patterns. Where design patterns produce similar APIs, I won’t even mention them.

Ready? Here we go.

1. Include and Go

Sometimes the best API is no API at all! For JS, that means simply including the source (via script tag, module loader, or build process) and letting your code do the rest. This can be useful for polyfills or other utilities that require no configuration.

<script src=”/rainbowSparkles.js”></script>

One common way to implement this is to run all your code in an immediately invoked function expression. This executes your code while keeping your functions and variables out of the global scope.

// inside rainbowSparkles.js
(function() {
  // Run code here.
})();

This API format is used in:

2. Global Function

This provides a global function that users simply call to run the code. You can pass in parameters to provide a bit of customization on how it works. It’s nice and simple; though, it isn’t well-suited for storing a lot of configuration, if that’s what you need.

// Add magic sparkles to all h1’s on the page.
rainbowSparkles(‘h1’);

// or…
// Crank the level of sparkles up to 11.
rainbowSparkles(11);

This API format is used in:

3. Constructor Interface

This API provides a global function that the user can construct new objects from. Each instance is created with the ‘new’ keyword, and they all inherit functionality from the same prototype.

I’ve found that this pattern is pretty common for UI components (tooltips, modals, etc) because it aligns well with the need for developers to place multiple components on the page that all share most of the same functionality.

var miniSparkles = new RainbowSparkles({ size: 2 });
var megaSparkles = new RainbowSparkles({ size: 9 });

miniSparkles.shower(1000);
megaSparkles.radiate(‘.call-to-action’);

Used in:

4. Factory Interface

This pattern sets up a global function that the user runs, giving an object in return. That object can contain whatever functionality you want, like configuration or methods.

As an API, this looks a lot like the constructor interface:

// constructor
var mySparkles = new RainbowSparkles({ size: 2 });

// factory
var mySparkles = rainbowSparkles({ size: 2 });

They both return objects, but since the factory pattern is a function, it can run arbitrary code (like logic and conditions) before giving you your object. That allows you to do things like return fundamentally different objects depending on context or configuration. For example:

var mySparkles = rainbowSparkles({
  object: ‘sparkles’,
  size: 6,
  shimmer: 8
});

var myRainbow = rainbowSparkles({
  object: ‘rainbow’,
  type: ‘double’
});

myRainbow.appear();
mySparkles.shower(1000);

Used in:

5. Singleton Interface

This approach means that your plugin provides a single global object containing all the functionality, settings, and data.

Singletons are often used to namespace a lot of functionality—whole applications, even. Despite exposing just one global, they’re not that restrictive (compared to a global function, for example).

rainbowSparkles.settings = { size: 5, shimmer: 8 };
rainbowSparkles.shower(1000);

Used in:

6. Constructor and Instance

This is simply setting up a constructor and pre-creating a single instance for users to use. This might make sense if you expect that most people will only need one instance, but you want to leave the option open for developers to create their own instances.

The only downside to this approach that I can think of is that you end up exposing two globals.

// It has the simplicity of a singleton...
rainbowSparkles.settings = { size: 5, shimmer: 8 };
rainbowSparkles.shower(1000);

// ...but users can also create new instances if needed.
var altSparkles = new RainbowSparkles({ size: 5, shimmer: 8 });
altSparkles.shower(1000);

Used in:

7. Method Chaining

This API pattern allows you to run several functions in sequence, each one, passing its result to the next one. You’ll probably recognize function chaining from jQuery, which uses it to great effect:

// Jquery does it.
$(‘#message’).text("Hello!").css("color", "red");

// We can do it too.
rainbowSparkles.shower(500).shimmer().fadeOut();

This API pattern can actually be used in conjunction with any of the patterns above. Any time you execute a function, you can return its context to chain additional methods.

This API format is used in:

8. Framework-Specific Plugin

Thinking your project would make a nice jQuery plugin? Maybe a React Component?

Here’s my opinion: if you want to get the most mileage for your project, don’t start it out as a framework-specific plugin. Doing so locks it into that ecosystem, reducing the number of people who can use it. On the other hand, a dependency-free JavaScript plugin can be included in all sorts of projects with a little integration code.

Of course once your dependency-free plugin is built and succeeding in the wild, you can always write integrations for jQuery, Angular, React, (and more), where each integration is a simple wrapper around your vanilla plugin. Doing it this way lets you follow the demand, instead of wasting time building integrations that nobody uses.

Plan to Plan

Bringing these approaches together helped me find an API that worked well for my upgrade, but ideally, I would have done this exercise in the beginning. It’s better to start with the right API in the first place, so you can provide smoother, non-breaking upgrades for developers. Of course, projects change and that's not always possible. As long as you know your options, you'll be able to learn as you go and make informed adjustments along the way.