If you use external libraries in your application, wrapping them may be very helpful. How to wrap external libraries and why it’s worth doing that? Today we’re going to dive into that, based on a TypeScript web app example 😉
You probably know what a wrapper is. As its name suggests, it’s a practice of putting another layer on a piece of something. In our case, wrapping a piece of code in another piece of code 🙂
But why would you do that? To make your life easier! 💪
Wrapping external libraries lets you abstract your code from their implementation details. In effect, it makes your life a lot easier when you want to keep the behavior, but change the library providing it. This approach also lets you use only those features from a given external dependency that you actually need.
Let’s see that with an example.
Wrapping HTTP client
A very good example is HTTP client wrapper. HTTP calls are used in almost every web application. In order to perform them, we need to choose an HTTP client. We can either use
fetch, or something more sophisticated like
However, with time, we may decide to replace it with something else. There might be many reasons for that – either the library stops to be maintained or something new and better is out there. It would be a shame if we’d now need to change the code in those thousands of places where the current library is being used. This would take a lot of time and might be error-prone. We can definitely prepare better for such cases 😉
Create HttpClient wrapper for axios
Let’s say that, for now, we will go with
axios. Instead of calling it directly from our code:
I will create an HttpClient wrapper for it and use it instead.
First, I create
httpClient.ts file in
wrappers folder. I like to have such a catalog in my React projects and keep all the wrappers there.
I start writing all wrappers with an interface. In that case, I treat the interface as a contract. It should say what I need this small wrapper to do, without worrying about implementation details.
IHttpClient interface initially looks as follows:
That’s what we have so far. We just need to retrieve the data with
Next step is to create the actual implementation of
axios. This is pretty straightforward using a
IHttpClient interface and taking a look at
This implementation lets us encapsulate a simple singleton inside the class.
The last step here is to expose the instance of our HTTP client. Remember to always export the interface type variable:
That’s basically how we wrap external libraries in TypeScript. Easy-peasy 😉
Using the wrapper
I can now use our wrapper in
Notice how easy that was. Since now, we only import stuff from
axios package in
httpClient.ts file. Only this single file is dependent on
axios npm package. None of our components (and other project files) know about
axios. Our IDE only knows that the wrapper is an object instance fulfilling
Extra wrapper features
Apart from nicely isolating us from dependencies, wrappers have more advantages. One of them is a possibility to configure the library in a single place. In our example with
axios – imagine that one day you want to add custom headers to each HTTP request. Having all API calls going via
AxiosHttpClient, you can configure such things there, in a single place. That way, you follow the DRY principle and keep all the logic related to
axios (or to any other external dependency) in a single place. It also comes with benefits like easy testability etc.
For clarity, I also added
post support to our
IHttpClient. You can check it here.
Replacing the wrapped library
Ok, it’s time to have our solution battle-tested. We have the HTTP client nicely wrapped and exposed as an instance of
IHttpClient. However, we came to the conclusion that
axios is not good enough, and we want to have it replaced with
Remember that in the real web application, you would have hundreds or thousands of usages of
IHttpClient instance. That’s where the power of wrappers comes into play 😎
So how do I make sure those thousands of usages will now use
fetch instead of
axios? That’s actually pretty straightforward. I’ll simply add a new class –
For completion, I included
POST here as well.
The one last thing I have to do to make our new
FetchHttpClient be used in the whole app in place of
AxiosHttpClient is to change a single line with export:
and that’s it! Our whole application now uses
POST HTTP requests 🙂 And it even still works 😅
Summary and source code
It’s always good to be as independent as possible of 3rd party stuff. Too many times I’ve been in a situation that some
npm package is so extensively used in a project, directly in the source code in hundreds of places, that it cannot be replaced without spending several days on it. Creating wrappers forces us to think abstract, which is another great advantage.
You can find the complete source code here: https://github.com/dsibinski/codejourney/tree/main/wrapping-external-libraries
Oh, how much those bull*t I saw in all my recent projects
What this approach actually brings to the table:
1. For each new developer: instead of looking into the library itself (usually well-documented), you need to debug a custom wrapper (usually not documented). What if you have 30 external libraries (and wrappers)? Or 100+?
2. How often do you really need to replace libraries in your project? I can’t remember any strong case in about 9 years of my work (including positions as Senior/Tech lead). And do you think you wouldn’t need to change your wrapper’s interface (API) in case of some drastic change?
This works in only one particular case – if you are ready to support and spend time on documentation for your wrapper. Elsewhere, it will cost you (mostly new developers in a team) a lot of headaches.
Hi Dmytro, thanks for your comment.
Extremism is always bad 🙂 I don’t recommend wrapping every library. I wouldn’t even think about abstracting stuff like mobx or react-hook-form. This would lead to – as you mentioned – having hundreds of wrappers with probably more logic inside the wrappers themselves than the actual library provides.
Referring to your first remark – if the wrappers are implemented incorrectly, i.e. having a lot of logic inside them instead of only exposing the 3rd party code with minimum amount of own code, then I’d agree it is a nightmare debugging this. However, to me, a wrapper should be minimalistic in a way that no documentation is needed for it. It only acts as a proxy to the external code. I create wrappers only for stuff that can be exposed via simple API, without much additional logic inside.
Regarding the second point – I had pretty different experiences here 😉 I saw external libraries used so extensively in the code that no one would even think about replacing them. And that’s often problematic.
If you need to change your wrapper’s API because of the changes in the wrapped library, then you either didn’t think abstract enough when creating the wrapper or you shouldn’t have created it in the first place (see my previous remarks about what not to wrap).
Wrappers’ role is not only to hide the 3rd party implementation details. They let you think first, use later. First: think which features of a library you might need, try to abstract them (in a form of contract). Later: use the contract’s implementation(s) in your code.