How to Limit Dependency on 3rd Party Tools and Libraries

tl; dr

Create a single entry point for 3rd party tools and libraries so when you need to change things you only have to make the change once.

NPM and other package managers have made integrating 3rd party libraries easier than ever.

Need to build a UI? There's a package for that.

Need to format time? There's a package for that.

Need to add authentication? There's a package for that.

Nowadays there's open sourced code that does practically everything. Just npm install it and you're on your way. Simple enough, right?

But what happens when that package you installed stops being maintained or you find out has a security vulnerability? What do you do when a newer, faster, and smaller package is released?

It's easy to allow 3rd party code to weave and integrate itself into your app, and it's only until you need to replace or remove it that you realize it's become more of a weed than a benefit.

Refactoring and changing libraries is eventually inevitable in practically any long term project. So what can you do to make the migration process easier?

Reducing Dependency

In order to reduce dependency on a 3rd party library, you can create a single entry point where the 3rd party library is accessed. Then, throughout the app, you can access the local entry point instead of accessing the package directly. The day you decide to migrate, all you have to do is migrate the entry point instead of every instance the package was used.

Wrapping 3rd party libraries in this manner also makes it easier for you to add or modify functionality if something needs to be adjusted.

If the newer library has a different API, or if you just aren't a fan of the API to begin with, this gives you the opportunity to create your own custom API while reaping all the rewards of using an existing library.

An Example

Let's say there's a package we want to add called text-upper that capitalizes text.

Instead of accessing the package directly, first we create an entry point in say utils/caseChanger. Usually it's better to name it something generic or descriptive instead of just naming the entry point the same as the package.

Inside the entry point can be something as simple as:

import package from 'text-upper'

export default package

Then anytime we need to upper case text we just access the library through our entry point like so:

import caseChanger from './utils/caseChanger'

upperCase.makeUpperCase('hello world')

Later on, we find an even better package called textMcChanger that not only makes text uppercase, but also makes text lower case too. We definitely want that.

So now all we have to do is modify our entry point to use this new package. And even though the new package has a slightly different API for capitalizing text, we only have to modify our entry point to ensure everywhere we're already capitalizing text doesn't also need to be refactored.

Our migration in the utils/caseChanger might end up looking something like this:

import betterPackage from 'textMcChanger'

// Modify API for backwards compatibility
betterPackage.makeUpperCase = text => betterPackage.upper(text, true)

// Modify API for consistency with old API
betterPackage.makeLowerCase = text => betterPackage.lower(textt, true)

export default betterPackage

Easy enough, right? All we had to do was change this one file and our app instantly reaps the benefits of the new package without requiring all the extra refactor.

Conclusion

Obviously it won't make sense to wrap every package like this. It would be silly to try to add a wrapper for React as your UI library and then later try to transform Vue to work like React. But I guess you could!

For most other things though, it's a good practice to follow, and you'll thank yourself later once you find the perfect replacement and you don't have to dread the migration.