How (And Why?) To Wrap External Libraries?
Dawid Sibiński
Posted on February 20, 2023
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 😉
Why?
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 axios
.
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:
// ProductsList.tsx – calling axios directly from component's code
import { useEffect, useState } from "react";
import axios from "axios";
import { Product } from "../types/product";
import { DummyJsonProductsResult } from "../types/dummyJsonProductsResult";
export const ProductsList = () => {
const [products, setProducts] = useState<Product[] | null>(null);
useEffect(() => {
axios
.get<DummyJsonProductsResult>("https://dummyjson.com/products")
.then((result) => {
setProducts(result.data.products);
});
}, []);
// ...
};
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:
// httpClient.ts – IHttpClient interface (get only)
interface IHttpClient {
get<TResponse>(url: string): Promise<TResponse>;
}
That’s what we have so far. We just need to retrieve the data with GET
method.
Next step is to create the actual implementation of IHttpClient
using axios
. This is pretty straightforward using a class
implementing IHttpClient
interface and taking a look at axios
‘s documentation:
// httpClient.ts – AxiosHttpClient implementation (get only)
class AxiosHttpClient implements IHttpClient {
private instance: AxiosInstance | null = null;
private get axiosClient(): AxiosInstance {
return this.instance ?? this.initAxiosClient();
}
private initAxiosClient() {
return axios.create();
}
get<TResponse>(url: string): Promise<TResponse> {
return new Promise<TResponse>((resolve, reject) => {
this.axiosClient
.get<TResponse, AxiosResponse<TResponse>>(url)
.then((result) => {
resolve(result.data);
})
.catch((error: Error | AxiosError) => {
reject(error);
});
});
}
}
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 :
export const httpClient: IHttpClient = new AxiosHttpClient();
That’s basically how we wrap external libraries in TypeScript. Easy-peasy 😉
Using the wrapper
I can now use our wrapper in ProductsList.tsx
component:
// ProductsList.tsx – using IHttpClient wrapper
import { useEffect, useState } from "react";
import { httpClient } from "../wrappers/httpClient"; // we don't import axios here anymore
import { Product } from "../types/product";
import { DummyJsonProductsResult } from "../types/dummyJsonProductsResult";
export const ProductsList = () => {
const [products, setProducts] = useState<Product[] | null>(null);
useEffect(() => {
httpClient // instead of axios, we use our wrapper in components
.get<DummyJsonProductsResult>("https://dummyjson.com/products")
.then((result) => {
setProducts(result.data.products);
});
}, []);
// ...
};
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 IHttpClient
contract:
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 fetch
.
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 – FetchHttpClient
implementing IHttpClient
interface:
// httpClient.ts – new FetchHttpClient using fetch for get and post
class FetchHttpClient implements IHttpClient {
get<TResponse>(url: string): Promise<TResponse> {
return new Promise<TResponse>((resolve, reject) => {
fetch(url)
.then((response) =>
response
.json()
.then((responseJson) => {
resolve(responseJson as TResponse);
})
.catch((error: Error) => {
reject(`Response JSON parsing error: ${error}`);
})
)
.catch((error: Error) => {
reject(error);
});
});
}
post<TResponse>(url: string, data?: object): Promise<TResponse> {
return new Promise<TResponse>((resolve, reject) => {
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
.then((response) =>
response
.json()
.then((responseJson) => {
resolve(responseJson as TResponse);
})
.catch((error: Error) => {
reject(`Response JSON parsing error: ${error}`);
})
)
.catch((error: Error) => {
reject(error);
});
});
}
}
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:
export const httpClient: IHttpClient = new FetchHttpClient(); // instead of new AxiosHttpClient()
and that’s it! Our whole application now uses fetch
for GET
and POST
HTTP requests 🙂 And it even still works 😅
Summary and source code
I hope that now you see how important it is to wrap external libraries. We have seen that on JavaScript/TypeScript app example, but this is applicable to any programming language and framework.
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
Posted on February 20, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.