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

@eugustavo/fergus

v0.1.0

Published

A modern, fully typesafe HTTP client built on top of fetch.

Readme

Fergus

"man of strength" — A modern, fully typesafe HTTP client built on top of fetch.

Fergus is a next-generation HTTP client designed to be the Axios 2.0 you always wanted: familiar API, zero dependencies, native fetch, and a type system that actually works for you.

import { createFergus } from '@eugustavo/fergus'

const api = createFergus({ baseURL: 'https://api.example.com', timeout: 5000 })

const { data, error } = await api.get<User[]>('/users')

Why Fergus?

| Feature | fetch | Axios | Fergus | |---|---|---|---| | Bundle size | 0KB (native) | ~13KB min+gzip | < 4KB min+gzip | | Runtime dependencies | 0 | 2+ | 0 | | TypeScript generics | Manual casting | Partial | Full end-to-end | | { data, error } destructuring | ❌ | ❌ | | | Axios-compatible response.data | ❌ | ✅ | | | Built-in retry w/ exponential backoff | ❌ | ❌ | | | Request deduplication | ❌ | ❌ | | | Timeout via AbortController | Manual | ✅ | | | Middleware pipeline (Koa-style) | ❌ | ❌ | | | Interceptors (Axios-compatible) | ❌ | ✅ | | | Shallow config merge (perf) | — | Deep merge | Shallow + headers only | | Tree-shakeable | — | Partial | ✅ (sideEffects: false) | | Native fetch (no XMLHttpRequest) | ✅ | ❌ | | | Strongly typed errors | ❌ | Partial | | | Request cancellation | Manual | ✅ | | | Node.js support | 18+ | All | 18+ |


Installation

npm install @eugustavo/fergus
# or
pnpm add @eugustavo/fergus
# or
yarn add @eugustavo/fergus

Requires Node.js >= 18 (native fetch support)


Quick Start

import { createFergus } from '@eugustavo/fergus'

const api = createFergus({
  baseURL: 'https://api.example.com',
  timeout: 5000,
  retry: 2,
})

const { data, error } = await api.get<User[]>('/users')

if (error) {
  console.error(error.message)
  return
}

console.log(data) // User[]

Response API

Fergus returns a FergusResult<T> object with data, error, status, and headers:

const { data, error, status, headers } = await api.get<User[]>('/users')

if (error) {
  console.error(error.message)
  return
}

console.log(data)    // User[]
console.log(status)  // 200

You can also access properties directly without destructuring:

const response = await api.get<User[]>('/users')
console.log(response.data)   // User[]
console.log(response.status) // 200

HTTP Methods

// GET
const { data: users } = await api.get<User[]>('/users')
const { data: user } = await api.get<User>('/users/1')

// POST
const { data: created } = await api.post<User>('/users', { name: 'Alice' })

// PUT
const { data: updated } = await api.put<User>('/users/1', { name: 'Alice Updated' })

// PATCH
const { data: patched } = await api.patch<User>('/users/1', { name: 'Alice' })

// DELETE
const { error } = await api.delete('/users/1')

// HEAD / OPTIONS
await api.head('/users')
await api.options('/users')

Configuration

Instance Defaults

const api = createFergus({
  baseURL: 'https://api.example.com',
  timeout: 5000,
  retry: 2,
  dedupe: true,
  headers: {
    'Accept': 'application/json',
  },
})

// Mutate defaults after creation (Axios-compatible)
api.defaults.timeout = 10_000
api.defaults.baseURL = 'https://v2.api.example.com'

Per-Request Config

const { data } = await api.get('/users', {
  params: { page: 1, limit: 20 },
  timeout: 3000,
  headers: { 'X-Custom': 'value' },
})

Query Params

// Appended automatically — no manual string building
const { data } = await api.get('/users', {
  params: { page: 1, limit: 20, active: true },
})
// → GET /users?page=1&limit=20&active=true

Retry

// Simple: number of attempts
const api = createFergus({ retry: 3 })

// Advanced: full config
const api = createFergus({
  retry: {
    attempts: 3,
    baseDelay: 300,       // ms before first retry
    maxDelay: 5000,       // cap on delay
    retryOn: [408, 429, 500, 502, 503, 504],
    retryOnNetworkError: true,
  },
})

Retry uses exponential backoff with ±10% jitter to avoid thundering herd:

attempt 0 → delay ~300ms
attempt 1 → delay ~600ms
attempt 2 → delay ~1200ms
...capped at maxDelay

Never retries: canceled requests, timeouts, or 4xx client errors (unless explicitly listed in retryOn).


Request Deduplication

const api = createFergus({ dedupe: true })

// These two fire simultaneously — only ONE fetch is made
const [r1, r2] = await Promise.all([
  api.get('/users'),
  api.get('/users'),
])

// Both receive the same result

Only deduplicates GET requests. POST/PUT/PATCH/DELETE are always executed independently.


Interceptors

// Request interceptor — add auth header
api.interceptors.request.use(async (config) => {
  const token = await getToken()
  return {
    ...config,
    headers: { ...config.headers, Authorization: `Bearer ${token}` },
  }
})

// Response interceptor — transform data
api.interceptors.response.use((result) => {
  return { ...result, data: transform(result.data) }
})

// Eject an interceptor
const id = api.interceptors.request.use(myFn)
api.interceptors.request.eject(id)

// Clear all interceptors
api.interceptors.request.clear()

Error Handling

const { data, error } = await api.get('/users')

if (error) {
  if (error.isNetworkError) {
    // No response received — DNS failure, connection refused, etc.
  }

  if (error.isTimeout) {
    // Request exceeded the configured timeout
  }

  if (error.isCanceled) {
    // Request was aborted via AbortSignal
  }

  if (error.status === 401) {
    // HTTP error with status code
    console.log(error.data) // Parsed response body
  }
}

Type Guards

import { FergusError } from '@eugustavo/fergus'

FergusError.isFergusError(err)  // true for any FergusError
FergusError.isNetworkErr(err)   // true for network errors
FergusError.isTimeoutErr(err)   // true for timeout errors
FergusError.isCanceledErr(err)  // true for canceled requests

Request Cancellation

const controller = new AbortController()

const { data, error } = await api.get('/users', {
  signal: controller.signal,
})

// Cancel from anywhere
controller.abort()

// Check if canceled
if (error?.isCanceled) {
  console.log('Request was canceled')
}

Response Types

// JSON (default)
const { data: json } = await api.get<MyType>('/data')

// Text
const { data: text } = await api.get<string>('/readme', { responseType: 'text' })

// Blob
const { data: blob } = await api.get<Blob>('/image.png', { responseType: 'blob' })

// ArrayBuffer
const { data: buffer } = await api.get<ArrayBuffer>('/file', { responseType: 'arrayBuffer' })

// FormData
const { data: form } = await api.get<FormData>('/form', { responseType: 'formData' })

TypeScript

Fergus is built TypeScript-first. All generics flow through end-to-end:

interface User {
  id: number
  name: string
  email: string
}

// data is User | null — no casting needed
const { data, error } = await api.get<User>('/users/1')

if (data) {
  console.log(data.name) // ✅ fully typed
}

// Error data is also typed
const { error: err } = await api.post<User, { message: string }>('/users', payload)
if (err?.data) {
  console.log(err.data.message) // ✅ typed error body
}

Advanced: Custom Middleware

import { createFergus } from '@eugustavo/fergus'
import type { Middleware } from '@eugustavo/fergus'

const loggingMiddleware: Middleware = async (ctx, next) => {
  const start = Date.now()
  await next()
  const duration = Date.now() - start
  console.log(`${ctx.config.method} ${ctx.config.url} — ${duration}ms`)
}

License

MIT