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

@sirmekus/oku

v1.0.2

Published

A lightweight, framework-agnostic HTTP client built on the native fetch API

Readme

oku

A lightweight, framework-agnostic HTTP client built on the native fetch API.

Features

  • Promise-based error handling — rejects on non-2xx responses and network failures, so you use standard try/catch or .catch().
  • Lifecycle hooksonStart and onComplete let you wire in any loading state, spinner, or analytics logic you already have.
  • Automatic body serialisationpost detects File / FileList values and switches between JSON.stringify and FormData automatically.
  • Bring-your-own headers — no cookies, XSRF tokens, or auth headers are injected. Pass exactly what you need.
  • TypeScript-first — all options and return types are fully typed and generic.

When to use oku

oku is a good fit when:

  • You want fetch without the boilerplate. You need JSON serialisation, consistent error handling, and typed responses — but not a heavy dependency like axios with its interceptor pipeline and adapter system.
  • You're building framework-agnostic code. oku has zero runtime dependencies and works in React, Vue, Svelte, SolidJS, vanilla JS, or any environment that supports the native fetch API (browsers, Node.js 18+, Deno, Bun).
  • You need predictable error handling. Non-2xx responses reject the promise just like network failures, so every error flows through one catch path — no need to manually inspect response.ok.
  • You upload files alongside JSON data. oku detects File/FileList values automatically and switches to FormData, so you never have to set Content-Type or build a FormData object yourself.
  • You want loading state hooks without a global store. onStart and onComplete callbacks let you wire spinners, progress bars, or analytics into any request without coupling to a specific state library.
  • You're working without a bundler. The IIFE build loads directly from a CDN <script> tag and exposes everything on a global, making it usable in plain HTML pages and browser extensions.
  • You need TypeScript generics on the response. Pass a type argument to get<T> or post<T> and the data field is typed automatically — no casting required.

oku is not the right tool when you need request cancellation (AbortController wiring), automatic retry logic, request deduplication, or a full interceptor pipeline. Reach for a more full-featured client (or your framework's data-fetching layer) in those cases.


oku vs axios

| | oku | axios | |---|---|---| | Bundle size | ~1 KB minified | ~10 KB minified | | Runtime dependencies | Zero | Zero (but heavier core) | | Underlying transport | Native fetch | XMLHttpRequest / fetch adapter | | Error handling | Rejects on non-2xx | Rejects on non-2xx | | File uploads | Auto-detects File/FileList, switches to FormData | Manual FormData construction | | Loading hooks | Built-in onStart / onComplete | Requires interceptors or wrapper | | Header injection | Explicit only — nothing injected by default | Auto-injects XSRF tokens, can attach cookies | | Request interceptors | Not supported | Supported | | Request cancellation | Not built-in | Built-in | | CDN / no-bundler usage | IIFE build included | UMD build available | | TypeScript generics | get<T>() / post<T>() | Full support | | Node.js support | 18+ (native fetch) | All versions |

Where oku wins

Bundle size. At ~1 KB, oku is roughly 90% smaller than axios. In library code, CDN-served pages, or anywhere bytes matter, this is the single most impactful difference.

Automatic file upload handling. Pass a File or FileList anywhere in data and oku switches to FormData automatically. With axios you construct FormData by hand and manage Content-Type yourself.

// oku — nothing extra needed
await http.post({ url: '/upload', data: { file: fileInput.files[0], label: 'avatar' } });

// axios — manual FormData construction
const fd = new FormData();
fd.append('file', fileInput.files[0]);
fd.append('label', 'avatar');
await axios.post('/upload', fd);

No magic header injection. Axios can silently attach XSRF tokens and cookies depending on your environment and config. oku only sends what you explicitly pass — fewer surprises and easier debugging.

Built-in lifecycle hooks. onStart / onComplete work with any state library or plain variables without setting up a global interceptor:

await http.get({
  url: '/api/orders',
  onStart: () => setLoading(true),
  onComplete: () => setLoading(false),
});

Pure fetch wrapper. oku doesn't reimplement browser networking — it wraps the platform API directly. Native caching, CORS, and streaming behaviour is preserved exactly.

Where axios wins

  • You need request or response interceptors (e.g. to attach a refreshed token on every request).
  • You need built-in request cancellation (axios exposes CancelToken; with oku you wire AbortController yourself).
  • You target Node.js < 18, where native fetch is unavailable.
  • You need automatic retry logic or request deduplication out of the box.

Installation

npm / yarn / pnpm

npm install @sirmekus/oku
# or
yarn add @sirmekus/oku
# or
pnpm add @sirmekus/oku

Script tag (CDN, no bundler required)

Include the IIFE build from unpkg or jsDelivr. All methods are available on the global HttpClient variable.

<!-- unpkg (latest) -->
<script src="https://unpkg.com/@sirmekus/oku/dist/index.global.js"></script>

<!-- jsDelivr (latest) -->
<script src="https://cdn.jsdelivr.net/npm/@sirmekus/oku/dist/index.global.js"></script>

<!-- Pin to a specific version (recommended for production) -->
<script src="https://unpkg.com/@sirmekus/[email protected]/dist/index.global.js"></script>

Once the script is loaded, use HttpClient directly - no import or bundler needed:

<script src="https://unpkg.com/@sirmekus/oku/dist/index.global.js"></script>
<script>
  HttpClient.get({ url: '/api/users' })
    .then(function (res) {
      console.log(res.data);
    })
    .catch(function (err) {
      console.error(err.statusCode, err.data);
    });

  // Or with async/await (modern browsers)
  async function loadUsers() {
    try {
      const res = await HttpClient.get({ url: '/api/users' });
      console.log(res.data);
    } catch (err) {
      console.error(err.statusCode, err.data);
    }
  }
</script>

ESM alternative — if your page already uses <script type="module"> you can import the ESM build directly from the CDN instead:

<script type="module">
  import http from 'https://unpkg.com/@sirmekus/oku/dist/index.mjs';

  const res = await http.get({ url: '/api/users' });
  console.log(res.data);
</script>

API Reference

get<T>(options: GetOptions): Promise<ResponseObject<T>>

Performs a GET request.

Resolves with a ResponseObject<T> on a 2xx response. Rejects with a ResponseObject<T> on a non-2xx response or network failure.

| Option | Type | Required | Default | Description | |---|---|---|---|---| | url | string | Yes | — | The request URL. | | headers | Record<string, string> | No | {} | Headers merged on top of { accept: "application/json" }. | | onStart | () => void | No | — | Called immediately before the request is sent. | | onComplete | () => void | No | — | Called after the request settles (success or failure). | | returnEntireResponse | boolean | No | false | When true, data is the full parsed response body. When false, data is response.data ?? response. |


post<T>(options: PostOptions): Promise<ResponseObject<T>>

Performs a POST, PUT, PATCH, or DELETE request.

Resolves with a ResponseObject<T> on a 2xx response. Rejects with a ResponseObject<T> on a non-2xx response or network failure.

| Option | Type | Required | Default | Description | |---|---|---|---|---| | url | string | Yes | — | The request URL. | | headers | Record<string, string> | No | {} | Headers merged on top of defaults. Do not set Content-Type manually for file uploads — the browser sets it with the correct boundary. | | onStart | () => void | No | — | Called immediately before the request is sent. | | onComplete | () => void | No | — | Called after the request settles (success or failure). | | data | Record<string, any> | No | {} | Request payload. Serialised to FormData if any value is a File or FileList, otherwise JSON.stringify'd. | | method | "POST" \| "PUT" \| "PATCH" \| "DELETE" | No | "POST" | HTTP method. |


rawFetch(url, options?): Promise<Response>

A thin wrapper around native fetch. Returns the raw Response object with no parsing applied. Useful for streaming, blob downloads, or any case where you need full control over the response.

| Parameter | Type | Description | |---|---|---| | url | string | The request URL. | | options | RequestInit & { headers?: Record<string, string> } | Standard fetch init options. headers are merged on top of { accept: "application/json" }. |


ResponseObject<T>

The shape returned (or rejected with) by get and post.

interface ResponseObject<T = any> {
  status: "success" | "error";
  statusCode: number;   // HTTP status code, or 0 for network-level failures
  data: T;
}

Usage Examples

Basic GET

import http from '@sirmekus/oku';

const res = await http.get({ url: '/api/users' });
console.log(res.data); // the response payload

Handling errors

Errors reject the promise, so handle them with try/catch or .catch().

import http, { ResponseObject } from '@sirmekus/oku';

try {
  const res = await http.get({ url: '/api/users/99' });
  console.log(res.data);
} catch (err) {
  const error = err as ResponseObject;
  console.error(error.statusCode); // e.g. 404
  console.error(error.data);       // server error body
}

Wiring up loading state

Pass onStart and onComplete to hook into any state management or UI you already have.

// useState
const [loading, setLoading] = useState(false);

const res = await http.get({
  url: '/api/orders',
  onStart: () => setLoading(true),
  onComplete: () => setLoading(false),
});

// Zustand
import { useLoadingStore } from '@/stores/loadingStore';
const { setLoading } = useLoadingStore.getState();

await http.post({
  url: '/api/orders',
  data: { item: 'book' },
  onStart: () => setLoading(true),
  onComplete: () => setLoading(false),
});

Global loading state via document events

If multiple parts of your app make requests independently, passing onStart/onComplete callbacks everywhere can be repetitive. An alternative is to dispatch CustomEvents on the document and listen for them in one central place — keeping requests fully decoupled from your loading UI.

Step 1 — register the listeners once (e.g. in your app bootstrap or a layout component):

document.addEventListener('http:start', () => {
  document.getElementById('global-spinner').style.display = 'block';
});

document.addEventListener('http:complete', () => {
  document.getElementById('global-spinner').style.display = 'none';
});

Step 2 — dispatch the events per request:

const httpEvents = {
  onStart:    () => document.dispatchEvent(new CustomEvent('http:start')),
  onComplete: () => document.dispatchEvent(new CustomEvent('http:complete')),
};

// Spread into any request — no extra code needed at the call site
const res = await http.get({ url: '/api/users', ...httpEvents });

You can also pass arbitrary detail in the event payload:

document.addEventListener('http:complete', (e) => {
  console.log('Request finished:', e.detail.url, e.detail.statusCode);
});

await http.get({
  url: '/api/orders',
  onStart: () =>
    document.dispatchEvent(new CustomEvent('http:start', { detail: { url: '/api/orders' } })),
  onComplete: () =>
    document.dispatchEvent(new CustomEvent('http:complete', { detail: { url: '/api/orders', statusCode: 200 } })),
});

This pattern works in any environment — vanilla JS, React, Vue, Svelte, or a plain HTML page loaded via <script> tag.


Injecting auth headers

const token = getAuthToken(); // your own logic

const res = await http.get({
  url: '/api/profile',
  headers: {
    Authorization: `Bearer ${token}`,
  },
});

POST with JSON

const res = await http.post({
  url: '/api/login',
  data: { email: '[email protected]', password: 'secret' },
});

PUT / PATCH / DELETE

await http.post({
  url: '/api/users/42',
  method: 'PUT',
  data: { name: 'Jane Doe' },
});

await http.post({
  url: '/api/users/42',
  method: 'DELETE',
});

File upload

Any File or FileList value in data triggers automatic FormData serialisation. Do not set Content-Type manually — the browser must set it so the multipart boundary is included.

// Single file
await http.post({
  url: '/api/avatar',
  data: { avatar: fileInput.files[0] },
});

// Multiple files under the same key
await http.post({
  url: '/api/attachments',
  data: { files: fileInput.files }, // FileList
});

// Mixed payload
await http.post({
  url: '/api/documents',
  data: {
    title: 'My Report',
    category: 'finance',
    file: fileInput.files[0],
  },
});

Raw fetch

Use rawFetch when you need the native Response object, e.g. for blob downloads or streaming.

import { rawFetch } from '@sirmekus/oku';

const res = await rawFetch('/api/export/csv', {
  headers: { Authorization: `Bearer ${token}` },
});
const blob = await res.blob();

Typing the response

Pass a type argument to get a typed data field.

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

const res = await http.get<User>({ url: '/api/users/1' });
// res.data is now typed as User
console.log(res.data.name);

Notes

  • accept: application/json is always set. Override it by passing your own accept key in headers.
  • Content-Type is set to application/json automatically for non-file POST payloads. For FormData payloads it is intentionally omitted so the browser can include the multipart boundary.
  • All requests use redirect: "manual". Redirects are not followed automatically.
  • No credentials, cookies, or XSRF tokens are handled. Inject them via headers if needed.
  • A statusCode of 0 in a rejected ResponseObject indicates a network-level failure (e.g. no internet, DNS failure) where no HTTP response was received.