Key Takeaways
- Define Clear Objectives: Before starting your library, identify the specific problem it will solve to maintain focus and ensure its utility.
- User-Centric API Design: Design your library with the end-user in mind, making it simple and intuitive to improve user adoption and satisfaction.
- Flexibility and Customization: Offer customization options through configurations, public methods, and event handling to accommodate diverse user needs.
- Testing and Documentation: Implement thorough testing using frameworks like Mocha or Jasmine, and ensure comprehensive documentation to aid understanding and usage.
- Module Loader Compatibility: Ensure your library supports various module loaders by using Universal Module Definition (UMD) or similar approaches to maximize compatibility.
- Versioning and Publishing: Use Semantic Versioning for updates and publish your library to package managers like npm or Bower to reach a wider audience.
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.
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!
Frequently Asked Questions (FAQs) about Designing and Building Your Own JavaScript Library
What are the benefits of creating my own JavaScript library?
Creating your own JavaScript library has several benefits. Firstly, it allows you to reuse code across multiple projects, saving you time and effort in the long run. Secondly, it helps you to organize your code in a more structured and readable manner. This is particularly useful when working on larger projects or collaborating with other developers. Lastly, creating your own library can be a great learning experience, helping you to deepen your understanding of JavaScript and software development principles.
How do I start creating a JavaScript library?
The first step in creating a JavaScript library is to define its purpose. What functionality do you want your library to provide? Once you have a clear idea of what you want your library to do, you can start writing the code. This typically involves defining a series of functions that provide the desired functionality. These functions are then exposed through a public API that can be used by other developers.
How do I test my JavaScript library?
Testing is a crucial part of developing a JavaScript library. There are several testing frameworks available for JavaScript, such as Jest, Mocha, and Jasmine. These frameworks allow you to write unit tests for your functions, ensuring that they behave as expected. In addition to unit tests, you may also want to write integration tests to check that the different parts of your library work together correctly.
How do I document my JavaScript library?
Good documentation is essential for any software library. It helps other developers understand how to use your library and what each function does. You should include a detailed description of each function in your library, including its inputs, outputs, and any side effects. You can also use tools like JSDoc to automatically generate documentation from your code comments.
How do I distribute my JavaScript library?
There are several ways to distribute a JavaScript library. One common method is to publish it on a package manager like npm. This allows other developers to easily install your library using a simple command. You can also distribute your library by hosting it on a CDN (Content Delivery Network) or by providing a download link on your website.
How do I maintain my JavaScript library?
Maintaining a JavaScript library involves fixing bugs, adding new features, and keeping the library up-to-date with the latest JavaScript standards and practices. It’s important to regularly test your library and listen to feedback from users. You should also consider versioning your library, so users can choose to use a stable version or the latest version with new features.
How do I ensure my JavaScript library is efficient?
To ensure your JavaScript library is efficient, you should focus on writing clean, concise code. Avoid unnecessary computations and memory allocations. Use tools like Chrome DevTools to profile your library and identify any performance bottlenecks. You should also consider minifying your library to reduce its file size and improve load times.
How do I make my JavaScript library compatible with different browsers?
Ensuring browser compatibility can be a challenge due to differences in how each browser interprets JavaScript. You can use tools like Babel to transpile your code into a version of JavaScript that is compatible with older browsers. You should also test your library in different browsers to identify and fix any compatibility issues.
How do I handle errors in my JavaScript library?
Error handling is an important part of developing a JavaScript library. You should aim to provide clear, helpful error messages that help users understand what went wrong. You can use try/catch blocks to catch and handle errors. You should also consider providing a way for users to report bugs and issues.
How do I get feedback on my JavaScript library?
There are several ways to get feedback on your JavaScript library. You can ask other developers to review your code, post your library on forums or social media, or publish it on a package manager like npm and ask for feedback. You should be open to criticism and willing to make changes based on the feedback you receive.
Tim Severien is an enthusiastic front-end developer from the Netherlands, passionate about JavaScript and Sass. When not writing code, he write articles for SitePoint or for Tim’s blog.