Like most full stack javascript developers, I have come to love the web Fetch API. It is a simple and elegant way to make http requests but the Fetch API lacks some features (and rightly so) that are essential for making http requests in a production environment. Here are some of those features:
In this post, I will show how to use the decorator pattern to enhance the Fetch API and add some of these features without modifying the Fetch API itself.
What is a decorator?
A decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it. They key point is that a decorator function returns a modified version of the original function.
Logging decorator
The simplest decorator to implement is the logging decorator. It simply logs the url and the response of the http request. Here is the code:
type Fetch = (...args: any) => Promise<Response>;
interface Logger {
log: (message: string) => void;
info: (message: string) => void;
error: (message: string) => void;
warn: (message: string) => void;
}
export const withLogger = (fetch: (...args: any) => Promise<Response>, logger: Logger = console): Fetch => {
return async (...args) => {
const [input] = args;
const response = await fetch(...args);
if (typeof input === "string") {
logger.info(`fetching ${input} and got ${response.status}`);
}
else if (input instanceof URL) {
logger.info(`fetching ${input} and got ${response.status}`);
}
else {
logger.info(`fetching ${input.url} and got ${response.status}`);
}
return response;
}
}
Now we can use the withLogger decorator to log the url and the response of the http request.
// This behaves exactly like the fetch api
const fetchWithLogger = withLogger(fetch);
// This will log the url and the response of the http request
fetchWithLogger("https://jsonplaceholder.typicode.com/todos/1");
Retry decorator
A more useful decorator is the retries decorator. It retries the http request if it fails. Here is the code:
interface RetryOptions {
retryCount: number
retryDelay?: number
retryOn?: number[]
}
export const withRetry = (fetch: Fetch, options: RetryOptions) => {
const { retryCount = 1, retryDelay = 1000, retryOn = [500] } = options;
let response: Response;
return async (...args: any): Promise<Response> => {
let retry = 0;
while (retry < retryCount) {
response = await fetch(...args);
if (retryOn.includes(response.status)) {
retry++;
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
else {
return response;
}
}
return response;
}
}
Now we can use the withRetry
decorator to retry the http request if it fails.
// This behaves exactly like the fetch api
const fetchWithRetry = withRetry(fetch, { retryCount: 3 });
// This will retry the http request if it fails
fetchWithRetry("https://jsonplaceholder.typicode.com/todos/1");
Chaining decorators
One of the advantages of using a decorator is that they can be chained together to even further extend the behavior of a function. This is called composition. Here is an example of how to use the withLogger
and withRetry
decorators together.
const fetchWithRetryAndLogger = withLogger(withRetry(fetch, { retryCount: 3 }));
// This will retry the http request if it fails and log the url and the response of the http request
fetchWithRetryAndLogger("https://jsonplaceholder.typicode.com/todos/1");
This github repository contains the full code for the decorators discussed in this post.
Conclusion
The decorator pattern is a simple and elegant way to extend the behavior of a function without explicitly modifying it. It is a powerful tool in the hands of a developer and can be used to solve a lot of problems. In this post, I have shown how to use the decorator pattern to enhance the Fetch API and add some features that are essential for making http requests in a production environment.
Top comments (0)