npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

return-fetch

v0.4.6

Published

A very light and simple library to extend fetch by implementing request, response interceptors.

Downloads

4,983

Readme

import returnFetch from "return-fetch";

const fetchExtended = returnFetch({
  baseUrl: "https://jsonplaceholder.typicode.com",
  headers: {Accept: "application/json"},
  interceptors: {
    request: async (args) => {
      console.log("********* before sending request *********");
      console.log("url:", args[0].toString());
      console.log("requestInit:", args[1], "\n\n");
      return args;
    },

    response: async (requestArgs, response) => {
      console.log("********* after receiving response *********");
      console.log("url:", requestArgs[0].toString());
      console.log("requestInit:", requestArgs[1], "\n\n");
      return response;
    },
  },
});

fetchExtended("/todos/1", {method: "GET"})
  .then((it) => it.text())
  .then(console.log);

Output

********* before sending request *********
url: https://jsonplaceholder.typicode.com/todos/1
requestInit: { method: 'GET', headers: { Accept: 'application/json' } } 


********* after receiving response *********
url: https://jsonplaceholder.typicode.com/todos/1
requestInit: { method: 'GET', headers: { Accept: 'application/json' } } 


{
  "userId": 1,
  "id": 1,
  "title": "delectus aut autem",
  "completed": false
}

Background

The Next.js framework(which I love so much) v13 App Router uses its own fetch implementation that extends node-fetch to do server side things like caching. I was accustomed to using Axios for API calls, but I have felt that now is the time to replace Axios with fetch finally. The most disappointing aspect I found when trying to replace Axios with fetch was that fetch does not have any interceptors. I thought surely someone must have implemented it, so I searched for libraries. However, there was no library capable of handling various situations, only one that could add a single request and response interceptors to the global fetch. That is the reason why I decided to implement it myself.

Philosophy

In implementing the fetch interceptors, I considered the following points:

  1. Minimalistic. I decided to only implement the following additional functions and not others:
    1. Implementing request and response interceptors
    2. Specifying a baseUrl
    3. Setting a default header
  2. No peer dependencies. I decided not to use any other libraries. I would like to keep the library as light as possible and running on any execution environments which have fetch (e.g. Node.js, Web Browsers, React Native, Web Workers).
  3. It should be easy to add interceptors because return-fetch will provide minimal functionality. Users should be able to extend fetch as they wish.
  4. Codes to add interceptors should be reusable and able to maintain the Single Responsibility Principle (SRP), and it should be possible to combine interceptors that adhere to the SRP.
  5. Liskov Substitution Principle (LSP) should be maintained. The fetch function created by return-fetch should be able to replace the standard fetch function anywhere without any problems.

Good Things

  • Superlight bundle size < 1KB.
  • No peer dependencies.
  • No side effects. Pure functional.
  • No classes. Just a function.
  • Recursive type definition to chain functions infinitely.
  • Any execution environment having fetch, possible for any fetch polyfills.
  • 100% TypeScript.
  • 100% test coverage.

Installation

Package Manager

Via npm

npm install return-fetch

Via yarn

yarn add return-fetch

Via pnpm

pnpm add return-fetch

<script> tag

<!--
  Pick your favourite CDN:
    - https://unpkg.com/return-fetch
    - https://cdn.jsdelivr.net/npm/return-fetch
    - https://www.skypack.dev/view/return-fetch
    - …
-->

<!-- UMD import as window.returnFetch -->
<script src="https://unpkg.com/return-fetch"></script>

<!-- Modern import -->
<script type="module">
    import returnFetch from 'https://cdn.skypack.dev/return-fetch/dist/index.js'

    // ... //
</script>

Demo

Run on Stickblitz.

Types

https://return-fetch.myeongjae.kim/docs/types/ReturnFetchDefaultOptions.html

Usage

#1. Display/Hide loading indicator

import returnFetch, { ReturnFetch } from "return-fetch";
import { displayLoadingIndicator, hideLoadingIndicator } from "@/your/adorable/loading/indicator";

// Write your own high order function to display/hide loading indicator
const returnFetchWithLoadingIndicator: ReturnFetch = (args) => returnFetch({
  ...args,
  interceptors: {
    request: async (args) => {
      setLoading(true);
      return args;
    },
    response: async (requestArgs, response) => {
      setLoading(false);
      return response;
    },
  },
})

// Create an extended fetch function and use it instead of the global fetch.
export const fetchExtended = returnFetchWithLoadingIndicator({
  // default options
});

//////////////////// Use it somewhere ////////////////////
fetchExtended("/sample/api");

#2. Throw an error if a response status is greater than or equal to 400

import returnFetch, { ReturnFetch } from "return-fetch";

// Write your own high order function to throw an error if a response status is greater than or equal to 400.
const returnFetchThrowingErrorByStatusCode: ReturnFetch = (args) => returnFetch({
  ...args,
  interceptors: {
    response: async (_, response) => {
      if (response.status >= 400) {
        throw await response.text().then(Error);
      }

      return response;
    },
  },
})

// Create an extended fetch function and use it instead of the global fetch.
export const fetchExtended = returnFetchThrowingErrorByStatusCode({
  // default options
});

//////////////////// Use it somewhere ////////////////////
fetchExtended("/sample/api/400").catch((e) => alert(e.message));

#3. Serialize request body and deserialize response body

You can import below example code because it is published as a separated npm package: return-fetch-json

import returnFetch, { FetchArgs, ReturnFetchDefaultOptions } from "return-fetch";

// Use as a replacer of `RequestInit`
type JsonRequestInit = Omit<NonNullable<FetchArgs[1]>, "body"> & { body?: object };

// Use as a replacer of `Response`
export type ResponseGenericBody<T> = Omit<
  Awaited<ReturnType<typeof fetch>>,
  keyof Body | "clone"
> & {
  body: T;
};

export type JsonResponse<T> = T extends object
  ? ResponseGenericBody<T>
  : ResponseGenericBody<string>;


// this resembles the default behavior of axios json parser
// https://github.com/axios/axios/blob/21a5ad34c4a5956d81d338059ac0dd34a19ed094/lib/defaults/index.js#L25
const parseJsonSafely = (text: string): object | string => {
  try {
    return JSON.parse(text);
  } catch (e) {
    if ((e as Error).name !== "SyntaxError") {
      throw e;
    }

    return text.trim();
  }
};

// Write your own high order function to serialize request body and deserialize response body.
export const returnFetchJson = (args?: ReturnFetchDefaultOptions) => {
  const fetch = returnFetch(args);

  return async <T>(
    url: FetchArgs[0],
    init?: JsonRequestInit,
  ): Promise<JsonResponse<T>> => {
    const response = await fetch(url, {
      ...init,
      body: init?.body && JSON.stringify(init.body),
    });

    const body = parseJsonSafely(await response.text()) as T;

    return {
      headers: response.headers,
      ok: response.ok,
      redirected: response.redirected,
      status: response.status,
      statusText: response.statusText,
      type: response.type,
      url: response.url,
      body,
    } as JsonResponse<T>;
  };
};

// Create an extended fetch function and use it instead of the global fetch.
export const fetchExtended = returnFetchJson({
  // default options
});

//////////////////// Use it somewhere ////////////////////
export type ApiResponse<T> = {
  status: number;
  statusText: string;
  data: T;
};

fetchExtended<ApiResponse<{ message: string }>>("/sample/api/echo", {
  method: "POST",
  body: {message: "Hello, world!"}, // body should be an object.
}).then(it => it.body);

#4. Compose above three high order functions to create your awesome fetch 🥳

Because of the recursive type definition, you can chain extended returnFetch functions as many as you want. It allows you to write extending functions which are responsible only for a single feature. It is a good practice to stick to the Single Responsibility Principle and writing a reusable function to write clean code.

import {
  returnFetchJson,
  returnFetchThrowingErrorByStatusCode,
  returnFetchWithLoadingIndicator
} from "@/your/customized/return-fetch";

/*
  Compose high order functions to create your awesome fetch.
   1. Add loading indicator.
   2. Throw an error when a response's status code is equal to 400 or greater.
   3. Serialize request body and deserialize response body as json and return it.
*/
export const fetchExtended = returnFetchJson({
  fetch: returnFetchThrowingErrorByStatusCode({
    fetch: returnFetchWithLoadingIndicator({
      // default options
    }),
  }),
});

//////////////////// Use it somewhere ////////////////////
fetchExtended("/sample/api/echo", {
  method: "POST",
  body: {message: "this is an object of `ApiResponse['data']`"}, // body should be an object.
}).catch((e) => alert(e.message));

#5. Use any fetch implementation

fetch has been added since Node.js v17.5 as an experimental feature, also available from Node.js v16.15 and still experimental(29 JUL 2023, v20.5.0). You can use 'node-fetch' as a polyfill for Node.js v16.15 or lower, or 'whatwg-fetch' for old web browsers, or 'cross-fetch' for both web browser and Node.js.

Next.js has already included 'node-fetch' and they extend it for server-side things like caching.

Whatever a fetch you use, you can use return-fetch as long as the fetch you use is compatible with the Fetch API.

#5-1. node-fetch

I implemented a simple proxy for https://postman-echo.com with node-fetch as an example using Next.js route handler.

// src/app/sample/api/proxy/postman-echo/node-fetch/[[...path]]/route.ts
import { NextRequest } from "next/server";
import nodeFetch from "node-fetch";
import returnFetch, { ReturnFetchDefaultOptions } from "return-fetch";

process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"; // to turn off SSL certificate verification on server side
const pathPrefix = "/sample/api/proxy/postman-echo/node-fetch";

export async function GET(request: NextRequest) {
  const {nextUrl, method, headers} = request;

  const fetch = returnFetch({
    // Use node-fetch instead of global fetch
    fetch: nodeFetch as ReturnFetchDefaultOptions["fetch"],
    baseUrl: "https://postman-echo.com",
  });

  const response = await fetch(nextUrl.pathname.replace(pathPrefix, ""), {
    method,
    headers,
  });

  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers,
  });
}

Send a request to the proxy route.

import {
  returnFetchJson,
  returnFetchThrowingErrorByStatusCode,
  returnFetchWithLoadingIndicator
} from "@/your/customized/return-fetch";

export const fetchExtended = returnFetchJson({
  fetch: returnFetchThrowingErrorByStatusCode({
    fetch: returnFetchWithLoadingIndicator({
      baseUrl: "https://return-fetch.myeongjae.kim",
    }),
  }),
});

//////////////////// Use it somewhere ////////////////////
fetchExtended(
  "/sample/api/proxy/postman-echo/node-fetch/get",
  {
    headers: {
      "X-My-Custom-Header": "Hello World!"
    }
  },
);

#5-2. whatwg-fetch

whatwg-fetch is a polyfill for browsers. I am going to send a request using whatwg-fetch to the proxy route.

import {
  returnFetchJson,
  returnFetchThrowingErrorByStatusCode,
  returnFetchWithLoadingIndicator
} from "@/your/customized/return-fetch";
import { fetch as whatwgFetch } from "whatwg-fetch";

export const fetchExtended = returnFetchJson({
  fetch: returnFetchThrowingErrorByStatusCode({
    fetch: returnFetchWithLoadingIndicator({
      fetch: whatwgFetch, // use whatwgFetch instead of browser's global fetch
      baseUrl: "https://return-fetch.myeongjae.kim",
    }),
  }),
});

//////////////////// Use it somewhere ////////////////////
fetchExtended(
  "/sample/api/proxy/postman-echo/node-fetch/get",
  {
    headers: {
      "X-My-Custom-Header": "Hello World!"
    }
  },
);

#5-3. cross-fetch

I implemented a simple proxy for https://postman-echo.com with cross-fetch as an example using Next.js route handler.

// src/app/sample/api/proxy/postman-echo/cross-fetch/[[...path]]/route.ts
import { NextRequest } from "next/server";
import crossFetch from "cross-fetch";
import returnFetch from "return-fetch";

process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"; // to turn off SSL certificate verification on server side
const pathPrefix = "/sample/api/proxy/postman-echo/cross-fetch";

export async function GET(request: NextRequest) {
  const {nextUrl, method, headers} = request;

  const fetch = returnFetch({
    fetch: crossFetch, // Use cross-fetch instead of built-in Next.js fetch
    baseUrl: "https://postman-echo.com",
  });

  const response = await fetch(nextUrl.pathname.replace(pathPrefix, ""), {
    method,
    headers,
  });

  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers,
  });
}

Send a request to the proxy route using cross-fetch on the client-side also.

import {
  returnFetchJson,
  returnFetchThrowingErrorByStatusCode,
  returnFetchWithLoadingIndicator
} from "@/your/customized/return-fetch";
import crossFetch from "cross-fetch";

export const fetchExtended = returnFetchJson({
  fetch: returnFetchThrowingErrorByStatusCode({
    fetch: returnFetchWithLoadingIndicator({
      fetch: crossFetch, // Use cross-fetch instead of browser's global fetch
      baseUrl: "https://return-fetch.myeongjae.kim",
    }),
  }),
});

//////////////////// Use it somewhere ////////////////////
fetchExtended(
  "/sample/api/proxy/postman-echo/node-fetch/get",
  {
    headers: {
      "X-My-Custom-Header": "Hello World!"
    }
  },
);

#5-4. Next.js built-in fetch

I implemented a simple proxy for https://postman-echo.com with Next.js built-in fetch as an example using Next.js route handler.

// src/app/sample/api/proxy/postman-echo/nextjs-fetch/[[...path]]/route.ts
import { NextRequest } from "next/server";
import returnFetch from "return-fetch";

process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"; // to turn off SSL certificate verification on server side
const pathPrefix = "/sample/api/proxy/postman-echo/nextjs-fetch";

export async function GET(request: NextRequest) {
  const {nextUrl, method, headers} = request;

  const fetch = returnFetch({
    // omit fetch option to use Next.js built-in fetch
    baseUrl: "https://postman-echo.com",
  });

  const response = await fetch(nextUrl.pathname.replace(pathPrefix, ""), {
    method,
    headers,
  });

  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers,
  });
}

Send a request to the proxy route using web browser default fetch.

import {
  returnFetchJson,
  returnFetchThrowingErrorByStatusCode,
  returnFetchWithLoadingIndicator
} from "@/your/customized/return-fetch";

export const fetchExtended = returnFetchJson({
  fetch: returnFetchThrowingErrorByStatusCode({
    fetch: returnFetchWithLoadingIndicator({
      baseUrl: "https://return-fetch.myeongjae.kim",
    }),
  }),
});

//////////////////// Use it somewhere ////////////////////
fetchExtended(
  "/sample/api/proxy/postman-echo/nextjs-fetch/get",
  {
    headers: {
      "X-My-Custom-Header": "Hello World!"
    }
  },
);

#5-5. React Native

(I have not written a documents for React Native yet, but it surely works with React Native becuase it does not have any dependencies on a specific fetch implementation.)

#6. Replace default fetch with your customized returnFetch

import {
  returnFetchJson,
  returnFetchThrowingErrorByStatusCode,
  returnFetchWithLoadingIndicator
} from "@/your/customized/return-fetch";

// save global fetch reference.
const globalFetch = fetch;
export const fetchExtended = returnFetchThrowingErrorByStatusCode({
  fetch: returnFetchWithLoadingIndicator({
    fetch: globalFetch, // use global fetch as a base.
  }),
});

// replace global fetch with your customized fetch.
window.fetch = fetchExtended;

//////////////////// Use it somewhere ////////////////////
fetch("/sample/api/echo", {
  method: "POST",
  body: JSON.stringify({message: "Hello, world!"})
}).catch((e) => alert(e.message));

#7. Use URL or Request object as a first argument of fetch

The type of a first argument of fetch is Request | string | URL and a second argument is RequestInit | undefined. Through above examples, we used a string as a first argument of fetch. return-fetch is also able to handle a Request or URL object as a first argument. It does not restrict any features of fetch.

Be careful! the default options' baseURL does not applied to a URL or Request object. Default headers are still applied to a Request object as you expected.

#7-1. URL object as a first argument

Even a baseUrl is set to 'https://google.com', it is not applied to a URL object. An URL object cannot be created if an argument does not have origin. You should set a full URL to a URL object, so a baseUrl will be ignored.

import returnFetch from "return-fetch";

const fetchExtended = returnFetch({
  baseUrl: "https://google.com",
  headers: {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-My-Header": "Hello, world!",
  },
});

fetchExtended(
  /*
    Even a baseURL is set to 'https://google.com', it is not applied to a URL object.
    An URL object cannot be created if an argument does not have origin.
    You should set a full URL to a URL object, so a baseURL will be ignored.
  */
  new Request(new URL("https://return-fetch.myeongjae.kim/sample/api/proxy/postman-echo/nextjs-fetch/post"), {
    method: "PUT",
    body: JSON.stringify({message: "overwritten by requestInit"}),
    headers: {
      "X-My-Headers-In-Request-Object": "Works well!",
    },
  }),
  {
    method: "POST",
    body: JSON.stringify({message: "Hello, world!"}),
  },
)
  .then((it) => it.json())
  .then(console.log);

#7-2. Request object as a first argument

Even a baseUrl is set to 'https://google.com', it is not applied to a Request object. While creating a Request object, an origin is set to 'https://return-fetch.myeongjae.kim', which is the origin of this page, so baseUrl will be ignored.

On Node.js, a Request object cannot be created without an origin, same as URL object.

import returnFetch from "return-fetch";

const fetchExtended = returnFetch({
  baseUrl: "https://google.com",
  headers: {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "X-My-Header": "Hello, world!",
  },
});

fetchExtended(
  /*
    Even a baseURL is set to 'https://google.com', it is not applied to a Request object.
    While creating a Request object, an origin is set to 'https://return-fetch.myeongjae.kim',
    which is the origin of this page, so baseURL will be ignored.
  */
  new Request("/sample/api/proxy/postman-echo/node-fetch/post", {
    method: "PUT",
    body: JSON.stringify({message: "overwritten by requestInit"}),
    headers: {
      "X-My-Headers-In-Request-Object": "Works well!",
    },
  }),
  {
    method: "POST",
    body: JSON.stringify({message: "Hello, world!"}),
  },
)
  .then((it) => it.json())
  .then(console.log);

#8. Retry a request

Interceptors are async functions. You can make an async call in interceptors. This example shows how to retry a request when a response status is 401.

let retryCount = 0;

const returnFetchRetry: ReturnFetch = (args) => returnFetch({
  ...args,
  interceptors: {
    response: async (response, requestArgs, fetch) => {
      if (response.status !== 401) {
        return response;
      }

      console.log("not authorized, trying to get refresh cookie..");
      const responseToRefreshCookie = await fetch(
        "https://httpstat.us/200",
      );
      if (responseToRefreshCookie.status !== 200) {
        throw Error("failed to refresh cookie");
      }

      retryCount += 1;
      console.log(`(#${retryCount}) succeeded to refresh cookie and retry request`);
      return fetch(...requestArgs);
    },
  },
});

const fetchExtended = returnFetchRetry({
  baseUrl: "https://httpstat.us",
});

fetchExtended("/401")
  .then((it) => it.text())
  .then((it) => `Response body: "${it}"`)
  .then(console.log)
  .then(() => console.log("\n Total counts of request: " + (retryCount + 1)))

#8-1. Doubling retry counts

If you nest returnFetchRetry, you can retry a request more than once. When you nest 4 times, you can retry a request 16 times (I know it is too much, but isn't it fun?).

let retryCount = 0;

// create a fetch function with baseUrl applied
const fetchBaseUrlApplied = returnFetch({baseUrl: "https://httpstat.us"});

const returnFetchRetry: ReturnFetch = (args) => returnFetch({
  ...args,
  // use fetchBaseUrlApplied as a default fetch function
  fetch: args?.fetch ?? fetchBaseUrlApplied,
  interceptors: {
    response: async (response, requestArgs, fetch) => {
      if (response.status !== 401) {
        return response;
      }

      console.log("not authorized, trying to get refresh cookie..");
      const responseToRefreshCookie = await fetch("/200");
      if (responseToRefreshCookie.status !== 200) {
        throw Error("failed to refresh cookie");
      }

      retryCount += 1;
      console.log(`(#${retryCount}) succeeded to refresh cookie and retry request`);
      return fetch(...requestArgs);
    },
  },
});

const nest = (
  remaining: number,
  providedFetch = fetchBaseUrlApplied,
): ReturnType<ReturnFetch> =>
  remaining > 0
    ? nest(remaining - 1, returnFetchRetry({fetch: providedFetch}))
    : providedFetch;

// nest 4 times -> 2^4 = 16
const fetchExtended = nest(4);

fetchExtended("/401")
  .then((it) => it.text())
  .then((it) => `Response body: "${it}"`)
  .then(console.log)
  .then(() => console.log("\n Total counts of request: " + (retryCount + 1)))

Derived Packages

  • return-fetch-json: A package that serialize request body object and deserialize response body as a JSON object.

License

MIT © Myeongjae Kim