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

@firekid/hurl

v1.1.0

Published

Zero-dependency HTTP client for Node.js and edge runtimes. Built on fetch. The modern replacement for axios, request, got, node-fetch, and ky. Works on Cloudflare Workers, Vercel Edge, Deno, and Bun.

Downloads

47

Readme

@firekid/hurl

npm version npm downloads npm bundle size TypeScript License: MIT CI GitHub stars Website

@firekid/hurl is a modern, zero-dependency HTTP client for Node.js 18+, Cloudflare Workers, Vercel Edge Functions, Deno, and Bun — built on native fetch with retries, interceptors, auth helpers, in-memory caching, request deduplication, and full TypeScript support.

npm install @firekid/hurl

Why hurl?

Most HTTP clients make you choose between features and bundle size, or between Node.js support and edge compatibility. @firekid/hurl does neither.

import hurl from '@firekid/hurl'

// Retry automatically on failure
const res = await hurl.get('https://api.example.com/users', { retry: 3 })

// Auth, timeout, caching — all in one call
const data = await hurl.get('/users', {
  auth: { type: 'bearer', token: process.env.API_TOKEN },
  timeout: 5000,
  cache: { ttl: 60000 },
})

// Parallel requests
const [users, posts] = await hurl.all([
  hurl.get('/users'),
  hurl.get('/posts'),
])

Comparison

| Feature | hurl | axios | ky | got | node-fetch | |---|:---:|:---:|:---:|:---:|:---:| | Zero dependencies | ✅ | ❌ | ✅ | ❌ | ✅ | | Bundle size | ~9KB | ~35KB | ~5KB | ~45KB | ~8KB | | Node.js 18+ | ✅ | ✅ | ✅ | ✅ | ✅ | | Cloudflare Workers | ✅ | ❌ | ✅ | ❌ | ❌ | | Vercel Edge | ✅ | ❌ | ✅ | ❌ | ❌ | | Deno / Bun | ✅ | ⚠️ | ✅ | ⚠️ | ❌ | | Built-in retries | ✅ | ❌ | ✅ | ✅ | ❌ | | Interceptors | ✅ | ✅ | ✅ | ❌ | ❌ | | Auth helpers | ✅ | ⚠️ | ❌ | ❌ | ❌ | | In-memory cache | ✅ | ❌ | ❌ | ❌ | ❌ | | Request deduplication | ✅ | ❌ | ❌ | ❌ | ❌ | | Upload progress | ✅ | ✅ | ❌ | ❌ | ❌ | | Download progress | ✅ | ✅ | ❌ | ❌ | ❌ | | Proxy support | ✅ | ✅ | ❌ | ✅ | ❌ | | CommonJS + ESM | ✅ | ✅ | ❌ | ❌ | ✅ | | TypeScript (built-in) | ✅ | ⚠️ | ✅ | ✅ | ⚠️ | | Throws on 4xx/5xx | ✅ | ✅ | ✅ | ✅ | ❌ |

✅ Full support   ⚠️ Partial / via plugin   ❌ Not supported


Installation

npm install @firekid/hurl
yarn add @firekid/hurl
pnpm add @firekid/hurl

Quick Start

import hurl from '@firekid/hurl'

const res = await hurl.get('https://api.example.com/users')

res.data        // parsed response body
res.status      // 200
res.headers     // Record<string, string>
res.requestId   // unique ID for this request
res.timing      // { start, end, duration }
res.fromCache   // boolean

HTTP Methods

hurl.get<T>(url, options?)
hurl.post<T>(url, body?, options?)
hurl.put<T>(url, body?, options?)
hurl.patch<T>(url, body?, options?)
hurl.delete<T>(url, options?)
hurl.head(url, options?)
hurl.options<T>(url, options?)
hurl.request<T>(url, options?)

Global Defaults

hurl.defaults.set({
  baseUrl: 'https://api.example.com',
  headers: { 'x-api-version': '2' },
  timeout: 10000,
  retry: 3,
})

hurl.defaults.get()
hurl.defaults.reset()

Authentication

// Bearer token
hurl.defaults.set({ auth: { type: 'bearer', token: 'my-token' } })

// Basic auth
hurl.defaults.set({ auth: { type: 'basic', username: 'admin', password: 'secret' } })

// API key (header)
hurl.defaults.set({ auth: { type: 'apikey', key: 'x-api-key', value: 'my-key' } })

// API key (query param)
hurl.defaults.set({ auth: { type: 'apikey', key: 'token', value: 'my-key', in: 'query' } })

Retry & Backoff

// Simple — retry 3 times with exponential backoff
await hurl.get('/users', { retry: 3 })

// Full config
await hurl.get('/users', {
  retry: {
    count: 3,
    delay: 300,
    backoff: 'exponential',
    on: [500, 502, 503],
  }
})

Retries are not triggered for abort errors. If no on array is provided, retries fire on network errors, timeout errors, and any 5xx status.


Timeout & Abort

await hurl.get('/users', { timeout: 5000 })

const controller = new AbortController()
setTimeout(() => controller.abort(), 3000)
await hurl.get('/users', { signal: controller.signal })

Interceptors

// Request interceptor
const remove = hurl.interceptors.request.use((url, options) => {
  return {
    url,
    options: {
      ...options,
      headers: { ...options.headers, 'x-trace-id': crypto.randomUUID() },
    },
  }
})
remove() // unregister

// Response interceptor
hurl.interceptors.response.use((response) => {
  console.log(response.status, response.timing.duration)
  return response
})

// Error interceptor
hurl.interceptors.error.use((error) => {
  if (error.status === 401) redirectToLogin()
  return error
})

// Clear all
hurl.interceptors.request.clear()
hurl.interceptors.response.clear()
hurl.interceptors.error.clear()

Caching

Caching applies to GET requests only. Responses are stored in memory with a TTL in milliseconds.

await hurl.get('/users', { cache: { ttl: 60000 } })
await hurl.get('/users', { cache: { ttl: 60000, key: 'all-users' } })
await hurl.get('/users', { cache: { ttl: 60000, bypass: true } })
import { clearCache, invalidateCache } from '@firekid/hurl'

// Clear the entire cache
clearCache()

// Invalidate a single entry by URL or custom key
invalidateCache('https://api.example.com/users')
invalidateCache('all-users') // if you used a custom cache key

Request Deduplication

When deduplicate is true and the same GET URL is called multiple times simultaneously, only one network request is made.

const [a, b] = await Promise.all([
  hurl.get('/users', { deduplicate: true }),
  hurl.get('/users', { deduplicate: true }),
])
// only one network request fired

Upload & Download Progress

// Upload
const form = new FormData()
form.append('file', file)

await hurl.post('/upload', form, {
  onUploadProgress: ({ loaded, total, percent }) => {
    console.log(`Uploading: ${percent}%`)
  }
})

// Download
await hurl.get('/large-file', {
  onDownloadProgress: ({ loaded, total, percent }) => {
    console.log(`Downloading: ${percent}%`)
  }
})

Proxy

Native fetch does not support programmatic proxy configuration out of the box. Proxy support depends on your Node.js version:

Node.js 18 — install undici@6 (v7 dropped Node 18 support), use ProxyAgent:

// npm install undici@6
import { ProxyAgent, setGlobalDispatcher } from 'undici'
setGlobalDispatcher(new ProxyAgent('http://proxy.example.com:8080'))

Node.js 20undici is bundled with ProxyAgent support:

import { ProxyAgent, setGlobalDispatcher } from 'undici'
setGlobalDispatcher(new ProxyAgent('http://proxy.example.com:8080'))

Node.js 22.3+ — supports EnvHttpProxyAgent which reads HTTP_PROXY/HTTPS_PROXY env vars automatically:

import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici'
setGlobalDispatcher(new EnvHttpProxyAgent())
// now set HTTP_PROXY=http://proxy.example.com:8080 in your env

Node.js 24+ — native fetch respects env vars when NODE_USE_ENV_PROXY=1 is set:

NODE_USE_ENV_PROXY=1 HTTP_PROXY=http://proxy.example.com:8080 node app.js

The proxy option in HurlRequestOptions is reserved for a future release where this will be handled automatically.



Parallel Requests

const [users, posts] = await hurl.all([
  hurl.get('/users'),
  hurl.get('/posts'),
])

Isolated Instances

const api = hurl.create({
  baseUrl: 'https://api.example.com',
  auth: { type: 'bearer', token: 'my-token' },
  timeout: 5000,
  retry: 3,
})

await api.get('/users')

// Extend with overrides
const adminApi = api.extend({
  headers: { 'x-role': 'admin' }
})

Error Handling

hurl throws a HurlError on HTTP errors (4xx/5xx), network failures, timeouts, aborts, and parse failures. It never resolves silently on bad status codes.

If you want to handle 4xx/5xx responses yourself without a try/catch, set throwOnError: false — the response will resolve normally and you can check res.status yourself.

const res = await hurl.get('/users', { throwOnError: false })
if (res.status === 404) {
  console.log('not found')
}
import hurl, { HurlError } from '@firekid/hurl'

try {
  await hurl.get('/users')
} catch (err) {
  if (err instanceof HurlError) {
    err.type        // 'HTTP_ERROR' | 'NETWORK_ERROR' | 'TIMEOUT_ERROR' | 'ABORT_ERROR' | 'PARSE_ERROR'
    err.status      // 404
    err.statusText  // 'Not Found'
    err.data        // parsed error response body
    err.headers     // response headers
    err.requestId   // same ID as the request
    err.retries     // number of retries attempted
  }
}

TypeScript

type User = { id: number; name: string }

const res = await hurl.get<User[]>('/users')
res.data // User[]

const created = await hurl.post<User>('/users', { name: 'John' })
created.data.id // number

Debug Mode

Logs the full request (method, url, headers, body, query, timeout, retry config) and response (status, timing, headers, data) to the console. Errors and retries are also logged.

await hurl.get('/users', { debug: true })

Response Shape

type HurlResponse<T> = {
  data: T
  status: number
  statusText: string
  headers: Record<string, string>
  requestId: string
  timing: {
    start: number
    end: number
    duration: number
  }
  fromCache: boolean
}

Request Options

type HurlRequestOptions = {
  method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
  headers?: Record<string, string>
  body?: unknown
  query?: Record<string, string | number | boolean>
  timeout?: number
  retry?: RetryConfig | number
  auth?: AuthConfig
  proxy?: ProxyConfig
  cache?: CacheConfig
  signal?: AbortSignal
  followRedirects?: boolean
  maxRedirects?: number
  onUploadProgress?: ProgressCallback
  onDownloadProgress?: ProgressCallback
  stream?: boolean
  debug?: boolean
  requestId?: string
  deduplicate?: boolean
}

Environment Support

@firekid/hurl runs anywhere the Fetch API is available. No adapters, no polyfills needed.

| Runtime | Support | |---|:---:| | Node.js 18+ | ✅ | | Cloudflare Workers | ✅ | | Vercel Edge Functions | ✅ | | Deno | ✅ | | Bun | ✅ |

Exports both ESM (import) and CommonJS (require).


Why Not Axios?

axios is 35KB, has no native edge runtime support, no built-in retry, no deduplication, and carries XMLHttpRequest baggage from a different era of the web.

got dropped CommonJS in v12 — if your project uses require(), you're stuck on an old version.

ky is browser-first. No Node.js, no proxy, no streaming.

node-fetch is a polyfill. Node.js has had native fetch since v18. You don't need it anymore.

request has been deprecated since 2020.

@firekid/hurl is built for how Node.js and the edge work today — native fetch, zero dependencies, everything included, works everywhere.


API Reference

| Method | Description | |---|---| | hurl.get(url, options?) | GET request → Promise<HurlResponse<T>> | | hurl.post(url, body?, options?) | POST request, body auto-serialized to JSON | | hurl.put(url, body?, options?) | PUT request | | hurl.patch(url, body?, options?) | PATCH request | | hurl.delete(url, options?) | DELETE request | | hurl.head(url, options?) | HEAD request → Promise<HurlResponse<void>> | | hurl.options(url, options?) | OPTIONS request | | hurl.request(url, options?) | Generic request, method from options | | hurl.all(requests) | Run requests in parallel | | hurl.create(defaults?) | New isolated instance | | hurl.extend(defaults?) | New instance inheriting current defaults | | hurl.defaults.set(defaults) | Set global defaults | | hurl.defaults.get() | Get current defaults | | hurl.defaults.reset() | Reset defaults to instance creation values | | hurl.interceptors.request.use(fn) | Register request interceptor | | hurl.interceptors.response.use(fn) | Register response interceptor | | hurl.interceptors.error.use(fn) | Register error interceptor | | clearCache() | Clear in-memory response cache |


Contributors

HeavstalTech HeavstalTech — signal fix, cache hardening, test suite


Built with ♥️ by Firekid · MIT License