I got mass an email from npm in March 2026 about the axios supply chain attack. A North Korean state actor compromised a maintainer and injected malicious code into the most popular HTTP client on npm. Millions of projects affected overnight.
That's when I looked at what axios actually does: it merges config, builds URLs, adds headers, runs interceptors, and wraps the response. All of that is maybe 200 lines of TypeScript on top of native fetch. But axios ships 53 KB gzipped with 2 dependencies that have their own dependencies.
So I built glyde - a TypeScript-first HTTP client with zero runtime dependencies. 1.73 KB gzipped.
What it looks like
import plane from "glyde"
const api = plane({ baseURL: "https://api.example.com" })
// Typed GET
const { data } = await api.get<User[]>("/users")
// POST with body
await api.post("/users", { name: "Yash", role: "admin" })
// Query params
await api.get("/search", { params: { q: "glyde", page: 1 } })
plane() creates an independent instance with its own config and interceptors. No shared global state.
Typed errors instead of status code checking
This is what error handling looks like with most HTTP clients:
// The old way
try {
await axios.get("/data")
} catch (err) {
if (err.response?.status === 404) { /* maybe? */ }
// What type is err? Who knows.
}
With glyde, errors have a type hierarchy with type guards:
import { isHttpError, isTimeoutError, isGlydeError } from "glyde"
try {
await api.get("/data")
} catch (err) {
if (isHttpError(err)) {
// TypeScript knows: err.status, err.response, err.config
console.log(err.status) // 404
console.log(err.response?.data) // parsed body
}
if (isTimeoutError(err)) {
// request exceeded timeout
}
}
The error hierarchy:
GlydeError (base)
+-- HttpError - non-2xx response (has status, response, config)
+-- TimeoutError - request exceeded timeout
+-- NetworkError - fetch failed (DNS, offline, CORS)
Async interceptors
Unlike most libraries that only support synchronous transforms, glyde interceptors are fully async:
// Refresh a token before every request
api.interceptors.request.use(async (config) => {
const token = await getToken()
return {
...config,
headers: { ...config.headers, Authorization: `Bearer ${token}` },
}
})
// Unwrap nested API responses
api.interceptors.response.use((response) => ({
...response,
data: response.data.result,
}))
Next.js App Router pattern
glyde works anywhere fetch exists, but I designed it with Next.js in mind. The recommended pattern:
Server-side (tower):
import plane from "glyde"
import { cookies } from "next/headers"
export async function tower() {
const api = plane({ baseURL: process.env.API_BASE_URL })
const cookieStore = await cookies()
api.interceptors.request.use((config) => {
const token = cookieStore.get("access_token")?.value
if (token) {
config.headers = { ...config.headers, Authorization: `Bearer ${token}` }
}
return config
})
return api
}
Client-side (passenger):
"use client"
import plane from "glyde"
export const passenger = plane({ baseURL: "/api/proxy" })
passenger.interceptors.response.use(
(response) => response,
(error) => {
if (error?.status === 401) window.location.href = "/login"
throw error
}
)
Token refresh? Handle it in Next.js middleware where you can actually write cookies. Don't fight the framework.
The numbers
| glyde | axios | |
|---|---|---|
| Size (gzipped) | 1.73 KB | 53 KB |
| Dependencies | 0 | 2 |
| TypeScript | Written in TS | Types bolted on |
| Engine | Native fetch | Legacy XHR |
Install
npm install glyde
Works in browsers, Node.js 18+, Bun, Deno, and Cloudflare Workers.
I'm actively working on retry plugins, request deduplication, and more interceptor examples. If you have feedback or feature requests, open an issue on GitHub.
Top comments (0)