Organizing HTTP requests using the API module pattern
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.
// HomePage.js
async function getAllProducts() {
const headers = { "x-secret-header": "ssshhh!" };
const response = await fetch("/api/products", { headers });
const body = await response.json();
return body;
}
// CartPage.js
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.
// CatalogApi.js
export async function getAllProducts() {
const headers = { "x-secret-header": "ssshhh!" };
const response = await fetch("/api/products", { headers });
const body = await response.json();
return body;
}
// CartApi.js
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:
// HomePage.js
import * as CatalogApi from "../api/CatalogApi";
CatalogApi.getAllProducts();
// CartPage.js
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
// HttpClient
export default async function request(path, method, body) {
const headers = {}; // add custom headers here
const url = ""; // add logic to get correct url for the environment
const response = await fetch(url, { method, body, headers });
// add response handling
// - session management
// - convert to correct data type - response.json(), etc
// - handle common errors like 401, 403 etc
return response;
}
We can now update our API modules to use HttpClient:
// CatalogApi.js
import * as HttpClient from "./HttpClient";
export function getAllProducts() {
return HttpClient.request("/api/products");
}
// CartApi.js
import * as HttpClient from "./HttpClient";
export function addToCart(itemId, qty) {
return HttpClient.request("/api/cart", "POST", { itemId, qty });
}
Thanks for reading! :)