DEV Community

Cover image for How to create usable and maintainable npm packages
Alexey Yakovlev
Alexey Yakovlev

Posted on

How to create usable and maintainable npm packages

Open source era is upon us. Many day to day problems that developers meet already have open source solutions on the web. When encountering an issue we instinctively first search the web for ready-made solutions and make attempts at adopting them. However how often do you find existing solutions clunky, poorly documented, unmaintained and unmaintainable?

For me it is a common situation when I end up creating a solution of my own. Lately I have also started making those solutions open source and available to everyone. When doing so I started thinking about ways I can make my solutions more user friendly than others. And I ended up with a list of things I look for when creating a new npm package, a sort of check list of prerequisites to publish a package.

So how can you make your npm packages more maintainable and usable?

As a disclaimer I should say that I am mostly talking about small to medium npm packages that are quickly developed to solve a small-ish problem. Larger packages are a domain of their own and out of scope of this post.

Documentation

It seems very obvious and basic, but how often do you stumble upon a package that has a very basic or outdated README or no README at all? Such packages are mostly useless to users.

As an engineer of a solution it is your responsibility to provide sufficient documentation on how to employ a tool of your creation. But what should be sufficient?

Never publish a package without a README and sources links

Analysing my usage of npm packages I discovered that it is best to start with a small introduction of your package - what it does, what it helps to achieve. For example, package nest-next starts by saying that it is a "Render Module to add Nextjs support for Nestjs". This is an example of a good short description that would come up in search. Also do not forget to add this description to your VCS hosting (likely GitHub) and package.json so that it comes up in search better. The actual README file could have a more detailed introduction.

Having a Table of Contents helps greatly. Allow readers to quickly scan contents of your README by providing a list of sections with relevant names and anchors. Try not to have a very long list of items and do not overextend your introduction: Table of Contents should be immediately visible when opening a README. When the list becomes very large (more than approximately 10-12 items) it either means that you should reduce the amount of items or that you are dealing with a bigger package and should split up your README in separate docs or a whole website.

Continue with installation instructions. Which peer dependencies should you install? Maybe there are certain prerequisites and engine limitations? State it there and supply readers with installation snippets that are easy to copy.

Finally, instruct users on actual usage. How do you employ your tool? Where do you configure it? What configuration options are available? How to import it's entities? Maybe there are certain features that are not yet implemented or behave unexpectedly? How is your solution different to similar ones? Try to fit the most important things without turning your README into a JSDoc, focus on actual usage and recipes without implementation details. Perhaps leave a link to a project of your own that employs this package as an example.

It would also be nice to tell readers where to leave feedback or submit enhancements. Invite users to leave GitHub Issues or submit a Pull Request with relevant links. This is also the time to acknowledge other packages that might have inspired you.

Do not forget to leave keywords and VCS links in your package.json. And obviously always include README in your published files.

Managing code

Once again, it's fairly obvious to make your code readable. However some of the more popular packages tend to have all the code in a single file with a mixed bag of code styles. Other packages overengineer solutions. Strike a balance between the two.

Employ type-checking to make your code safer to change. You might not even use TypeScript or actual .ts files. You may use JSDoc typings and JS checks to leverage some type-safety without need to compile your code. I used this strategy in one of my packages and found it really useful. And having types through TypeScript annotations or JSDoc is a must for any exported functions.

Never write all the code in a single file. Create separate files for functions, maybe even folders for different types of functions and classes. Try not to add any dependencies to your project unless they are peer or likely to be reused in users node_modules. Use bundlephobia to track your package size.

Do not make your users like that

Do not invest in automatic tests. This might seem counterintuitive but I find spending time on unit tests for small packages wasteful. Unless your package is a unit of itself, a simple function or class with clear inputs and outputs.

While TDD and unit tests are amazing for product development, I find them fairly useless due to nature of small to medium packages that are either never-changing or ever-changing forcing you to endlessly update tests instead of focusing on the solution to the problem at hand. This obviously changes for larger packages and packages with huge user bases but it is not often you create one to solve a day to day problem.

Do not employ powerful and hard-to-configure tools to build and develop your project. Leave a basic .editorconfig file to keep codestyle under control for tiny packages. Do not minify your sources - it will not give a significant enough difference for small packages. It is far better to install a package with less dependencies than a package with minified sources. For most compilation needs Microsoft TypeScript Compiler (tsc) should do the job. Perhaps do not transpile your code at all.

Keep your package.json clean. State all the required snippets in scripts section. Specify a valid engines property. Use a valid main or module field and include only necessary files: dist folder or index file, README and LICENSE files. And perhaps most importantly properly specify package dependencies: peer dependencies should be peer, and no dev deps should be in actual dependency list.

It is also helpful to have a readable commit log. Packages with a single commit in VCS do not look very reliable. Great commit history also helps when discovering code through blame.

Remember, that it is impossible make a solution to all the problems. To conclude this section, your code should do two things: 1) solve the desired problem and no other problem with as little dependencies as possible; 2) be easy to extend or modify so that users can easily alter your solution to suit their goals.

Versioning

Another simple thing that is somehow hard to get right. Always employ Semantic Versioning. Invest in making your commits both human and machine readable. Conventional Commits can help you with that.

Do you speak semantic versioning?

It is not uncommon to find a package maintainer who would accept your contribution and then forget to publish a new version of their package. To make sure that it never happens with you create a basic CI workflow that would automatically manage versions and publish your package according to newly pushed commits to VCS. But do not use external services for CI - GitHub Actions and GitLab-CI would suffice.

Luckily such workflows are largely reusable and I have a few public Gists with GitHub Actions workflows for different types of projects. More serious project with tests and linting may employ this multi-stage workflow and smaller packages would be fine with a simple publish-on-push workflow. Both workflows employ bump-package-version-action of my own, check documentation for more details on it.

Be a human

Just that. Respect your users, respond to issues with manners and in a reasonable time, discuss enhancements with contributors and with detailed feedback. Focus not on having a package that you think works, but on a package that solves users problems and respects their opinion.

Remember that your goal is not to have the most popular package. Your goal should be collaborating on creating the best possible tool to solve a problem. Even if someone discovered a better solution as a fork of yours do not be mad at them - ask them if there is a way to integrate their solution into yours.

Conclusion

Let's rewind all the things I stated to a more concise list. When creating a npm package do:

  • create documentation for the user
  • provide the user with installation and usage instructions
  • warn the user of known issues and limitations
  • leave a link to your VCS
  • invite users to leave feedback and contributions
  • type-check your code
  • provide types for exported entities
  • keep dependencies up to date
  • strive to have a smaller package
  • create a readable and extendable code
  • employ Semantic Versioning
  • follow Conventional Commits
  • automate versioning and publishing
  • respect and collaborate with users

And do not:

  • publish a package with an insufficient or without a README
  • create a JSDoc out of your README
  • leave users without links to source code
  • write the code in a single file
  • overengineer solutions
  • invest in unit tests (unless your package is a unit of its own)
  • solve more than one problem at a time
  • release breaking changes without major versions
  • add unnecessary dependencies
  • forget to publish latest versions
  • argue with your users about functionality

Compiled list on a board

Do you agree with this list? Perhaps you find some points unreasonable or have something to add? Do you even believe in the open source itself and the future being largely open source? Be welcome to discuss in the comments.

Top comments (0)