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

@codihausvn/fetchpipe

v0.1.0

Published

Composable, zero-dependency API client SDK foundation

Readme

fetchpipe

Composable, zero-dependency API client foundation. Build type-safe clients for any REST API by plugging together small, focused modules.

Architecture

Why

Every new project, you rewrite the same fetch wrapper, token management, retry logic, error handling. Or you reach for axios — then wrap it with another layer for auth, interceptors, error formatting.

fetchpipe gives you a single composable foundation that works with any REST API. No framework lock-in, no backend assumptions.

Compose only what you need. No retry? Don't add it. Need session auth instead of bearer? Swap the plugin. Each plugin is independent and tree-shakeable.

Type-safe by design. Each .with() accumulates types through TypeScript intersections — your IDE knows exactly what methods are available based on which plugins you composed. No casting, no as any.

Commands separate "what" from "when". Define requests as reusable thunks, decorate them without mutation, execute when ready. Build an SDK for your team's API in minutes.

Runs everywhere. Uses platform globals (fetch, URL) — browser, Node, Deno, Bun, React Native. Inject a mock fetch for tests.

| | raw fetch | axios | fetchpipe | |---|---|---|---| | Auth management | DIY | Interceptor | Built-in plugins | | Retry | DIY | DIY | Built-in plugin | | Type inference | No | No | Auto via compose | | Bundle size | 0 | ~13KB | ~3KB | | Extensible | Hard | Interceptors | Plugin system |

Install

npm install fetchpipe

Quick Start

import { createClient, rest, bearerAuth, type Command } from 'fetchpipe'

const api = createClient('https://api.example.com')
  .with(rest())
  .with(bearerAuth('my-token'))

const getUsers = (): Command<User[]> => () => ({
  path: '/users',
  method: 'GET',
})

const users = await api.request(getUsers())

Plugins

rest(config?)

Core plugin. Adds .request(command).

createClient(url).with(rest({
  extractResponse: 'json',            // return parsed JSON (default)
  extractResponse: 'wrapped:data',    // unwrap { data: ... }
  extractResponse: 'raw',             // return raw Response
  extractResponse: (res) => { ... },  // custom extractor
  credentials: 'include',
  onRequest: (init) => init,          // global request hook
  onResponse: (data, init) => data,   // global response hook
}))

bearerAuth(token)

Static or dynamic bearer token.

// Static
.with(bearerAuth('my-api-key'))

// Dynamic — called on every request
.with(bearerAuth(async () => await getTokenFromVault()))

// Methods: api.getToken(), api.setToken(token)

sessionAuth(config?)

Full auth lifecycle — login, auto-refresh, logout. All paths configurable.

.with(sessionAuth({
  loginPath: '/auth/login',
  refreshPath: '/auth/refresh',
  logoutPath: '/auth/logout',
  autoRefresh: true,
  msRefreshBeforeExpires: 30000,
  storage: memoryStorage(),       // or custom AuthStorage
}))

await api.login({ email: '[email protected]', password: 'secret' })
await api.request(protectedCommand())  // token auto-attached
await api.logout()

retry(config?)

Exponential backoff. Must compose after rest().

.with(retry({
  maxRetries: 3,       // default
  baseDelay: 300,      // ms, default
  maxDelay: 10000,     // ms, default
  retryOn: (error, attempt) => error.status >= 500,
}))

logger(config?)

Request/response logging. Must compose after rest().

.with(logger({ logRequest: true, logResponse: true, logErrors: true }))

Command Decorators

import { withHeaders, withToken, withOptions, endpoint } from 'fetchpipe'

withHeaders(cmd, { 'X-Custom': 'value' })    // inject headers
withToken(cmd, 'override-token')              // override auth
withOptions(cmd, init => ({ ...init, signal: AbortSignal.timeout(5000) }))
endpoint({ path: '/raw', method: 'POST' })   // command from raw options

Plugin Composition

const api = createClient(url)
  .with(rest())            // innermost — actual fetch
  .with(bearerAuth(token)) // any order — discovered via duck typing
  .with(retry())           // wraps .request()
  .with(logger())          // wraps outermost (onion model)

Rules:

  1. rest() first — provides .request()
  2. Auth plugins — any position, adds getToken() discovered at runtime
  3. Wrapping plugins — after rest(), last .with() wraps outermost

Custom Plugins

const timing = () => <Schema>(client: ApiClient<Schema>) => {
  const original = (client as any).request
  return {
    async request<T>(cmd: Command<T>): Promise<T> {
      const t = performance.now()
      try { return await original.call(this, cmd) }
      finally { console.log(`${(performance.now() - t).toFixed(1)}ms`) }
    },
  }
}

Platform Globals

createClient(url, {
  globals: { fetch: customFetch, URL: customURL, logger: customLogger },
})

Error Handling

import { isApiError } from 'fetchpipe'

try {
  await api.request(cmd())
} catch (err) {
  if (isApiError(err)) {
    err.message   // first error message
    err.status    // HTTP status
    err.errors    // error details array
    err.response  // raw Response
  }
}

Real-World Examples

// Mattermost — raw JSON responses
const mm = createClient('https://mm.example.com/api/v4')
  .with(rest({ extractResponse: 'json' }))
  .with(bearerAuth(token))

// Laravel — wrapped { data: ... }
const api = createClient('https://app.example.com/api')
  .with(rest({ extractResponse: 'wrapped:data' }))
  .with(sessionAuth({ credentials: 'include' }))
  .with(retry({ maxRetries: 2 }))

// Internal microservice
const svc = createClient('http://user-service.internal:3000')
  .with(rest())
  .with(bearerAuth(async () => await getServiceToken()))
  .with(retry({ maxRetries: 5, retryOn: (err) => err.status === 503 }))
  .with(logger())

API Reference

| Export | Type | Description | |--------|------|-------------| | createClient(url, opts?) | Factory | Create base client | | rest(config?) | Plugin | REST transport — .request() | | bearerAuth(token) | Plugin | Static/dynamic bearer auth | | sessionAuth(config?) | Plugin | Login/refresh/logout lifecycle | | retry(config?) | Plugin | Exponential backoff retry | | logger(config?) | Plugin | Request/response logging | | memoryStorage() | Utility | In-memory token storage | | endpoint(opts) | Helper | Command from raw options | | withHeaders(cmd, h) | Decorator | Add headers | | withToken(cmd, t) | Decorator | Override auth token | | withOptions(cmd, fn) | Decorator | Transform RequestInit | | ApiError | Class | Structured API error | | isApiError(err) | Guard | Type guard for ApiError |

Requirements

  • Node.js >= 18 (or any runtime with fetch + URL)
  • TypeScript >= 5.0

License

MIT