Why You Need an API Layer and How To Build It in React

<iframe src="https://codesandbox.io/embed/why-you-need-an-api-layer-and-how-to-build-it-in-react-33y1i1?fontsize=14&hidenavigation=1&theme=dark"
style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
title="Why You Need an API Layer and How To Build It in React"
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
></iframe>

What is an API layer?

<endpointType>API.js
apis
├── configs
│ ├── axiosConfig.js
│ └── axiosUtils.js

├── AuthorAPI.js
.
.
.
├── ProductAPI.js
.
.
.
└── UserAPI.js
import React, { useEffect, useState } from "react"
import { ProductAPI } from "src/apis/ProductAPI"
function ProductListComponent(props) {
const [products, setProducts] = useState([])
useEffect(() => {
ProductAPI.getAll().then((products) => {
// response handling
setProducts(products)
})
}, [])
// presenting the product list ...
return <div>...</div>
}
export default ProductListComponent

Why your architecture needs an API layer

1. Avoiding code duplication

// the logic required to call the API to retrieve the list of all products
// with the axios HTTP client
axios.request({
method: "GET",
url: `/api/v1/products/`
})

2. Centralizing API requests in the same place

// src/apis/configs/axiosConfigs.jsimport axios from 'axios';// initializing the axios instance with custom configs
const api = axios.create({
withCredentials: true,
// adding a custom language header
headers: {
"Custom-Language": "en",
},
});
export default api;

3. Easily handle request cancellation

function getProducts(cancel = false) {
// ...
}

Implement an API Layer in React withaxios

Prerequisites

npm install axios

Defining the API layer with cancellation

// src/apis/configs/axiosConfigs.jsimport axios from "axios"export const api = axios.create({
withCredentials: true,
baseURL: "https://yourdomain.com/api/v1",
})
// defining a custom error handler for all APIs
const errorHandler = (error) => {
const statusCode = error.response?.status
// logging only errors that are not 401
if (statusCode && statusCode !== 401) {
console.error(error)
}
return Promise.reject(error)
}
// registering the custom error handler to the
// "api" axios instance
api.interceptors.response.use(undefined, (error) => {
return errorHandler(error)
})
// src/apis/configs/axiosUtils.jsexport function defineCancelApiObject(apiObject) {
// an object that will contain a cancellation handler
// associated to each API property name in the apiObject API object
const cancelApiObject = {}
// each property in the apiObject API layer object
// is associated with a function that defines an API call
// this loop iterates over each API property name
Object.getOwnPropertyNames(apiObject).forEach((apiPropertyName) => {
const cancellationControllerObject = {
controller: undefined,
}
// associating the request cancellation handler with the API property name
cancelApiObject[apiPropertyName] = {
handleRequestCancellation: () => {
// if the controller already exists,
// canceling the request
if (cancellationControllerObject.controller) {
// canceling the request and returning this custom message
cancellationControllerObject.controller.abort()
}
// generating a new controller
// with the AbortController factory
cancellationControllerObject.controller = new AbortController()
return cancellationControllerObject.controller
},
}
})
return cancelApiObject
}
// src/apis/productAPI.jsimport { api } from "./configs/axiosConfigs"
import { defineCancelApiObject } from "./configs/axiosUtils"
export const ProductAPI = {
get: async function (id, cancel = false) {
const response = await api.request({
url: `/products/:id`,
method: "GET",
// retrieving the signal value by using the property name
signal: cancel ? cancelApiObject[this.get.name].handleRequestCancellation().signal : undefined,
})
// returning the product returned by the API
return response.data.product
},
getAll: async function (cancel = false) {
const response = await api.request({
url: "/products/",
method: "GET",
signal: cancel ? cancelApiObject[this.getAll.name].handleRequestCancellation().signal : undefined,
})
return response.data.products
},
search: async function (name, cancel = false) {
const response = await api.request({
url: "/products/search",
method: "GET",
params: {
name: name,
},
signal: cancel ? cancelApiObject[this.search.name].handleRequestCancellation().signal : undefined,
})
return response.data.products
},
create: async function (product, cancel = false) {
await api.request({
url: `/products`,
method: "POST",
data: product,
signal: cancel ? cancelApiObject[this.create.name].handleRequestCancellation().signal : undefined,
})
},
}
// defining the cancel API object for ProductAPI
const cancelApiObject = defineCancelApiObject(ProductAPI)

API Layer in action

git clone https://github.com/Tonel/api-layer-example-semaphore
cd api-layer-example-semaphore
npm install
npm start

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Semaphore

Semaphore

Supporting developers with insights and tutorials on delivering good software. · https://semaphoreci.com