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

katana-openapi-client

v0.5.0

Published

TypeScript client for Katana Manufacturing ERP API with automatic retries, rate limiting, and pagination

Readme

katana-openapi-client

npm

TypeScript/JavaScript client for the Katana Manufacturing ERP API with automatic resilience features.

Features

  • Automatic Retries - Exponential backoff with jitter, Retry-After aware (delta-seconds and HTTP-date), idempotency-safe method rules
  • Proactive Rate Limiting - Token bucket that adapts to X-Ratelimit-* headers
  • Timeouts & Cancellation - Per-attempt timeout plus AbortSignal support
  • Auto-Pagination - Automatically collects all pages for GET requests
  • Same behaviour for every endpoint - Resilience lives in the fetch layer, so client.fetch() and every generated SDK function get it automatically
  • Type Safety - Full TypeScript types generated from OpenAPI spec
  • Browser & Node.js - Works in both environments
  • Tree-Shakeable - Only import what you need

Installation

npm install katana-openapi-client
# or
pnpm add katana-openapi-client
# or
yarn add katana-openapi-client

Development

The package is managed by pnpm; the exact pnpm version is pinned by packageManager in package.json, and pnpm 10+ switches itself to that pin automatically, so any reasonably recent global pnpm works. From the repository root:

npm install --global pnpm
pnpm --dir packages/katana-client install --frozen-lockfile

Run development tasks from the repository root using the package-local scripts:

pnpm --dir packages/katana-client run generate
pnpm --dir packages/katana-client run lint
pnpm --dir packages/katana-client run typecheck
pnpm --dir packages/katana-client test
pnpm --dir packages/katana-client run build

typecheck covers src/ and tests/. Live smoke tests against the Katana test tenant run separately and skip when KATANA_TEST_API_KEY is unset (they never use KATANA_API_KEY); see Testing Guide:

pnpm --dir packages/katana-client test:live

Quick Start

import { KatanaClient } from 'katana-openapi-client';

// Create client with API key
const client = await KatanaClient.create({
  apiKey: 'your-api-key',
});

// Or use environment variable (KATANA_API_KEY)
const client = await KatanaClient.create();

// Or provide API key directly
const client = KatanaClient.withApiKey('your-api-key');

// Make requests - auto-pagination collects all pages
const response = await client.get('/products');
const { data } = await response.json();
console.log(`Found ${data.length} products`);

Types-Only Import

If you only need TypeScript types without any runtime code:

import type { Product, SalesOrder, Variant } from 'katana-openapi-client/types';

function processProduct(product: Product) {
  // ...
}

Configuration

const client = await KatanaClient.create({
  // API key (or set KATANA_API_KEY env var)
  apiKey: 'your-api-key',

  // Custom base URL (default: https://api.katanamrp.com/v1)
  baseUrl: 'https://api.katanamrp.com/v1',

  // Per-attempt timeout in ms (default: 30000; null disables)
  timeoutMs: 30_000,

  // Proactive rate limit (default: 60 req/min; null disables)
  requestsPerMinute: 60,

  // Retry configuration
  retry: {
    maxRetries: 5,           // Default: 5
    backoffFactor: 1.0,      // Default: 1.0 -> waits up to 2s, 4s, 8s, 16s, 32s
    backoffJitter: 1.0,      // Default: 1.0 (full jitter; 0 = deterministic)
    maxBackoffSeconds: 120,  // Default: 120 (caps backoff and Retry-After)
    respectRetryAfter: true, // Default: true
  },

  // Pagination configuration
  pagination: {
    maxPages: 100,           // Default: 100
    maxItems: undefined,     // Limit total items (optional)
    defaultPageSize: 250,    // Default: 250
  },

  // Disable auto-pagination globally
  autoPagination: false,

  // Optional logger (e.g. console): retry, rate-limit and pagination events,
  // plus an error entry for every 4xx response except 429
  logger: console,
});

Retry Behavior

The client implements the same retry strategy as the Python client:

| Status / failure | GET/HEAD/PUT/DELETE/OPTIONS/TRACE | POST/PATCH | | ----------------------------------- | --------------------------------- | ---------- | | 429 (Rate Limit) | Retry | Retry | | 502, 503, 504 | Retry | No Retry | | Other 4xx / 5xx | No Retry | No Retry | | Network error / per-attempt timeout | Retry | Retry | | Caller abort (signal) | No Retry | No Retry |

Key behavior: POST and PATCH requests are retried for rate limiting (429) because rate limits are transient and don't indicate idempotency issues.

Delays: the n-th retry waits up to backoffFactor * 2^n seconds (full jitter), or the server's Retry-After (delta-seconds or HTTP-date) when present — both capped at maxBackoffSeconds. When retries are exhausted the last response is returned (or the last network error thrown).

Timeouts & cancellation: each attempt is aborted after timeoutMs (time to response headers) and retried. Pass signal to cancel: backoff and rate-limit waits stop immediately and an aborted request is never retried.

const controller = new AbortController();
const response = await client.fetch('/products', { signal: controller.signal });

Proactive Rate Limiting

Beyond reactive 429 retries, the client proactively paces outgoing requests through a token bucket (60 req/min by default) and adapts to Katana's X-Ratelimit-* headers:

  • Sync down — if the server reports fewer remaining requests than expected (e.g. another client sharing the API key), the local budget drains to match. It never syncs up; the server is authoritative on the lower bound only.
  • Reset gate — when X-Ratelimit-Remaining hits 0, all requests block until X-Ratelimit-Reset elapses, so the client doesn't fire into an exhausted window.

The limiter sits innermost in the fetch chain, so each retry attempt and each paginated page consumes one token — matching how Katana counts requests server-side.

// Tune the budget
const client = await KatanaClient.create({ requestsPerMinute: 120 });

// Disable proactive limiting (reactive 429 retry still applies)
const client = await KatanaClient.create({ requestsPerMinute: null });

This mirrors the Python client's RateLimitTransport.

Auto-Pagination

Auto-pagination is ON by default for all GET requests:

// Collects all pages automatically
const response = await client.get('/products');
const { data, pagination } = await response.json();
console.log(`Collected ${pagination.total_items} items from ${pagination.collected_pages} pages`);

To disable auto-pagination:

// Explicit page parameter disables auto-pagination
const response = await client.get('/products', { page: 2, limit: 50 });

// Or per request (third argument) / globally via configuration
const firstPage = await client.get('/products', undefined, { autoPagination: false });
const first200 = await client.get('/products', undefined, { maxItems: 200 });
const client = await KatanaClient.create({
  autoPagination: false,
});

// Per-call overrides for generated SDK functions
const { data } = await getAllProducts({
  client: client.sdk,
  fetch: client.fetchWith({ maxItems: 200 }),
});

Responses without pagination metadata (e.g. GET /products/{id}) are returned untouched, and endpoints that return a bare JSON array keep that shape.

Error Handling

The client returns standard Response objects. Use parseError for typed error handling:

import {
  KatanaClient,
  parseError,
  AuthenticationError,
  RateLimitError,
  ValidationError,
} from 'katana-openapi-client';

const response = await client.post('/products', { name: 'Widget' });

if (!response.ok) {
  const body = await response.json();
  const error = parseError(response, body);

  if (error instanceof AuthenticationError) {
    console.error('Invalid API key');
  } else if (error instanceof RateLimitError) {
    console.error(`Rate limited. Retry after ${error.retryAfter}s`);
  } else if (error instanceof ValidationError) {
    // `error.message` is a human-readable, multi-line summary; `error.details`
    // is the structured Ajv error array exactly as Katana sends it.
    console.error(error.message);
    console.error(error.details);
    // [{ path: '/name', code: 'required', message: '...', info: { missingProperty: 'name' } }]
  } else {
    console.error(`Error ${error.statusCode}: ${error.message}`);
  }
}

Available error classes:

  • AuthenticationError (401)
  • RateLimitError (429) - includes retryAfter seconds
  • ValidationError (422) - includes details, the Ajv-style validation error array ({ path, code, message, info } per item, where code is the failed Ajv keyword). message is a formatted, multi-line summary mirroring the Python client's wording.
  • ServerError (5xx)
  • NetworkError - connection failures
  • KatanaError - base class for all errors

The catch-all path also surfaces messages from Katana's nested { "error": { ... } } envelope, including for undocumented status codes.

HTTP Methods

// GET (auto-paginated by default)
const products = await client.get('/products');
const productById = await client.get('/products/123');
const filtered = await client.get('/products', { category: 'widgets' });

// POST
const created = await client.post('/products', {
  name: 'New Product',
  sku: 'PROD-001',
});

// PUT
const updated = await client.put('/products/123', {
  name: 'Updated Product',
});

// PATCH
const patched = await client.patch('/products/123', {
  name: 'Patched Name',
});

// DELETE
const deleted = await client.delete('/products/123');

Advanced: Generated SDK

The package exports generated SDK functions with full TypeScript types. You can use them with the resilient client:

import { KatanaClient, getAllProducts, createProduct } from 'katana-openapi-client';

// Create the resilient client
const katana = await KatanaClient.create();

// Use SDK functions with the resilient client
const { data, error } = await getAllProducts({ client: katana.sdk });
if (data) {
  console.log(`Found ${data.data?.length ?? 0} products`); // `{ data: [...] }` envelope
}

// Or use the config shorthand
const result = await getAllProducts(katana.getConfig());

The SDK functions provide:

  • Full TypeScript types for all request/response bodies
  • Auto-completion for query parameters
  • Type-safe error handling

Response Helpers

SDK calls resolve to { data, error, request, response }. The response helpers return the data or throw the matching typed error — parity with the Python client's unwrap / unwrap_data / is_success / is_error / get_error_message:

import {
  getAllProducts,
  getErrorMessage,
  getProduct,
  isSuccess,
  unwrap,
  unwrapData,
} from 'katana-openapi-client';

const product = unwrap(await getProduct({ client: katana.sdk, path: { id: 1 } })); // Product
const products = unwrapData(await getAllProducts({ client: katana.sdk })); // Product[]

const result = await getProduct({ client: katana.sdk, path: { id: 2 } });
if (isSuccess(result)) {
  console.log(result.data.name); // narrowed to Product
} else {
  console.error(getErrorMessage(result));
}
  • unwrap(result) returns the data (undefined for a 204, e.g. most DELETEs) or throws AuthenticationError (401), ValidationError (422), RateLimitError (429, after retries), ServerError (5xx), KatanaError (any other status), or NetworkError (no response: connection failure, timeout, abort — original error as cause).
  • unwrapData(result) unwraps the { data: [...] } list envelope (and passes through the bare-array GET /bin_locations). Passing a single-resource result is a compile error.
  • isSuccess / isError are type guards; getError / getErrorMessage return the typed error / its message for a failed call (undefined on success).

Environment Variables

  • KATANA_API_KEY - API key for authentication
  • KATANA_BASE_URL - Override the base URL (optional)

Loading from .env files

Node.js 20.6+ (recommended):

node --env-file=.env your-script.js

Node.js 20.0-20.5 (use dotenv):

npm install dotenv
import 'dotenv/config';
import { KatanaClient } from 'katana-openapi-client';

const client = KatanaClient.withApiKey(process.env.KATANA_API_KEY!);

Note: This library supports Node.js 20+ (and browsers) but does not bundle dotenv. If you need .env file loading on Node.js < 20.6, install dotenv as a direct dependency in your project.

Documentation

For more detailed documentation:

License

MIT