How to fetch data from a REST API in Typescript using Axios

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.

ยท

6 min read

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?

I do have an Idea that will solve all our problems

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).๐Ÿ‘‹๐Ÿผ Bye

โ€” Your lady who writes code.