So. The axios hack.
If you missed it quick recap, a week ago, the maintainer’s account got compromised. Hackers slipped in a postinstall script that quietly reached into machines and siphoned cloud credentials, API keys, and crypto wallets. The kind of breach that makes you stare at your node_modules folder with profound suspicion.
Now, I’m sitting there reading the incident report and something itches at the back of my brain.
Why does axios - a client-side HTTP library have any dependencies at all?
I went digging. And yeah. I was right. Axios ships with a non-trivial dependency tree, a postinstall surface, and ~14KB of gzipped bundle weight. For something that fundamentally just wraps fetch and http. That’s not a library. That’s a small town.
So I did what any reasonable developer does when they’re mildly annoyed at 11pm: I opened a new project folder and decided to fix it myself.
Day 1: “How Hard Can It Be”
(Famous last words. All great disasters start with these four words.)
I fired up Qwen Coder 3.6 — a model I’ve been putting through its paces lately and honestly? It’s become one of my favourite tools to work with. Different vibe from the usual suspects, really solid at systems-level reasoning. Full breakdown in a separate post coming soon, but the short version is: it can steer code the way you want it steered.
The pitch for kiattp was simple:
- Zero dependencies. Not “few dependencies.” Zero.
- No postinstall scripts. Ever.
- Drop-in axios replacement — so nobody has to refactor their entire codebase
- Fast. Like, embarrassingly fast compared to what we’ve been tolerating
Three days of steering, testing, profiling, and arguing with benchmarks later — we had something.
v0.1.0: It Worked. It Was Fast. I Was Briefly Smug.
The first version cleared 4,000 ops/sec against axios’s ~940 in MSW mock benchmarks. That’s roughly 4× faster. The bundle came in at ~3.7KB gzipped. No deps. No postinstall.
I celebrated for maybe twenty minutes.
Then I started stress-testing the axios compatibility layer.
The Bug That Humbled Me
Here’s the thing about drop-in replacements: users don’t tell you how they’re using the original. They just swap it in and expect everything to work. And if it doesn’t? They don’t file issues. They just quietly go back to the old library and never mention it.
So I was testing a project that used axios.create() with a baseURL, and I noticed something cursed:
const api = axios.create({ baseURL: 'https://api.example.com' });
// This worked fine ✅
api.get('users');
// This completely ignored baseURL ❌
api.get('/users');
The second call sent a request to /users. Just… bare /users. Into the void.
Turns out when the request path starts with /, my compatibility layer was treating it as an absolute path and skipping the baseURL concatenation entirely. Which is wrong. Which is how axios actually works. Which I did not implement correctly.
This is the part of the story where I remind you that axios has been battle-tested for years by millions of developers, and I had been working on my replacement for approximately 72 hours.
Humbling.
Fixing It (Without Wrecking Performance)
The fix sounds simple: if there’s a baseURL and the path starts with /, still prepend the base. Strip the trailing slash from baseURL, strip the leading slash from the path, join them cleanly.
function buildURL(base: string | undefined, path: string): string {
if (!base) return path;
const b = base.replace(/\/+$/, '');
const p = path.replace(/^\/+/, '');
return `${b}/${p}`;
}
Six lines. But the performance implications of where you do this normalization are non-trivial. URL construction sits in the hot path of every single request. Do it wrong and you’re allocating strings everywhere, blowing up the CPU cache, killing your ops/sec.
It took several iterations to land on an approach that kept performance at spec. The final numbers:
| ops/sec | Mean | p99 | Size (gzipped) | |
|---|---|---|---|---|
| kiattp | ~4,000 | 0.25ms | 0.80ms | ~3.7KB |
| axios | ~940 | 1.07ms | 4.83ms | ~14KB |
| native fetch | ~6,100 | 0.16ms | 0.32ms | 0KB |
Yes, native fetch is faster. It should be. It’s a browser primitive. The ~0.09ms overhead we add over fetch is the cost of config normalization, interceptors, error handling, and not having to write 50 lines of boilerplate per request. That’s a good trade.
What kiattp Actually Does
Beyond “axios but fast and small,” here’s what the library actually ships with:
Zero-dep, zero-drama installs:
npm install kiattp
# No postinstall. No audit warnings. No surprise scripts.
The same API you already know:
import { get, post, createInstance } from 'kiattp';
const users = await get<User[]>('https://api.example.com/users');
const api = createInstance({
baseURL: 'https://api.example.com',
headers: { Authorization: 'Bearer token' },
});
const data = await api.get<User[]>('/users'); // yes, the slash works now
Full axios compatibility layer — drop it in as a replacement without touching your codebase:
import { axios } from 'kiattp/axios';
const api = axios.create({ baseURL: 'https://api.example.com' });
// CancelToken, isCancel, isAxiosError, all, spread — all there
A plugin system instead of baking everything into the core:
api.use(retry_plugin({ maxRetries: 3, backoff: 'exponential', jitter: true }));
api.use(logger_plugin({ level: 'info' }));
api.use(timeout_plugin({ timeout: 10_000 }));
Interceptors that work exactly like you’d expect:
api.interceptors.request.use((config) => {
config.headers['authorization'] = 'Bearer ' + getToken();
return config;
});
api.interceptors.response.use(
(res) => res,
(err) => { console.error(`Failed: ${err.message}`); return err; }
);
Binary and streaming responses, upload/download progress, XSRF support, Node.js http adapter with proxy and redirect control the full table is in the README, I’ll spare you the exhaustive spec dump here.
The Security Angle Is Actually The Point
I want to come back to where this started, because I think it matters.
The axios attack vector was specifically the dependency tree and the postinstall hook. That’s the surface that got exploited. A library with no dependencies and no postinstall scripts has a fundamentally smaller attack surface. Not zero nothing is zero but meaningfully smaller.
When you npm install kiattp, you get exactly what’s in the kiattp package. There’s no follow-redirects to get compromised. No form-data to inherit vulnerabilities from. No mysterious postinstall script touching your filesystem.
Supply chain security isn’t just a DevSecOps team’s problem anymore. It’s a library author’s design decision. And I think the default should be: if you don’t need a dependency, don’t have one.
What’s Next
The library is live. Benchmarks are reproducible, the BENCHMARK.md has full methodology if you want to verify or poke holes in the numbers.
Subpath exports are set up so you can pull in only what you need:
import { httpAdapter } from 'kiattp/http'; // Node.js adapter only
import { retry_plugin } from 'kiattp/plugins/retry'; // just the plugin
import { axios } from 'kiattp/axios'; // just the compat layer
I’ll be doing a follow-up post on the Qwen Coder 3.6 workflow — specifically how I structured the steering prompts and the test harness to keep the model producing output that actually benchmarks well rather than just looks right. There’s a real art to using it effectively.
For now: try it, break it, file issues, tell me what the axios compat layer is missing for your use case. That’s the only way this gets properly battle-tested.
npm install kiattp
GitHub: dev-dami/kiattp
Thanks for reading. If you enjoyed this, the post on Qwen Coder 3.6 is coming soon and yes, I did use it to help build the very library this post is about. The recursion is not lost on me.
Top comments (0)