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

@api-wrappers/api-core

v1.0.2

Published

Shared HTTP client runtime for the api-wrappers organisation. Provides request orchestration, a plugin lifecycle, transport abstraction, and built-in cache/retry/logger plugins.

Downloads

1,043

Readme

@api-wrappers/api-core

GitHub Repo stars

Shared TypeScript HTTP runtime for API wrapper libraries.

@api-wrappers/api-core gives wrapper packages a small, predictable foundation for request execution, retries, timeouts, auth headers, caching, rate limiting, GraphQL requests, custom transports, and plugin-based request/response middleware.

It is designed for packages that expose domain-specific clients while keeping their internal HTTP layer consistent and testable.

Features

  • Typed REST helpers: get, post, put, patch, delete, head, options, and request.
  • requestWithResponse for wrappers that need response headers, status, or plugin metadata.
  • GraphQL helper with typed data and variables.
  • Deterministic plugin lifecycle with setup, beforeRequest, afterResponse, onError, and dispose.
  • Built-in auth, cache, logger, rate-limit, retry, and timeout plugins.
  • Native HeadersInit support for default, per-request, and GraphQL headers.
  • Type guards for ergonomic unknown error handling in TypeScript.
  • Fetch transport with JSON bodies, raw string bodies, abort signals, and timeout handling.
  • Response parsing for JSON, text, and binary payloads.
  • Query string support for primitives and repeated array values.
  • ESM and CommonJS builds with TypeScript declarations.

Requirements

  • TypeScript 5+
  • A runtime with fetch, Request, Response, and AbortController available. Modern Node, Bun, browsers, and edge runtimes satisfy this.
  • For older runtimes, pass a custom fetch implementation or a full custom Transport.

Installation

bun add @api-wrappers/api-core
npm install @api-wrappers/api-core

Quick Start

import {
	createAuthPlugin,
	createClient,
	createRetryPlugin,
	createTimeoutPlugin,
} from "@api-wrappers/api-core";

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

const client = createClient({
	baseUrl: "https://api.example.com/v1",
	defaultHeaders: { accept: "application/json" },
	plugins: [
		createAuthPlugin(() => process.env.API_TOKEN),
		createRetryPlugin({ maxAttempts: 3, delayMs: 300 }),
		createTimeoutPlugin({ timeoutMs: 30_000 }),
	],
});

const user = await client.get<User>("/users/123");

baseUrl and request paths are slash-safe:

client.get("/users");
client.get("users");

Both work with baseUrl: "https://api.example.com/v1/".

Client Configuration

import { createClient } from "@api-wrappers/api-core";

const client = createClient({
	baseUrl: "https://api.example.com",
	defaultHeaders: {
		accept: "application/json",
	},
	timeoutMs: 10_000,
	retry: {
		maxAttempts: 2,
		delayMs: 250,
		jitter: true,
		retriableStatusCodes: [429, 500, 502, 503, 504],
	},
	plugins: [],
	logger: console,
});

| Option | Purpose | | --- | --- | | baseUrl | Base URL prepended to relative request paths. | | defaultHeaders | Headers merged into every request. Per-request headers win. | | timeoutMs | Default request timeout. Can be overridden per request. | | retry | Global retry policy. Can be overridden by createRetryPlugin. | | plugins | Plugin list for auth, cache, logging, rate limiting, etc. | | transport | Full request executor override, useful in tests. | | fetch | Custom fetch implementation used by the default transport. | | logger | Internal diagnostics logger. Defaults to console. |

Requests

await client.get<SearchResult>("/search", {
	query: {
		q: "alien",
		page: 2,
		with_genres: [878, 12],
		skip: undefined,
	},
	headers: { accept: "application/json" },
	timeoutMs: 5_000,
	signal: abortController.signal,
	tags: ["search"],
	cacheKey: "search:alien:2",
});

Query values can be strings, numbers, booleans, nullish values, or arrays of those primitives. null and undefined are skipped. Arrays are encoded as repeated query parameters:

?with_genres=878&with_genres=12

Request Methods

client.get<T>(path, options);
client.post<T>(path, body, options);
client.put<T>(path, body, options);
client.patch<T>(path, body, options);
client.delete<T>(path, options);
client.head<T>(path, options);
client.options<T>(path, options);
client.request<T>(path, { method: "POST", body });

Plain objects and arrays are JSON encoded. Strings and native BodyInit values are sent as-is, which supports APIs that expect text query languages:

const games = await client.post<Game[]>(
	"/games",
	"fields name,rating; limit 10;",
	{
		headers: {
			"content-type": "text/plain",
			accept: "application/json",
		},
	},
);

Binary responses can stay on the shared request path by selecting an explicit response type:

const bytes = await client.post<ArrayBuffer>("/games.pb", query, {
	headers: { accept: "application/octet-stream" },
	responseType: "arrayBuffer",
});

Response Metadata

Use requestWithResponse when a wrapper needs more than the parsed body:

const result = await client.requestWithResponse<MoviePage>("/movie/popular");

result.data;
result.response.status;
result.response.headers.get("x-ratelimit-remaining");
result.request.url;
result.meta["cache.served"];

GraphQL

interface GetMediaQuery {
	Media: { id: number; title: { romaji: string } };
}

interface GetMediaVariables {
	id: number;
}

const data = await client.graphql<GetMediaQuery, GetMediaVariables>("/", {
	query: `
		query GetMedia($id: Int) {
			Media(id: $id) { id title { romaji } }
		}
	`,
	variables: { id: 1 },
	operationName: "GetMedia",
});

console.log(data.Media.title.romaji);

GraphQL uses the same transport, plugin lifecycle, retry policy, timeout handling, and error classes as REST requests.

Built-In Plugins

Plugins are ordinary objects that run through a deterministic lifecycle. Lower priority values run earlier in beforeRequest; higher values run earlier in afterResponse.

Auth

createAuthPlugin("static-token");
createAuthPlugin(() => tokenStore.getAccessToken());
createAuthPlugin({
	getToken: () => apiKey,
	headerName: "x-api-key",
	scheme: null,
});

The default header is:

authorization: Bearer <token>

Retry

createRetryPlugin({
	maxAttempts: 3,
	delayMs: 300,
	jitter: true,
	retriableStatusCodes: [429, 500, 502, 503, 504],
});

429 responses respect retry-after when present. Numeric values are treated as seconds; HTTP-date values are also supported.

Timeout

createTimeoutPlugin({ timeoutMs: 30_000 });

Timeouts throw TimeoutError.

Rate Limit

createRateLimitPlugin({
	maxConcurrent: 4,
	minTimeMs: 250,
});
createRateLimitPlugin({
	maxRequestsPerInterval: 30,
	intervalMs: 60_000,
});

The limiter releases slots on successful responses, transport failures, and plugin failures.

Cache

import { createCachePlugin, MemoryStore } from "@api-wrappers/api-core";

const cache = createCachePlugin({
	store: new MemoryStore(),
	ttlMs: 60_000,
	methods: ["GET"],
});

const client = createClient({
	baseUrl: "https://api.example.com",
	plugins: [cache],
});

await client.get("/users/1", { tags: ["user"] });
await cache.invalidate("GET:https://api.example.com/users/1");
await cache.invalidateByTag("user");

Cache hits skip the transport and set meta["cache.served"].

Logger

createLoggerPlugin({
	logRequest: true,
	logResponse: true,
	logError: true,
	logger: console,
});

Pass a structured logger or no-op logger to control diagnostics.

Custom Plugins

import type { ApiPlugin } from "@api-wrappers/api-core";

export function createClientIdPlugin(clientId: string): ApiPlugin {
	return {
		name: "client-id",
		priority: 2,
		beforeRequest(ctx) {
			return {
				...ctx,
				headers: {
					...ctx.headers,
					"client-id": clientId,
				},
			};
		},
	};
}

Hooks may return a new context or undefined to keep the current one.

| Hook | When it runs | | --- | --- | | setup(client) | Once, lazily before the first request. | | beforeRequest(ctx) | Before transport execution. | | afterResponse(ctx) | After response parsing. | | onError(error, ctx) | For transport, HTTP, and plugin failures. | | dispose() | When client.dispose() is called. |

Read docs/guides/plugins.md for the full plugin contract.

Error Handling

import {
	isApiError,
	isGraphQLRequestError,
	isRateLimitError,
	isTimeoutError,
} from "@api-wrappers/api-core";

try {
	await client.get("/resource");
} catch (error) {
	if (isRateLimitError(error)) {
		console.log(error.retryAfterMs);
	} else if (isTimeoutError(error)) {
		console.log("timed out");
	} else if (isGraphQLRequestError(error)) {
		console.log(error.graphqlErrors);
	} else if (isApiError(error)) {
		console.log(error.status, error.responseBody);
	}
}

| Error | Meaning | | --- | --- | | ApiError | Non-2xx HTTP response after retries are exhausted. | | RateLimitError | HTTP 429 response. Includes retryAfterMs when available. | | TimeoutError | Request exceeded timeout. | | GraphQLRequestError | GraphQL response contained an errors array. |

Testing

Use a custom transport for deterministic tests:

import { BaseHttpClient } from "@api-wrappers/api-core";

const client = new BaseHttpClient({
	baseUrl: "https://api.example.com",
	transport: {
		execute: async (ctx) =>
			new Response(JSON.stringify({ url: ctx.url }), {
				headers: { "content-type": "application/json" },
			}),
	},
});

This exercises the client, request options, plugins, and error handling without making network calls.

Package Exports

import {
	ApiError,
	BaseHttpClient,
	createAuthPlugin,
	createCachePlugin,
	createClient,
	createLoggerPlugin,
	createRateLimitPlugin,
	createRetryPlugin,
	createTimeoutPlugin,
	gql,
	GraphQLRequestError,
	isApiCoreError,
	isApiError,
	isGraphQLRequestError,
	isRateLimitError,
	isTimeoutError,
	MemoryStore,
	RateLimitError,
	TimeoutError,
} from "@api-wrappers/api-core";

import type {
	ApiCoreError,
	ApiPlugin,
	ApiResponse,
	ClientConfig,
	FetchLike,
	HeaderInput,
	QueryParams,
	RequestContext,
	RequestOptions,
	ResponseContext,
	Transport,
} from "@api-wrappers/api-core";

The package publishes:

  • ESM: dist/index.mjs
  • CommonJS: dist/index.cjs
  • Type declarations for both module formats
  • README and docs

More Documentation

Development

bun install
bun run verify
bun run check
bun run typecheck
bun test
bun run build
bun run pack:dry-run

dist is generated by tsdown. The published package includes dist, docs, README.md, and package.json.