How to fetch data from a REST API in Typescript using Axios
Writing a wrapper around Axios that is properly typed and easy to use.
Introdution
In plain javascript, making a get request with Axios to an API is straightforward all you need to do is:
const axios = require('axios').default;
// Make a request for a user with a given ID
axios.get('/user?ID=12345')
.then(function (response) {
// handle success
console.log(response);
})
.catch(function (error) {
// handle error
console.log(error);
})
.then(function () {
// always executed
});
But when working with Typescript, you have a few more issues to worry about.
- How do we define the type/shape of the data we receive from our REST API?
- How can we ensure that only a certain type/shape of data is passed in the body of my request?
Both problems can be solved by introducing a wrapper around Axios that is properly typed and easy to use.
A wrapper here means a class or function you use to access Axios. Generally for third-party packages or anything I npm install. If I'm going to use them in many places in my application, I wrap them in a class so that for any reason I want to change them I don't have to change my whole codebase.
First, Create your Axios instance.
Of course, we have to install Axios
npm install axios
Looking at the Axios documentation we see that we can create an Axios instance with certain configurations such as the baseURL
:
// Api.ts
import axios from 'axios';
export const API = axios.create({
baseURL: 'https://some-domain.com/api/',
});
Secondly, We define a class to wrap around Axios.
I call it the Service
class. It is where the magic happens. The class basically exposes the axios methods we would be using in our application. It also makes use of Typescript generics to enforce our preferred return type.
A simple service class looks like this:
// Service.ts
import { AxiosRequestConfig } from "axios";
import { API } from "./Api";
export function getToken(value: string): void {
return localStorage.getItem("token")
}
export class Service<PrimaryType, PayloadType = Record<string, unknown>> {
url: string;
axiosConfig = {} as AxiosRequestConfig;
constructor(url: string, useAuth = false, secret: string = getToken()) {
this.url = url;
if (useAuth) {
this.axiosConfig = {
...this.axiosConfig,
headers: {
Authorization: `Bearer ${secret}`
}
};
}
}
async getOne(id: number | string, queryString = ""): Promise<PrimaryType> {
const response = await API.get(
`${this.url}/${id}${queryString ? "?" + queryString : ""}`,
this.axiosConfig
);
return response.data;
}
async getAll(queryString = ""): Promise<PrimaryType[]> {
const response = await API.get(
`${this.url}/${queryString ? "?" + queryString : ""}`,
this.axiosConfig
);
return response.data;
}
async create(payload: PayloadType): Promise<PrimaryType> {
const response = await API.post(`${this.url}`, payload, this.axiosConfig);
return response.data;
}
async update(
id: number | string,
payload: PayloadType
): Promise<PrimaryType> {
const response = await API.patch(
`${this.url}/${id}`,
payload,
this.axiosConfig
);
return response.data;
}
async delete(id: number | string): Promise<void> {
const response = await API.delete(`${this.url}/${id}`, this.axiosConfig);
return response.data;
}
async getPaginated(queryString = ""): Promise<PaginationResponse<PrimaryType>> {
const response = await API.get(
`${this.url}/${queryString ? "?" + queryString : ""}`,
this.axiosConfig
);
return response.data;
}
}
Not quite simple yeah ๐, Don't worry I'll explain:
The url
property
The URL you want to send a request to is stored here.
The axiosConfig
property
You can set up a few default Axios configurations here. And it can be modified to meet the needs of your request at any time.
The constructor
This is where we define how we initialize the class. It does a few things:
- It accepts the URL you what to send the request to, and store it in the
url
property. - It accepts an optional boolean value to determine whether to send an authorization token or not.
- It also accepts a third optional value in case you what to use a secret key that is not stored in the local storage (or your default location).
- it sets up the
axiosConfig
to be ready for an authenticated request or non-authenticated request.
The getAll
method
The getAll
method is used to send a GET
request for a list of items, like when you are expecting an array of products from the API
The getOne
method
It is used to send a GET
request when you are expecting a single-item response from the API
The create
method
Used to send an Axios POST
request to the API. The create
method assumes that the return type is a single item. You may also decide to call it post
instead of create
The update
method
This method is used to send a PATCH
request to the API. It also expects to return a single item.
The delete
method
This method, as the name implies sends a DELETE
request.
The getPaginated
method
This method is unique because we use the power of Typescript generics to get a paginated list of data. Assuming you are expecting a list of paginated items like:
{
"data": [
...
],
"total": 200,
"pages": 20,
"page": 2,
"next": 3,
"previous": 1,
"limit": 10
}
The typescript interface representation of the above JSON object is:
interface PaginationResponse<T> {
data: T[];
total: number;
pages: number;
page: number;
next: number;
previous: number;
limit: number;
}
Now no matter what the shape/type the data I'm fetching is I can have a steady expectation of what my paginated response looks like.
Quick tip: I use app.quicktype.io to convert JSON objects to typescript interfaces quickly.
The getToken
function
The goal of this function is to get the authorization token from wherever it is stored. It may not be local storage for you.
Using the Service Class
Let's assume we are working with an API that allows us to create, read update, and delete products. The product has the following Typescript interface:
interface Product {
name: string;
price: number;
description: string;
userId: number;
createdAt: Date;
updatedAt: Date;
}
interface CreateProductPayload {
name: string;
price: number;
description: string;
}
Basically, to use the service class we need to initialize it like so:
// Without authentication.
const productService = new Service<Product>('/products')
// With authentication
const productService = new Service<Product>('/products', true)
// With payload type.
const productService = new Service<Product, CreateProductPayload>('/products', true)
You can then make an API call with the service when you need to in your application.
// Get a list of products.
productService.getAll()
// Get a single product with id = 233
productService.getOne(233)
// Get the paginated list of products.
productService.getPaginated("limit=10&page=2")
// Create a product.
productService.create({
name: "Chocolates ๐ซ",
price: 2300,
description: "8 bars of chocolate in on pack.",
})
// update a product with id = 234
productService.update(234, {
name: "Chocolate Bars ๐ซ",
price: 2300,
description: "8 bars of chocolate in on pack.",
})
// Delete a product with id = 233
productService.delete(233)
In Conclusion
The service class can be modified to suit your needs or your API specifications. You can also add other methods as you see fit.
I hope it helps you and saves you a lot of stress when working with Typescript and Axios.
Odabo (till we meet again).๐๐ผ
โ Your lady who writes code.