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

klaim

v2.0.0

Published

Klaim is a lightweight TypeScript library designed to manage APIs and record requests, optimized for an optimal user experience.

Downloads

2,836

Readme

NPM Downloads

📚 Table of Contents

🚀 Features

  • Efficient API Management: Easily manage multiple APIs with streamlined integration and interaction capabilities.
  • API Grouping: Organize related APIs into logical groups with shared settings and configuration.
  • Route Grouping: Organize related routes into logical groups with inherited settings.
  • Request Recording: Seamlessly track requests for debugging and monitoring.
  • User Experience Optimization: Focused on performance and usability for a smooth user experience.
  • Lightweight: Minimal footprint for fast load times and minimal performance impact.
  • Middleware Support: Easily add middleware to modify requests and responses (before and after).
  • Hook System: Subscribe to hooks to monitor and react to specific events.
  • Stats & Observability: Track calls, latency, error rate and cache hit rate per route out of the box.
  • Caching: Enable caching on requests to reduce network load and improve performance.
  • Retry Mechanism: Automatically retry failed requests to enhance reliability.
  • Rate Limiting: Control the frequency of API calls to prevent abuse and respect API provider limits.
  • Circuit Breaker: Fail fast and stop hammering a failing API/route after repeated failures, with automatic recovery testing.
  • Request Deduplication: Coalesce concurrent identical GET calls into a single network request.
  • Timeout: Abort requests that exceed a specified duration with an optional custom error message.
  • TypeScript Support: Fully typed for enhanced code quality and developer experience.
  • Response Validation: Validate responses using schemas for increased reliability and consistency.
  • Pagination: Handle paginated requests easily with support for both page and offset based pagination.
  • Batch Requests: Run several route calls concurrently and get partial success/failure results, Promise.allSettled-style.

📥 Installation

Install Klaim via npm:

// Using npm
npm install klaim

// Using bun
bun add klaim

// Using deno
deno add @antharuu/klaim

🛠 Usage

Basic API Configuration

First, set up the API configuration. Define the API and its base URL.

import {Api, Route} from 'klaim';
// For deno: import { Api, Route } from "@antharuu/klaim";

// Your simple Todo type
type Todo = {
    userId: number;
    id: number;
    title: string;
    completed: boolean;
};

// Create a new API with the name "hello" and the base URL "https://jsonplaceholder.typicode.com/"
Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
    // Define routes for the API
    Route.get<Todo[]>("listTodos", "todos");
    Route.get<Todo>("getTodo", "todos/[id]");
    Route.post<Todo>("addTodo", "todos");
});

Route Definition

Routes represent endpoints in your API and can be defined with different HTTP methods. Routes can include parameters and custom configurations:

Api.create("api", "https://api.example.com", () => {
    // Basic GET route
    Route.get("listUsers", "/users");

    // GET route with URL parameter
    Route.get("getUser", "/users/[id]");

    // POST route with custom headers and body
    Route.post("createUser", "/users", {
        "Content-Type": "application/json"
    }, {userId: 1, name: "John Doe"});

    // PUT route with parameter
    Route.put("updateUser", "/users/[id]");

    // DELETE route
    Route.delete("deleteUser", "/users/[id]");

    // PATCH route
    Route.patch("updateUserStatus", "/users/[id]/status");

    // OPTIONS route
    Route.options("userOptions", "/users");
});

Groups

Klaim provides powerful grouping capabilities for both APIs and routes. Groups can be used to organize related elements, share configuration, and maintain a clean structure in your application.

API Groups

Organize multiple APIs that serve related purposes:

import {Group, Api, Route} from 'klaim';

// Create a group for user-related services
Group.create("userServices", () => {
    // Authentication API
    Api.create("auth", "https://auth.example.com", () => {
        Route.post("login", "/login");
        Route.post("register", "/register");
    });

    // User Management API
    Api.create("users", "https://users.example.com", () => {
        Route.get("list", "/users");
        Route.get("getOne", "/users/[id]");
    });
}).withRetry(3); // Apply retry mechanism to all APIs in the group

// Access grouped APIs
await Klaim.userServices.auth.login({}, {username: "user", password: "pass"});
await Klaim.userServices.users.list();

Route Groups

Organize routes within an API into logical groups:

Api.create("hello", "https://api.example.com/", () => {
    // Group user-related routes
    Group.create("users", () => {
        Route.get<User[]>("list", "/users");
        Route.get<User>("getOne", "/users/[id]");
        Route.post<User>("create", "/users");
    }).withCache(60);  // Cache all user routes for 60 seconds

    // Group product-related routes
    Group.create("products", () => {
        Route.get("list", "/products");
        Route.get("getOne", "/products/[id]");
    });
});

// Use grouped routes
const users = await Klaim.hello.users.list();
const product = await Klaim.hello.products.getOne({id: 1});

Nested Groups

Create complex hierarchies with nested groups:

Group.create("services", () => {
    // Internal services group
    Group.create("internal", () => {
        Api.create("logs", "https://logs.internal.example.com", () => {
            Route.post("write", "/logs");
        });

        Api.create("metrics", "https://metrics.internal.example.com", () => {
            Route.post("track", "/metrics");
        });
    }).withRetry(5);  // More retries for internal services

    // External services group
    Group.create("external", () => {
        Api.create("weather", "https://api.weather.com", () => {
            Route.get("forecast", "/forecast/[city]");
        });

        Api.create("geocoding", "https://api.geocoding.com", () => {
            Route.get("search", "/search/[query]");
        });
    }).withCache(300);  // Cache external services longer
});

// Access nested groups
await Klaim.services.internal.logs.write({}, {message: "Log entry"});
await Klaim.services.external.weather.forecast({city: "Paris"});

Group Configuration

Groups can share configuration among all their members:

Group.create("apis", () => {
    Api.create("service1", "https://api1.example.com", () => {
        Route.get("test", "/test");
    });

    Api.create("service2", "https://api2.example.com", () => {
        Route.get("test", "/test");
    });
})
    .withCache(60)  // Enable caching for all APIs
    .withRetry(3)   // Enable retries for all APIs
    .before(({config}) => {  // Add authentication for all APIs
        config.headers.Authorization = `Bearer ${getToken()}`;
    })
    .after(({data}) => {  // Process all responses
        logResponse(data);
    });

Request Handling

Handle requests using the defined routes:

import {Klaim} from 'klaim';
// For deno: import { Klaim } from "@antharuu/klaim";

// Make a request to the "listTodos" route
const listOfTodos = await Klaim.hello.listTodos<Todo[]>();

// Make a request to the "getTodo" route with the parameter "id"
const todo = await Klaim.hello.getTodo<Todo>({id: 1});

// Make a request to the "addTodo" route
const newTodo = await Klaim.hello.addTodo<Todo>({}, {title: "New Todo", completed: false, userId: 1});

Middleware Usage

Add middleware to modify requests and responses. Use before middleware to alter requests before they are sent and after middleware to process responses:

Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
    // With before middleware
    Route.get<Todo>("getRandomTodo", "todos")
        .before(({url}) => {
            const random = Math.floor(Math.random() * 10) + 1;
            return {url: `${url}/${random}`};
        });

    // With after middleware
    Route.get<Todo>("getFirstTodo", "todos")
        .after(({data: [first]}) => ({data: first}));
});

Global Middleware

before/after middleware also exists at a global level, on the Klaim object itself. A global middleware runs for every route of every API, in addition to (not instead of) any local before/after already set on a route. This is the same before/after concept, just extended to a global scope — no new middleware system was introduced.

import {Klaim} from 'klaim';
// For deno: import { Klaim } from "@antharuu/klaim";

// Runs before every request, for every API/route
Klaim.before(({url, config}) => {
    console.log(`[global] requesting ${url}`);
    return {config: {...config, headers: {...config.headers as object, "X-Trace-Id": "123"}}};
});

// Runs after every response, for every API/route
Klaim.after(({data}) => {
    console.log("[global] response received");
    return {data};
});

Multiple global middlewares can be stacked — each call to Klaim.before/Klaim.after adds another one, executed in registration order (unlike the local before/after, which is a single callback replaced if called again).

Execution order

For a single route call, the full order is:

  1. global before middlewares (registration order, each fed the previous result)
  2. (api-level before, if the API used .before(...) — see note below)
  3. route before (the existing local .before(...) hook)
  4. network request execution
  5. route after (the existing local .after(...) hook)
  6. (api-level after, if the API used .after(...) — see note below)
  7. global after middlewares (registration order, each fed the previous result)

Known limitation: as of this version, an API-level before/after (set with Api.create(...).before(...) directly on the API, as opposed to on one of its routes) is stored but not invoked during a request — only the route-level before/after runs. This is a pre-existing behavior, unrelated to global middleware, and is documented here for transparency rather than fixed by this feature.

Hook Subscription

Subscribe to hooks to monitor specific events:

import {Hook} from 'klaim';
// For deno: import { Hook } from "@antharuu/klaim";

// Subscribe to the "hello.getFirstTodo" hook
Hook.subscribe("hello.getFirstTodo", ({url}) => {
    console.log(`Requesting ${url}`);
});

Stats & Observability

Klaim ships a built-in Stats singleton that observes every route call automatically, without touching or conflicting with your own Hook.subscribe callbacks. It's built on Hook.onAny, a multi-listener observation point separate from the single-callback-per-route Hook.subscribe API, so Stats and your user hooks can coexist on the same route.

For each route it tracks, non-blocking and with a bounded memory footprint:

  • calls: total number of calls observed
  • errors / errorRate: failed calls and their ratio
  • cacheHits / cacheHitRate: calls served from cache and their ratio
  • avgLatencyMs: cumulative running average latency (no per-call array growth)
  • p95LatencyMs: approximate 95th percentile from a bounded sliding window (last 100 calls)
import {Stats} from 'klaim';
// For deno: import { Stats } from "@antharuu/klaim";

// After making some calls...
await Klaim.hello.listTodos();
await Klaim.hello.getTodo({id: 1});

// Read metrics for a single route
const routeStats = Stats.i.get("hello.listTodos");
console.log(routeStats?.calls, routeStats?.avgLatencyMs, routeStats?.errorRate);

// Read metrics for every observed route
const allStats = Stats.i.getAll();
console.table(allStats);

// Reset all collected metrics (e.g. between test runs)
Stats.i.reset();

Caching Requests

Enable caching on requests to reduce network load and improve performance. By default, the cache duration is 20 seconds, but you can specify a custom duration in seconds.

Caching Individual Routes

You can enable caching on individual routes:

Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
    // Get a list of todos with default cache duration (20 seconds)
    Route.get<Todo[]>("listTodos", "todos").withCache();

    // Get a specific todo by id with custom cache duration (300 seconds)
    Route.get<Todo>("getTodo", "todos/[id]").withCache(300);

    // Add a new todo (no cache)
    Route.post<Todo>("addTodo", "todos");
});

Now, when making requests, the caching feature will be applied.

Invalidating Cached Routes

Cache entries are namespaced internally as ${parent}.${routeName} (e.g. "hello.getTodo"). All parameterized variants of a route (different args, query strings, TTLs or response policies) share that namespace, so a single call clears every cached variant — without wiping unrelated entries the way Cache.clear() would.

import {Cache, Klaim} from 'klaim';

// Invalidate only "hello.getTodo", leaving other cached routes untouched
Cache.i.invalidate("hello.getTodo");

// Or invalidate directly from the route handler
Klaim.hello.getTodo.invalidate();

// A broader pattern invalidates every route namespaced under it
Cache.i.invalidate("hello"); // clears hello.getTodo, hello.listTodos, ...

Caching the Entire API

You can also enable caching for all routes defined within an API:

Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
    // Define routes for the API
    Route.get<Todo[]>("listTodos", "todos");
    Route.get<Todo>("getTodo", "todos/[id]");
    Route.post<Todo>("addTodo", "todos");
}).withCache(); // Enable default cache duration (20 seconds) for all routes

Retry Mechanism

Configure automatic retries for failed requests:

// Apply retry at the API level
Api.create("api", "https://api.example.com", () => {
    Route.get("users", "/users");
}).withRetry(3);  // Will retry failed requests up to 3 times

// Apply retry at the route level
Api.create("api", "https://api.example.com", () => {
    Route.get("unstableRoute", "/unstable-endpoint").withRetry(5);  // Will retry up to 5 times
});

Rate Limiting

Control the frequency of API calls to prevent abuse and respect API provider rate limits:

// Apply rate limiting at the API level
Api.create("api", "https://api.example.com", () => {
    Route.get("users", "/users");
    Route.get("posts", "/posts");
}).withRate({ limit: 5, duration: 10 });  // Max 5 requests every 10 seconds for this API

// Apply rate limiting at the route level
Api.create("api", "https://api.example.com", () => {
    // This route has its own stricter limits
    Route.get("expensive", "/expensive-operation").withRate({ limit: 2, duration: 60 });  // Max 2 requests per minute
    
    // This route uses the default limits (5 per 10 seconds if not specified)
    Route.get("normal", "/normal-operation").withRate();
});

// Handling rate limit errors
try {
    await Klaim.api.expensive();
} catch (error) {
    if (error.message.includes('Rate limit exceeded')) {
        console.log('Please wait before trying again');
    }
}

Circuit Breaker

Stop hammering an API/route that keeps failing: once a configurable number of consecutive failures is reached, the circuit opens and subsequent calls fail fast with a CircuitOpenError — no network request, no retry attempt consumed — until a reset timeout elapses. After the timeout, the circuit moves to half-open and lets exactly one probe call through: if it succeeds the circuit closes again (normal traffic resumes and the failure count resets); if it fails the circuit reopens and the timeout restarts.

The breaker guards the whole call, not each individual retry attempt: it is checked once before fetchWithRetry's retry loop starts, and only the loop's overall outcome (all attempts exhausted vs. at least one success) is recorded against it. This keeps the breaker focused on "is this endpoint down" rather than reacting to single transient errors that retry/backoff is already designed to absorb.

Like retry and rate limiting, the breaker can be configured at the route level (takes precedence) or at the API level (shared across all routes of that API that don't set their own):

// Apply a circuit breaker at the route level
Api.create("api", "https://api.example.com", () => {
    Route.get("flaky", "/flaky-endpoint").withBreaker({ failureThreshold: 3, resetTimeout: 15 });
});

// Apply a circuit breaker at the API level (shared by all its routes)
Api.create("api", "https://api.example.com", () => {
    Route.get("users", "/users");
    Route.get("posts", "/posts");
}).withBreaker({ failureThreshold: 5, resetTimeout: 30 }); // Defaults if omitted

// Handling circuit breaker errors
try {
    await Klaim.api.flaky();
} catch (error) {
    if (error.name === 'CircuitOpenError') {
        console.log(`Circuit is open, retry in ${error.retryAfterMs}ms`);
    }
}

Request Deduplication

When several callers trigger the exact same GET request while a first call for it is still in flight, Klaim coalesces them: only one network call is made, and every caller receives the same result (success or failure). The dedup key is built from the route and its fully-resolved URL/params, so two GET calls with different parameters are never mixed up.

This only ever applies to GET requests. Mutating methods (POST, PUT, PATCH, DELETE) always hit the network, since sharing or replaying a write between callers would be incorrect.

⚠️ Note: cancelling a call via .cancel() (see Request Cancellation) aborts the shared in-flight request, which also rejects every other caller currently coalesced onto that same GET. Avoid cancelling calls you expect other code to be relying on concurrently.

Api.create("api", "https://api.example.com", () => {
    Route.get("users", "/users");
});

// Both calls share a single underlying fetch
const [a, b] = await Promise.all([
    Klaim.api.users(),
    Klaim.api.users()
]);

Request Timeout

Bound each request attempt with a timeout in seconds and an optional custom message. Timeout is disabled by default; .withTimeout() enables 5 seconds, and a route setting takes precedence over the API setting.

The budget starts after before, rate limiting and onCall, before cache lookup or fetch. It includes headers and body reading/decoding, but excludes validation, after, hooks and retry backoff. Each retry gets a fresh budget and transport controller. Cache hits are also inside the budget, but do not fetch or abort.

On expiration, Klaim rejects with TimeoutError and asks cooperative transports to abort. Late responses cannot start body reading, update the cache or run success callbacks; an already-started body read cannot publish a late result. Timers and caller-signal relays are removed when the attempt settles, without aborting successful requests. With retries enabled, the final timeout remains the cause of RetryExhaustedError.

A signal supplied through before as config.signal is relayed with its reason when timeout is enabled, and passed through unchanged otherwise. Caller cancellation preserves the existing retry/backoff behavior; it is not a new global cancellation policy. Without AbortController, only logical timeout and late-result guards are available. Server-side effects already performed cannot be undone, and synchronous work cannot be preempted. Native transport cancellation is tested on Node; Bun, Deno and browser transports are not verified by these tests.

Api.create("api", "https://api.example.com", () => {
    Route.get("slow", "/slow").withTimeout(5, "Too slow");
}).withTimeout(10);

try {
    await Klaim.api.slow();
} catch (error) {
    console.error(error);
}

Request Cancellation

Every call returned by a route exposes .cancel(reason?), letting you cancel that specific in-flight call without affecting concurrent calls to the same route.

Api.create("api", "https://api.example.com", () => {
    Route.get("search", "/search");
});

const call = Klaim.api.search();

// Somewhere else, e.g. a component unmount or a newer request superseding this one
call.cancel();

try {
    await call;
} catch (error) {
    if (error.name === "CancelledError") {
        console.log("Call was cancelled");
    }
}

Calling .cancel() a second time, or after the call has already settled, is a no-op. Pass a custom reason to reject with something other than the default CancelledError.

When the route has .withTimeout() enabled, cancellation reuses the same per-attempt AbortController used for the timeout budget, so the underlying transport (e.g. fetch) is aborted immediately with no extra allocation and no signal collision between concurrent calls. Without a timeout, cancellation still settles the call immediately and guards any late result (cache write, success callbacks) from completing, but cannot abort synchronous work already in progress. This is independent of, and composes with, a signal supplied through before as config.signal.

Response Validation

You can use Yup to validate the response schema for increased reliability and consistency. You can specify a schema for individual routes to ensure the response data conforms to the expected structure.

⚠️ Note: This feature requires the yup package to be installed.

Adding Validation to Individual Routes

Enable validation on individual routes:

import * as yup from 'yup';

// Define the schema using Yup
const todoSchema = yup.object().shape({
    userId: yup.number().required(),
    id: yup.number().min(1).max(10).required(),
    title: yup.string().required(),
    completed: yup.boolean().required()
});

Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
    // Get a specific todo by id with validation
    Route.get<Todo>("getTodo", "todos/[id]").validate(todoSchema);
});

// This request will fail because the id is out of range
const todoFail = await Klaim.hello.getTodo<Todo>({id: 15});

// This request will succeed
const todo = await Klaim.hello.getTodo<Todo>({id: 1});

Using Zod for Validation

You can also validate responses with zod via the built-in zodAdapter. zod schemas expose parseAsync/safeParseAsync instead of validate, so zodAdapter wraps a zod schema into the { validate } interface expected by Route#validate.

⚠️ Note: zod is not a dependency of Klaim - it stays entirely optional and must be installed by your project to use zodAdapter.

import { z } from 'zod';
import { Api, Klaim, Route, zodAdapter } from 'klaim';

// Define the schema using zod
const todoSchema = z.object({
    userId: z.number(),
    id: z.number().min(1).max(10),
    title: z.string(),
    completed: z.boolean()
});

Api.create("hello", "https://jsonplaceholder.typicode.com/", () => {
    // Get a specific todo by id with validation
    Route.get<Todo>("getTodo", "todos/[id]").validate(zodAdapter(todoSchema));
});

// This request will fail because the id is out of range,
// throwing a ValidationError with the zod issues as `cause`
const todoFail = await Klaim.hello.getTodo<Todo>({id: 15});

// This request will succeed
const todo = await Klaim.hello.getTodo<Todo>({id: 1});

Pagination

Configure pagination for routes that require it:

// Basic usage with custom limit and offset parameter
Api.create("api", "https://api.example.com", () => {
    Route.get("list", "/items").withPagination({
        limit: 20,          // Items per page
        page: 1,            // Default page number
        pageParam: "offset", // Parameter name for page/offset or any other custom parameter
        limitParam: "limit"  // Parameter name for limit
    }); // All options are optional
});

// Using paginated endpoints
const page1 = await Klaim.api.list();        // First page
const page2 = await Klaim.api.list(2);       // Second page
const customPage = await Klaim.api.list(5);  // Fifth page

⚠️ Note: The pagination feature simplifies your pagination parameters, but your API/backend needs to respond to these parameters. Klaim does not handle the pagination logic, only the parameters management.

Batch Requests

Run several route calls concurrently with batch() and get a partial-success result, the same way Promise.allSettled works: a failing call never rejects the whole batch nor blocks the other calls. Each entry of the result is either {status: 'fulfilled', value} or {status: 'rejected', reason}.

batch() only orchestrates existing route calls — it does not bypass per-route protections, so caching, retry, timeout and rate limiting configured on your routes/APIs still apply exactly as if each call had been made individually.

Two input shapes are supported, both returning a result with the same shape as the input:

  • Named object (recommended): keys let you destructure results by name.
  • Array: results keep the same order/positions as the input.

Prefer thunks (() => Klaim.api.route()) over bare promises so the request is only fired when batch() runs.

import { batch, Klaim } from "klaim";

// Named form
const {todos, user} = await batch({
    todos: () => Klaim.hello.listTodos(),
    user: () => Klaim.hello.getUser({id: 1})
});

if (todos.status === "fulfilled") console.log(todos.value);
if (user.status === "rejected") console.error(user.reason);

// Array form
const [todosResult, userResult] = await batch([
    () => Klaim.hello.listTodos(),
    () => Klaim.hello.getUser({id: 1})
]);

🔗 Links

📢 Project Status

Klaim is now considered feature complete. The library has reached a state where it provides all the core functionality originally envisioned (even more), and no new features are currently planned.

However, this doesn't mean the project is abandoned:

  • Bug fixes and maintenance will continue to be addressed
  • Issues remain open for bug reports and suggestions
  • Pull requests are welcome if you'd like to contribute additional features or improvements

If you have ideas for new features that would enhance Klaim, please feel free to open an issue to discuss them. Collaborative contributions through pull requests are especially appreciated!

Thank you for your interest in this project. I personally will be moving on to focus on other libraries, but I'm grateful for all the support and feedback the community has provided.

🤝 Contributing

Contributions are welcome! Please see the Contributing Guide for more details.

📜 License

This project is licensed under the MIT License - see the LICENSE file for details.