Motivation
We often start using third-arty libraries like axios directly in our code. There is nothing wrong about this. However, in a world of ever changing libraries, packages, versions, etc. using these libraries API directly might lead to inconsistent code.
A good practice is to create your own abstraction and wrap the calls to the library API into your wrapper. This will allow you to keep code more consistent, and also more easily switch to a different library/package in the future if you will have to. This is because you wrap all the calls to the third-party library in one place and as long as your wrapper interface does not change, you will be able to just replace the implementation of your wrapper methods to switch to the new library.
Let’s Code!
In the specific case of code used to make Http request, we can create a an interface called IHttpClient and then a class called HttpClient that will implement such an interface. Inside our HttpClient methods we will invoke axios methods.
For now, let’s just pretend our HttpClient will have only a generic get and post methods:
export interface IHttpClient {
get<T>(parameters: IHttpClientRequestParameters): Promise<T>
post<T>(parameters: IHttpClientRequestParameters): Promise<T>
}
We will need also an interface to pass our parameters to the get and post with one argument to avoid code cluttering. We’ll call this IHttpClientRequestParameters and it will take some a generic T to define the type of the payload passed in (if any) and additional common parameters that are used for any kind of Http request:
export interface IHttpClientRequestParameters<T> {
url: string
requiresToken: boolean
payload?: T
}
Here is the description in details of each property of our request parameters interface:
- url: this is the full url of the API endpoint to which we need to make the request (it will have to include the query string parameters)
- requiresToken: this is a boolean that indicates if our request will have to also add an authentication token (i.e. Jwt token)
- payload: this is the payload for the POST or PUT request so it is optional. Now we can code our HttpClient class implementing our IHttpClient interface and using axios. Let’s start by importing the things we need from axios and write the initial declaration of our HttpClient class without any implementation yet:
import axios, { AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios'
export class HttpClient implements IHttpClient {
// ... implementation code will go here
}
Let’s now add the implementation of the get method as defined in our interface. Notice here I am using Promise syntax, but you are welcome to just use async/await syntax. Our get method will take an instance of IHttpClientRequestParameters and return a Promise:
get<T>(parameters: IHttpClientRequestParameters): Promise<T> {
return new Promise<T>((resolve, reject) => {
// extract the individual parameters
const { url, requiresToken } = parameters
// axios request options like headers etc
const options: AxiosRequestConfig = {
headers: {}
}
// if API endpoint requires a token, we'll need to add a way to add this.
if (requiresToken) {
const token = this.getToken()
options.headers.RequestVerificationToken = token
}
// finally execute the GET request with axios:
axios
.get(url, options)
.then((response: any) => {
resolve(response.data as T)
})
.catch((response: any) => {
reject(response)
})
})
}
Similarly, now add the implementation of the post method as defined in our interface. Like get, our post method will take an instance of IHttpClientRequestParameters and return a Promise:
post<T>(parameters: IHttpClientRequestParameters): Promise<T> {
return new Promise<T>((resolve, reject) => {
const { url, payload, requiresToken } = parameters
// axios request options like headers etc
const options: AxiosRequestConfig = {
headers: {}
}
// if API endpoint requires a token, we'll need to add a way to add this.
if (requiresToken) {
const token = this.getToken()
options.headers.RequestVerificationToken = token
}
// finally execute the GET request with axios:
axios
.post(url, payload, options)
.then((response: any) => {
resolve(response.data as T)
})
.catch((response: any) => {
reject(response)
})
})
}
Now you can export an instance of the HttpClient and use throughout your code:
export const httpClient = new HttpClient()
Here is an example where we consume it to fetch a list of “items” of type IItem:
fetchItems(): Promise<IItem[]> {
// prepare our request parameters
const getParameters: IHttpClientPostParameters = {
url: 'http://yourapiurl/items',
requiresToken: false
}
// just return httpClient.get (which is a promise) or again use async/await if you prefer
return httpClient.get<IItem[]>(getParameters)
}
Conclusion
You now have an HttpClient that abstract the different Http request like get or post etc and encapsulate the code in one place. Within the HttpClient you use axios, but later might switch to another library for any reason in the future, and you are not polluting your code with axios methods.
Originally published on my blog here ScalingVue.com
Top comments (4)
Hey Damiano,
many thanks for this Tutorial. I'am new @ typescript and don't know how to use that Code with a POST-Method in a Service-Class.
So i have to send FormData:
and my service:
Nice!
Just one doubt. Why don't you simply return the axios promise in each method instead putting it into a new Promise constructor?
To avoid coupling with axios. That is the purpose of the post.
I think he was talking about the implementation. The implementation is already coupled with axios. Can you explain why e.g. you didn't just return the promise returned by
axios.get
in your implementation of the get method?