It started with a Cloudflare Worker.
I had a simple task, hit an external API from a Worker, return the data. I reached for axios like I always do. Deployed it. Got this:
Error: Cannot read properties of undefined (reading 'prototype')
axios doesn't run on Cloudflare Workers. It depends on Node.js built-ins that the edge doesn't have. Fine. I'll switch.
The search for an alternative
Got — huge ecosystem, great docs. Dropped CommonJS in v12. My project uses require(). Hard pass.
Ky — clean API, zero deps. Browser-first. No Node.js support, no proxy, can't use it on the server side of my stack.
node-fetch — a polyfill. Node.js has had native fetch since v18. Why am I installing a polyfill for something that already exists?
redaxios — tiny, but it's just a thin axios-compatible wrapper around fetch. No retries, no interceptors, no auth helpers.
ofetch — closest to what I wanted, but no request deduplication, no in-memory cache, and the TypeScript types felt incomplete.
Every option had a wall. Edge runtime. CommonJS. Missing features. Another plugin needed.
So I built @firekid/hurl
Built on native fetch. Zero dependencies. Works on Node.js 18+, Cloudflare Workers, Vercel Edge, Deno, and Bun — no adapters, no polyfills, no config.
npm install @firekid/hurl
That Cloudflare Worker that broke with axios? Here's what it looks like now:
import hurl from '@firekid/hurl'
export default {
async fetch(request) {
const res = await hurl.get('https://api.example.com/data', {
retry: 3,
timeout: 5000,
auth: { type: 'bearer', token: ENV.API_TOKEN },
})
return new Response(JSON.stringify(res.data))
}
}
It just works. No error. No wall.
What it ships with out of the box
Everything I kept reaching for plugins to do — built in, zero extra installs:
Retries with exponential backoff
await hurl.get('/flaky-endpoint', {
retry: {
count: 4,
backoff: 'exponential',
on: [429, 500, 502, 503],
}
})
In-memory caching with TTL
// Won't hit the network again for 60 seconds
await hurl.get('/config', { cache: { ttl: 60000 } })
Request deduplication
// 10 components call this simultaneously — only 1 network request fires
await hurl.get('/user', { deduplicate: true })
Typed errors — no more optional chaining into error.response
import hurl, { HurlError } from '@firekid/hurl'
try {
await hurl.get('/protected')
} catch (err) {
if (err instanceof HurlError) {
err.status // 401
err.data // parsed error body
err.retries // how many times it retried before throwing
err.type // 'HTTP_ERROR' | 'TIMEOUT_ERROR' | 'ABORT_ERROR'
}
}
Interceptors
hurl.interceptors.request.use((url, options) => ({
url,
options: {
...options,
headers: { ...options.headers, 'x-trace-id': crypto.randomUUID() },
},
}))
Isolated instances
const api = hurl.create({
baseUrl: 'https://api.example.com',
auth: { type: 'bearer', token: process.env.TOKEN },
retry: 3,
})
const adminApi = api.extend({ headers: { 'x-role': 'admin' } })
Migration from axios is 2 lines
// Before
import axios from 'axios'
const res = await axios.get('/users')
// After
import hurl from '@firekid/hurl'
const res = await hurl.get('/users')
res.data, res.status, res.headers — same shape. It maps over cleanly.
The numbers
| hurl | axios | got | ky | |
|---|---|---|---|---|
| Bundle size | ~9KB | ~35KB | ~45KB | ~5KB |
| Edge runtime | ✅ | ❌ | ❌ | ✅ |
| Node.js + CJS | ✅ | ✅ | ❌ | ❌ |
| Built-in retries | ✅ | ❌ | ✅ | ✅ |
| In-memory cache | ✅ | ❌ | ❌ | ❌ |
| Deduplication | ✅ | ❌ | ❌ | ❌ |
| Zero dependencies | ✅ | ❌ | ❌ | ✅ |
It's live
I published it. 437 downloads in the first week, zero marketing. Just devs hitting the same walls I hit.
If you're on Cloudflare Workers, Vercel Edge, or just tired of the axios plugin ecosystem — give it a shot.
npm install @firekid/hurl
Star it if it saves you a headache. Open an issue if it doesn't. 🔥
Built by Firekid
Top comments (0)