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

@bylg/fetcher

v0.3.2

Published

A small wrapper around fetch and Zod. It allows you to predefine headers and context and have a type-safe response.

Downloads

231

Readme

BYLG Fetcher

BYLG Fetcher is a small wrapper around fetch and Zod. It allows you to predefine headers and context and have a type-safe response.

A FetcherClient instance provides two methods, fetcher and combineFetchers, that are used to define and combine fetcher functions respectively. A fetcher function could be an asynchronous function that makes requests to a remote server and returns a typed response.

Installation

npm install @bylg/fetcher
yarn add @bylg/fetcher
pnpm add @bylg/fetcher

Zod is a peer dependency, so you will need to install that as well.

npm install zod
yarn add zod
pnpm add zod

Quick Start

You can create a new instance of the FetcherClient by passing it a context schema and headers schema. You can then use the fetcher and combineFetchers methods to define and combine fetcher functions respectively.

import { FetcherClient } from "@bylg/fetcher";
import { z } from "zod";

const f = new FetcherClient({
  ctx: z.string().url(), // Define the context schema
  headers: {
    Authorization: z.string().min(1), // Define the headers schema
  },
});

// Define a fetcher function
const getTodo = f.fetcher(({ ctx, headers, get, post }, id: string) => {
  const url = `${ctx}/todos/${id}`; // Use the context
  const schema = z.string(); // Define the response schema
  const options = {}; // Define the request options. Headers are automatically added.
  return get(url, schema, options);
});

// Define a fetcher function
const createTodo = f.fetcher(({ ctx, post }, content: string) => {
  const url = `${ctx}/todos`; // Use the context
  const schema = z.object({
    id: z.string(),
    content: z.string(),
  }); // Define the response schema
  const data = { content }; // Define the data to be sent in the request body
  const options = {}; // Define the request options. Headers are automatically added.
  return post(url, schema, data, options);
});

// Combine the fetcher functions
const createTodoFetcher = f.combineFetchers({
  getTodo,
  createTodo,
});

// Define the context and headers
// Alternatively, you can use a function that returns the context and headers
const todoFetcher = createTodoFetcher({
  ctx: "https://example.com", // The type of ctx is inferred from the context schema
  headers: { Authorization: "some-token" }, // The type of headers is inferred from the headers schema
});

// Call the fetcher functions
const todo = await todoFetcher.getTodo("1");
//                                 ^?  (property) getTodo: (input: string) => Promise<string>
const newTodo = await todoFetcher.createTodo({ content: "test" });
//                                   ^?  (property) createTodo: (input: string) => Promise<{ id: string; content: string; }>

Above code defines context and headers schemas and creates a FetcherClient instance with them. Two fetcher functions are defined using the fetcher method, and then combined using the combineFetchers method.

Now you can use it in React Query:

import { useQuery } from "react-query";
import { todoFetcher } from "path/to/todoFetcher";

const Todo = ({ id }) => {
  const { data, isLoading, error } = useQuery(["todo", id], () =>
    todoFetcher.getTodo(id)
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return <div>{data}</div>;
};
import { useMutation } from "react-query";
import { todoFetcher } from "path/to/todoFetcher";

const TodoForm = () => {
  const [content, setContent] = useState("");
  const { mutate, isLoading, error } = useMutation(todoFetcher.createTodo);

  const handleSubmit = () => {
    e.preventDefault();
    mutate({ content });
  };

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return (
    <form onSubmit={handleSubmit}>
      <input value={content} onChange={(e) => setContent(e.target.value)} />
      <button type="submit">Create</button>
    </form>
  );
};

Using postProcess to refresh token

const f = new FetcherClient({
  ctx: z.object({
    url: z.string().url(),
    refreshToken: z.string(),
  }),
  headers: {
    Authorization: z.string().min(1),
  },
});

const getTodo = f.fetcher(async ({ ctx, get }, id: string) => {
  const { data, response } = await get(
    `${ctx.url}/todos/${id}`,
    z.union([z.object({ code: z.string() }), z.string()])
  );
  if (
    response.status === 401 &&
    typeof data === "object" &&
    "code" in data &&
    data.code === "token-expired"
  ) {
    // throw error to trigger onError
    throw new Error(data.code);
  }
  return { data, response };
});

const createFetchers = f.combineFetchers({
  getTodo,
});

let token = "old-token";
const refreshTokensRetryCounts = new Map<string, number>();

const fetchers = createFetchers(() => ({
  ctx: { url: "https://example.com", refreshToken: "refresh-token" },
  headers: { Authorization: token },
  postProcess: {
    // when your fetcher throw an error, it will call this function with the error and original fetcher function and input
    onError: async ({ options, input, error, fetcher, fetcherName }) => {
      if (refreshTokensRetryCounts.get(fetcherName) === 2) {
        throw new Error(`Refresh token failed when calling: ${fetcherName}`);
      }
      // if the error is token expired, we will refresh the token and retry the original fetcher
      if (error instanceof Error && error.message === "token-expired") {
        refreshTokensRetryCounts.set(
          fetcherName,
          (refreshTokensRetryCounts.get(fetcherName) ?? 0) + 1
        );
        // get new token and update token
        token = await refreshTokens(options.ctx.refreshToken);
        await fetcher(options, input as any);
      }
      return;
    },
  },
}));

await fetchers.getTodo("1");