Design and Build Your Own JavaScript Library: Tips & Tricks

Tim Severien
Share

This article was peer reviewed by Adrian Sandu, Vildan Softic and Dan Prince. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Libraries: we use them all the time. A library is packaged code that developers can use in their projects, which invariably saves work and prevents reinventing the wheel. Having reusable packages, either open or closed-source, is better than rebuilding the same feature, or manually copying and pasting from past projects.

But other than packaged code, what is a library exactly? With a few exceptions, a library should always be one file, or several in a single folder. Its code should be maintained separately and should remain as-is when implementing it in your project. A library should allow you to set project-specific configuration and/or behavior. Think of it as a USB-device that only allows communication through the USB port. Some devices, such as mice and keyboards, allow configuration through an interface provided with or by the device.

In this article, I will explain how libraries are built. Although most of the topics covered will apply to other languages, this article is mainly focused on building a JavaScript library.

Why Build Your Own Javascript Library?

First and foremost, libraries make the reuse of existing code very convenient. You don’t have to dig up an old project and copy some files, you just pull the library in. This also fragments your application, keeping the application codebase smaller and making it easier to maintain.

Christ Church Library, Oxford
Christ Church Library (source)

Any code that makes achieving a certain goal easier and which can be reused, like an abstraction, is a candidate to be bundled into a library. An interesting example is jQuery. Although jQuery’s API is considerably more than a simplified DOM API, it meant a lot a few years ago, when cross-browser DOM manipulation was rather difficult.

If an open-source project becomes popular and more developers use it, it’s likely people will join in and help with that project by submitting issues or contributing to the code base. Either way, it will benefit the library and all the projects depending on it.

A popular open-source project can also lead to great opportunities. A company may be impressed by the quality of your work and offer you a job. Maybe a company will ask you to help integrate your project into their application. After all, no one knows your library better than you.

For many it’s merely a hobby—enjoying writing code, helping others, and learning and growing in the process. You can push your limits and try new things.

Scope and Goals

Before writing the first line of code, it should be clear what the purpose of your library is—you have to set goals. With them, you can maintain focus on what problem you hope to solve with your library. Keep in mind that your library should be easier to use and to remember than the problem in its raw form. The simpler the API, the easier it will be for users to learn to use your library. To quote the Unix philosophy:

Do One Thing and Do It Well

Ask yourself: What problem does your library solve? How do you intend to solve it? Will you write everything yourself, or can you utilize someone else’s library?

No matter the size of the library, try to make a roadmap. List every feature you want, then scrap as many as you can until you have a tiny, but functional library, much like a minimum viable product. That will be your first release. From there, you can create milestones for every new feature. Essentially, you’re breaking up your project into bite-size chunks, making every feature more of an accomplishment and more enjoyable. Believe me, this will keep you sane.

API Design

Personally, I really like to approach my library from the perspective of the end user. You could name it user-centric design. In essence, you are creating an outline of your library, hopefully giving it more thought and making it more convenient for whoever chooses to use it. At the same time you get to think about which aspects should be customizable, something discussed later in this article.

The ultimate API quality test is to eat your own dog food, to use your library in your own projects. Try to substitute application code with your library, and see if it covers all the features you desire. Try to keep the library as bare as possible, while keeping it flexible enough to make it work for their edge-cases too, through customization (as described later in this article).

Here’s an example of what the implementation, or outline of a User-Agent string library could look like:

// Start with empty UserAgent string
var userAgent = new UserAgent;

// Create and add first product: EvilCorpBrowser/1.2 (X11; Linux; en-us)
var application = new UserAgent.Product('EvilCorpBrowser', '1.2');
application.setComment('X11', 'Linux', 'en-us');
userAgent.addProduct(application);

// Create and add second product: Blink/20420101
var engine = new UserAgent.Product('Blink', '20420101');
userAgent.addProduct(engine);

// EvilCorpBrowser/1.2 (X11; Linux; en-us) Blink/20420101
userAgent.toString();

// Make some more changes to engine product
engine.setComment('Hello World');

// EvilCorpBrowser/1.2 (X11; Linux; en-us) Blink/20420101 (Hello World)
userAgent.toString();

Depending on the complexity of your library, you may also want to give some thought to structuring. Utilizing design patterns is a great way to structure your library, or even to overcome some technical problems. It also reduces the risk of refactoring large parts when adding new features.

Flexibility and Customization

Something that makes libraries great, is flexibility, yet it is also difficult to draw a line between what you can and what you can’t customize. A perfect example of that is chart.js vs D3.js. Both are excellent libraries to visualize data. Chart.js makes it really easy to create and style different types of built-in charts. But if you need more control over graphics, D3.js is what you need.

There are various ways to give control to the user: configuration, exposing public methods and through callbacks and events.

Configuring a library is often done during initialization, but some libraries allow you to modify options during run-time. Options are often limited to tiny bits and pieces and changing these shouldn’t do anything other than updating these values for later use.

// Configure at initialization
var userAgent = new UserAgent({
  commentSeparator: ';'
});

// Run-time configuration using a public method
userAgent.setOption('commentSeparator', '-');

// Run-time configuration using a public property
userAgent.commentSeparator = '-';

Methods can be exposed to interact with an instance, for example to retrieve data from the instance (getters), to put data in the instance (setters), and to perform actions.

var userAgent = new UserAgent;

// A getter to retrieve comments from all products
userAgent.getComments();

// An action to shuffle the order of all products
userAgent.shuffleProducts();

Callbacks are sometimes passed with public methods, often to run user code after an asynchronous task.

var userAgent = new UserAgent;

userAgent.doAsyncThing(function asyncThingDone() {
  // Run code after async thing is done
});

Events have a lot of potential. They are similar to callbacks, except adding event handlers shouldn’t trigger actions. Events are often used to indicate, you probably guessed, events! Much like a callback, you can provide additional information and return a value for the library to work with.

var userAgent = new UserAgent;

// Validate a product on addition
userAgent.on('product.add', function onProductAdd(e, product) {
  var shouldAddProduct = product.toString().length < 5;

  // Tell the library to add the product or not
  return shouldAddProduct;
});

In some cases, you may want to allow users to extend your library. For this, you can expose a public method or property users can populate, much like Angular modules (angular.module('myModule')) and jQuery’s fn (jQuery.fn.myPlugin), or do nothing and simply let users access your library’s namespace:

// AngryUserAgent module
// Has access to UserAgent namespace
(function AngryUserAgent(UserAgent) {

  // Create new method .toAngryString()
  UserAgent.prototype.toAngryString = function() {
    return this.toString().toUpperCase();
  };

})(UserAgent);

// Application code
var userAgent = new UserAgent;
// ...

// EVILCORPBROWSER/1.2 (X11; LINUX; EN-US) BLINK/20420101
userAgent.toAngryString();

Similarly, this allows you to overwrite methods as well.

// AngryUserAgent module
(function AngryUserAgent(UserAgent) {

  // Store old .toString() method for later use
  var _toString = UserAgent.prototype.toString;

  // Overwrite .toString()
  UserAgent.prototype.toString = function() {
    return _toString.call(this).toUpperCase();
  };

})(UserAgent);

var userAgent = new UserAgent;
// ...

// EVILCORPBROWSER/1.2 (X11; LINUX; EN-US) BLINK/20420101
userAgent.toString();

In case of the latter, giving users access to your library’s namespace, gives you less control over how extensions/plugins are defined. To make sure extensions follow some convention, you can (and should) write documentation.

Testing

Writing an outline makes a great start for test-driven development. In short, this is when you write down criteria in the form of tests, before writing the actual library. If these tests check whether a feature behaves like it should and you write those before writing your library, the strategy is called behavior-driven development. Either way, if your tests cover every feature in your library and your code passes all the tests, you can safely assume that your library works.

Jani Hartikainen explains how you can write unit tests with Mocha in Unit Test Your JavaScript Using Mocha and Chai. In Testing JavaScript with Jasmine, Travis, and Karma, Tim Evko shows how to set up a sweet testing pipeline with another framework called Jasmine. These two testing frameworks are very popular, but there are many more in many flavors.

My outline, created earlier in this article, already had comments on what the expected output is. This is where all tests start: with an expectation. A Jasmine test for my library would look like this:

describe('Basic usage', function () {
  it('should generate a single product', function () {
    // Create a single product
    var product = new UserAgent.Product('EvilCorpBrowser', '1.2');
    product.setComment('X11', 'Linux', 'en-us');

    expect(product.toString())
      .toBe('EvilCorpBrowser/1.2 (X11; Linux; en-us)');
  });

  it('should combine several products', function () {
    var userAgent = new UserAgent;

    // Create and add first product
    var application = new UserAgent.Product('EvilCorpBrowser', '1.2');
    application.setComment('X11', 'Linux', 'en-us');
    userAgent.addProduct(application);

    // Create and add second product
    var engine = new UserAgent.Product('Blink', '20420101');
    userAgent.addProduct(engine);

    expect(userAgent.toString())
      .toBe('EvilCorpBrowser/1.2 (X11; Linux; en-us) Blink/20420101');
  });

  it('should update products correctly', function () {
    var userAgent = new UserAgent;

    // Create and add first product
    var application = new UserAgent.Product('EvilCorpBrowser', '1.2');
    application.setComment('X11', 'Linux', 'en-us');
    userAgent.addProduct(application);

    // Update first product
    application.setComment('X11', 'Linux', 'nl-nl');

    expect(userAgent.toString())
      .toBe('EvilCorpBrowser/1.2 (X11; Linux; nl-nl)');
  });
});

Once you’re completely satisified with the API design for your first version, it’s time to start thinking about architecture and how your library will be used.

Module Loader Compatibility

You may or may not use a module loader. However, the developer that chooses to implement your library might, so you will want to make your library compatible with module loaders. But which one? How can you choose between CommonJS, RequireJS, AMD and others?

Actually, you don’t have to! Universal Module Definition (UMD) is another strategy aiming to support multiple module loaders. You can find different flavors of snippets online, but you can also find variations on the UMD GitHub repository to make your library UMD-compatible. Start your library using one of the templates, or add UMD with your favorite build tool, and you won’t have to worry about module loaders.

If you wish to use ES2015 import/export syntax, I highly suggest using Babel to compile to ES5 combined with Babel’s UMD plugin. That way you can use ES2015 in your project, while still producing a library fit for all.

Documentation

I’m all for thorough documentation for all projects, but it’s often considered a lot of work, deferred and eventually forgotten.

Basic Information

Documentation should always start with basic information such as a project name and a description. It will help others to understand what your library does and whether it is a good choice for them.

You can provide additional information like scope and goals to better inform users, and a roadmap so they know what to expect in the future or know how they can contribute.

API, Tutorials and Examples

Of course, you need to make users aware of how to use your library. This starts with API documentation. Tutorials and examples make great additions, but writing these can be a lot of work. Inline documentation, however, isn’t. These are comments that can be parsed and converted to documentation pages with JSDoc.

Meta-tasks

Some users may want to make changes to your library. In most cases this will be for contribution, but some may want to create a custom build for private use. For these users, it’s useful to include documentation for meta-tasks like a list of commands to build the library, run tests, generate, convert or download data, etc.

Contribution

When you open-source your library, contributions are great. To guide contributors, you can add documentation in which you explain the steps for making a contribution and the criteria it should fulfill. It will make it easier for you to review and accept contributions, and for them to do it right.

License

Last but not least, include a license. Technically, if you choose not to include one, it will still be copyrighted, but not everyone knows that.

I find ChooseALicense.com a great resource to choose a license without needing to be a legal specialist. After choosing a license, just save the text in a LICENSE.txt file in your project’s root.

Wrap It up and Add a Bow

Versioning is essential for a good library. If you ever choose to make breaking changes, a user probably wants to keep using the version that works for them.

The current de-facto standard for version naming is Semantic Versioning, or SemVer. SemVer versions consists of three numbers, each indicating a different change: major, minor and patch.

Adding Versions/Releases to Your Git Repository

If you have a git repository, you can add version numbers to your repository. You could consider them snapshots of your repository. Tags, we call them. To create a tag, open the terminal and type:

# git tag -a [version] -m [version message]
git tag -a v1.2.0 -m "Awesome Library v1.2.0"

Many services, like GitHub, will provide an overview of all your versions and download links for each.

Publishing to Common Repositories

npm

Many programming languages come with a package manager, or have third party package manager available. These allow us to pull in libraries specifically for those languages. Examples are PHP’s Composer and RubyGems for Ruby.

Node.js, a sort of stand-alone JavaScript engine, comes with npm. If you’re not familiar with npm, we have a great beginner’s guide.

By default, your npm package will be published publicly. Fear not! You can also publish private packages, set up a private registry or completely avoid publishing at all.

To publish your package, your project will need a package.json file. You can do that manually or use the interactive wizard. To start the wizard, type:

npm init

The version property should match your git tag. Also, be sure to have a README.md file. Just like GitHub, npm uses that for the page presenting your package.

After that, you can publish your package by typing:

npm publish

That’s it! You have published your npm package.

Bower

A few years ago, another package manager surfaced called Bower. This package manager, however, isn’t designed for a specific language, but for a specific platform–the web. You can find all major front-end assets right there. Publishing your package on Bower is only interesting if your library is browser-compatible.

If you’re not familiar with Bower, we have a beginner’s guide for that, too.

Much like npm, you can set up a private repository, too. You can also prevent it from being published completely in the wizard.

Interestingly, during the past year or two, many people seem to be converting to npm for front-end assets. Although npm packages are primarily JavaScript, many front-end packages are published on npm, as well. Either way, Bower is still popular, so I definitely recommend publishing your package on Bower as well.

Have I mentioned that Bower is actually an npm module, and was originally inspired by it? The commands are really similar. To generate a bower.json file, type:

bower init

Just like npm init, the instructions are self-explanatory. Finally, to publish your package:

bower register awesomelib https://github.com/you/awesomelib

Just like that you’ve put your library in the wild for everyone to use in their Node projects and/or on the web!

Conclusion

The core product is the library. Make sure it solves a problem, is easy to use and stable, and you will make your team or many developers very happy.

A lot of the tasks I mentioned are easily automated, for example: running tests, creating a tag, updating your version in package.json and republishing your package to npm and bower. This is where you enter the realm of continuous integration and use tools like Travis CI or Jenkins. The article by Tim Evko that I mentioned earlier touches on this.

Have you built and published a library? Please do share in the comments section below!