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 🙏

© 2026 – Pkg Stats / Ryan Hefner

x-fetch

v0.3.0

Published

A simple but elegant `fetch` API wrapper with less than `830B` minified and brotlied, use `fetch` like a charm

Downloads

1,534

Readme

x-fetch

GitHub Actions Workflow Status Codecov type-coverage CodeRabbit Pull Request Reviews npm GitHub Release

Conventional Commits Renovate enabled JavaScript Style Guide Code Style: Prettier changesets

A simple but elegant fetch API wrapper with less than 830B minified and brotlied, use fetch like a charm

TOC

Usage

Install

# npm
npm i x-fetch

# yarn
yarn add x-fetch

# pnpm
pnpm add x-fetch

# bun
bun add x-fetch

API

import {
  HttpMethod,
  createXFetch,
  isXFetchError,
  xfetch,
  middlewares,
  type XFetchMiddleware,
} from 'x-fetch'

// plain url, GET method, returns JSON
await xfetch('url')

// with options — method, body, query, headers, etc.
await xfetch('url', {
  method: HttpMethod.POST, // or 'POST'
  // plain object or array (auto JSON-stringified), or BodyInit
  body: {},
  // URLSearchParams-compatible query object
  query: {
    key: 'value',
  },
  // type: 'arrayBuffer' | 'blob' | 'json' | 'text' | null, // defaults to 'json'
})

// --- Middlewares ---

const middleware: XFetchMiddleware = async (ctx, next) => {
  // mutate ctx before the request
  ctx.headers.set('Authorization', 'Bearer token')
  const res = await next() // same as next(ctx)
  // mutate / inspect the response
  return res
}

// global — applies to all xfetch calls
middlewares.use(middleware)
middlewares.eject(middleware)

// per-call — merged with global middlewares (global → per-call → fetch)
await xfetch('url', { middlewares: [middleware] })

// ctx is mutable — middleware can override type, url, etc.
const forceText: XFetchMiddleware = async (ctx, next) => {
  ctx.type = 'text'
  return next()
}

// --- Common recipes ---

// Base URL
const withBaseUrl: XFetchMiddleware = async (ctx, next) => {
  ctx.url = new URL(ctx.url, 'https://api.example.com').href
  return next()
}

// Retry
const retry: XFetchMiddleware = async (ctx, next) => {
  try {
    return await next()
  } catch {
    return next() // reuses ctx, mutations from failed attempt persist
  }
}

// Retry with snapshot — restore original context on retry
const retryWithSnapshot: XFetchMiddleware = async (ctx, next) => {
  const snapshot = { ...ctx, headers: new Headers(ctx.headers) }
  try {
    return await next()
  } catch {
    return next(snapshot) // fresh context, no leaked mutations
  }
}

// Timeout
const withTimeout: XFetchMiddleware = async (ctx, next) => {
  ctx.signal = AbortSignal.timeout(5000)
  return next()
}

// Cache
const cache = new Map<string, Response>()
const withCache: XFetchMiddleware = async (ctx, next) => {
  const key = ctx.url
  if (cache.has(key)) {
    return cache.get(key)!.clone()
  }
  const res = await next()
  cache.set(key, res.clone())
  return res
}

// Upload / download progress — extend context with custom callbacks
declare module 'x-fetch' {
  interface XFetchMiddlewareContext {
    onUploadProgress?: (loaded: number, total: number) => void
    onDownloadProgress?: (loaded: number, total: number) => void
  }
}

const trackProgress = (
  reader: ReadableStreamDefaultReader<Uint8Array>,
  total: number,
  onProgress: (loaded: number, total: number) => void,
) =>
  new ReadableStream({
    start(controller) {
      let loaded = 0
      function pump() {
        reader.read().then(
          ({ done, value }) => {
            if (done) {
              controller.close()
              return
            }
            loaded += value!.length
            onProgress(loaded, total)
            controller.enqueue(value!)
            pump()
          },
          err => controller.error(err),
        )
      }
      pump()
    },
  })

const withUploadProgress: XFetchMiddleware = async (ctx, next) => {
  if (ctx.onUploadProgress && ctx.body instanceof ReadableStream) {
    const total = Number(ctx.headers.get('Content-Length') || 0)
    ctx.body = trackProgress(ctx.body.getReader(), total, ctx.onUploadProgress)
  }
  return next()
}

const withDownloadProgress: XFetchMiddleware = async (ctx, next) => {
  if (!ctx.onDownloadProgress) {
    return next()
  }
  const res = await next()
  const reader = res.clone().body?.getReader()
  if (!reader) {
    return res
  }
  const total = Number(res.headers.get('Content-Length') || 0)
  return new Response(trackProgress(reader, total, ctx.onDownloadProgress), res)
}

await xfetch('/api', {
  onUploadProgress: (loaded, total) => console.log(`↑ ${loaded}/${total}`),
  middlewares: [withUploadProgress],
})

// XHR-based fetch — native progress via init callbacks
declare global {
  interface RequestInit {
    onUploadProgress?: (loaded: number, total: number) => void
    onDownloadProgress?: (loaded: number, total: number) => void
  }
}

const xhrFetch: typeof fetch = (input, init) =>
  new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    const url = input instanceof Request ? input.url : String(input)
    xhr.open(init?.method || 'GET', url)
    new Headers(init?.headers).forEach((v, k) => xhr.setRequestHeader(k, v))
    const { onUploadProgress, onDownloadProgress, signal, ...rest } = init || {}
    xhr.upload.onprogress = e => onUploadProgress?.(e.loaded, e.total)
    xhr.onprogress = e => onDownloadProgress?.(e.loaded, e.total)
    signal?.addEventListener('abort', () => xhr.abort())
    xhr.onload = () =>
      resolve(
        new Response(xhr.response, {
          status: xhr.status,
          statusText: xhr.statusText,
        }),
      )
    xhr.onerror = () => reject(new Error('XHR failed'))
    xhr.send((rest as RequestInit).body as XMLHttpRequestBodyInit)
  })

const { xfetch } = createXFetch(xhrFetch)

await xfetch('/file', {
  onUploadProgress: (loaded, total) => console.log(`↑ ${loaded}/${total}`),
  onDownloadProgress: (loaded, total) => console.log(`↓ ${loaded}/${total}`),
})

// or — middleware that bypasses next() for native XHR progress events
const withXhrProgress: XFetchMiddleware = (ctx, next) =>
  !ctx.onUploadProgress && !ctx.onDownloadProgress
    ? next()
    : new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest()
        xhr.open(ctx.method, ctx.url)
        ctx.headers.forEach((v, k) => xhr.setRequestHeader(k, v))
        xhr.upload.onprogress = e => ctx.onUploadProgress?.(e.loaded, e.total)
        xhr.onprogress = e => ctx.onDownloadProgress?.(e.loaded, e.total)
        xhr.onload = () =>
          resolve(
            new Response(xhr.response, {
              status: xhr.status,
              statusText: xhr.statusText,
            }),
          )
        xhr.onerror = () => reject(new Error('XHR failed'))
        xhr.send(ctx.body as XMLHttpRequestBodyInit)
      })

await xfetch('/upload', {
  onUploadProgress: (loaded, total) => console.log(`${loaded}/${total}`),
  middlewares: [retry, withXhrProgress], // withXhrProgress last — innermost
})

// next() always throws XFetchError, inspect and handle in middleware
const withFallback: XFetchMiddleware = async (ctx, next) => {
  try {
    return await next()
  } catch (error) {
    if (isXFetchError(error) && error.response?.status === 404) {
      return new Response('{"error":"not found"}', { status: 200 })
    }
    throw error
  }
}

// call site — non-HTTP errors are also wrapped as XFetchError
try {
  await xfetch('url')
} catch (error) {
  if (isXFetchError(error)) {
    console.error(error.message, error.response?.status, error.data)
  }
}

// --- Isolated instance ---

const { xfetch, middlewares } = createXFetch() // or createXFetch(customFetch)

Migration Guides

From axios

// axios
const { data } = await axios.get('/users', { params: { page: 1 } })
await axios.post('/users', { name: 'John' })

// x-fetch
const data = await xfetch('/users?page=1')
await xfetch('/users', { method: 'POST', body: { name: 'John' } })

| axios | x-fetch | | ----------------------------------- | ------------------------------ | | axios.create({ baseURL }) | middlewares.use(withBaseUrl) | | axios.interceptors.request.use() | middlewares.use() | | axios.interceptors.response.use() | middlewares.use() | | error.response?.status | error.response?.status |

From ky

// ky
await ky.get('/users', { searchParams: { page: 1 } }).json()
await ky.post('/users', { json: { name: 'John' } })

// x-fetch
await xfetch('/users', { query: { page: 1 } })
await xfetch('/users', { method: 'POST', body: { name: 'John' } })

| ky | x-fetch | | -------------------------- | ----------------------------------------- | | ky.create({ prefixUrl }) | middlewares.use(withBaseUrl) | | ky.extend({ hooks }) | createXFetch() with middlewares.use() | | HTTPError | XFetchError |

From ofetch

// ofetch
await $fetch('/users', { query: { page: 1 } })
const api = $fetch.create({ baseURL: 'https://api.example.com' })

// x-fetch
await xfetch('/users', { query: { page: 1 } })
const { xfetch, middlewares } = createXFetch()
middlewares.use(withBaseUrl)
await xfetch('/users', { query: { page: 1 } })

| ofetch | x-fetch | | ------------------------------------ | ------------------------------ | | $fetch.create({ baseURL }) | middlewares.use(withBaseUrl) | | $fetch.create({ onRequest }) | middlewares.use() | | $fetch.create({ onResponse }) | middlewares.use() | | $fetch.create({ onResponseError }) | try/catch in middleware |

Sponsors and Backers

Sponsors and Backers

Sponsors

| 1stG | RxTS | UnTS | | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | 1stG Open Collective sponsors | RxTS Open Collective sponsors | UnTS Open Collective sponsors |

Backers

| 1stG | RxTS | UnTS | | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | 1stG Open Collective backers | RxTS Open Collective backers | UnTS Open Collective backers |

Changelog

Detailed changes for each release are documented in CHANGELOG.md.

License

MIT © JounQin@1stG.me