Let’s say you’re writing a frontend for an online store. You would have to make requests to get the shopping cart, add items to the cart, get product details, search for a product, list all products, etc.

If you’re directly calling fetch or axios in your code, they would look something like this.

async function getAllProducts ( ) { const headers = { "x-secret-header" : "ssshhh!" } ; const response = await fetch ( "/api/products" , { headers } ) ; const body = await response . json ( ) ; return body ; }

async function addToCart ( itemId , qty ) { const headers = { "x-secret-header" : "ssshhh!" } ; const payload = JSON . stringify ( { itemId , qty } ) ; const response = await fetch ( "/api/cart" , { method : "POST" , headers , body : payload } ) ; const body = await response . json ( ) ; return body ; }

This is totally fine if you’re only making a handful of requests, but if your codebase makes a lot of HTTP requests, it might be better to abstract them into their own modules instead of calling them directly.

What is an API module?

An API module is just a JS module that contains HTTP logic organized by business domain. For the online store example, the business domains would be Cart , Search , Inventory , Product Catalog , Order , etc. The respective API modules would be CartApi , SearchApi , InventoryApi , CatalogApi and OrderApi .

Let’s rewrite the above code using the API module pattern by creating CartApi and CatalogApi modules.

export async function getAllProducts ( ) { const headers = { "x-secret-header" : "ssshhh!" } ; const response = await fetch ( "/api/products" , { headers } ) ; const body = await response . json ( ) ; return body ; }

export async function addToCart ( itemId , qty ) { const headers = { "x-secret-header" : "ssshhh!" } ; const payload = JSON . stringify ( { itemId , qty } ) ; const response = await fetch ( "/api/cart" , { method : "POST" , headers , body : payload } ) ; const body = await response . json ( ) ; return body ; }

Now we can import these into the UI modules:

import * as CatalogApi from "../api/CatalogApi" ; CatalogApi . getAllProducts ( ) ;

import * as CartApi from "../api/CartApi" ; CartApi . addToCart ( itemId , qty ) ;

Benefits

Improve readability and testability by abstracting the HTTP code away from the UI or business code

One place to make modifications like renaming payload structure, query param names etc

One place to massage response into something that’s useful for the other parts of the codebase

Organizing HTTP code by business domain thereby improving code discoverability by new members of the team

Colocate custom app status codes sent by the server

HttpClient

If we see that there’s a lot of common code used in API modules, we can add one more layer called HttpClient or ApiClient to keep them DRY. The common code can be things like:

Adding extra headers

Logging

Using right config for production, development etc — hostnames, headers, etc

Session handling etc

export default async function request ( path , method , body ) { const headers = { } ; const url = "" ; const response = await fetch ( url , { method , body , headers } ) ; return response ; }

We can now update our API modules to use HttpClient:

import * as HttpClient from "./HttpClient" ; export function getAllProducts ( ) { return HttpClient . request ( "/api/products" ) ; }

import * as HttpClient from "./HttpClient" ; export function addToCart ( itemId , qty ) { return HttpClient . request ( "/api/cart" , "POST" , { itemId , qty } ) ; }

Thanks for reading! :)