Node.js / TypeScript SDK
The Node.js SDK wraps the global fetch function to passively capture outgoing API calls. Zero changes to your application logic.
Installation
npm install quotawatchQuick example
import { QuotaWatch } from 'quotawatch';
// Initialize once at startup
QuotaWatch.init({
apiKey: 'qw_live_...',
environment: process.env.NODE_ENV ?? 'production',
apis: [
{
name: 'OpenAI',
baseUrl: 'https://api.openai.com',
limits: { requestsPerMinute: 60, requestsPerDay: 10_000 },
},
],
});
// Your existing code — unchanged
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
body: JSON.stringify({ model: 'gpt-4o', messages: [] }),
});How it works
On init(), the SDK monkey-patches globalThis.fetch. Every outgoing fetch() call is intercepted. If the URL matches a configured baseUrl, an event is recorded asynchronously and the real request proceeds immediately.
What gets captured
{
"api": "OpenAI",
"endpoint": "/v1/chat/completions",
"method": "POST",
"status": 200,
"latencyMs": 847,
"timestamp": "2026-05-07T12:00:00Z",
"environment": "production",
"hit429": false,
"rateLimitHeaders": {
"x-ratelimit-remaining-requests": "43",
"x-ratelimit-reset-requests": "2026-05-07T12:01:00Z"
}
}Supported HTTP clients
| Client | Supported | Notes |
|---|---|---|
| global fetch | ✅ | Auto-patched on init |
| axios | ✅ | Use patchAxios() — see below |
| node-fetch | ⚠️ | Only if assigned to globalThis.fetch |
| undici | ⚠️ | Only if assigned to globalThis.fetch |
Axios
Axios doesn't use globalThis.fetch, so it needs an explicit interceptor. Call patchAxios() once after QuotaWatch.init():
import axios from 'axios';
import { QuotaWatch, patchAxios } from 'quotawatch';
const qw = QuotaWatch.init({ ... });
// Patch the default axios instance
patchAxios(axios, qw);
// Or patch a custom instance
const client = axios.create({ baseURL: 'https://api.openai.com' });
patchAxios(client, qw);
// Now all calls through axios are monitored automatically
const res = await axios.post('https://api.openai.com/v1/chat/completions', body);axios.create() clients, call patchAxios() on each one.Manual recording
Use instance.record()only when using clients that aren't auto-patched (e.g. a vendor SDK with a bundled HTTP transport, or a custom XMLHttpRequest wrapper). For fetch and Axios, the interceptors handle everything.
const qw = QuotaWatch.getInstance();
if (qw) {
qw.record({
api: 'MyAPI', // must match your ApiConfig name
endpoint: '/v1/resource',
method: 'POST',
status: 200,
latencyMs: 142,
timestamp: new Date().toISOString(),
environment: 'production',
hit429: false,
rateLimitHeaders: {}, // e.g. { 'x-ratelimit-remaining': '42' }
});
}API reference
QuotaWatch.init(config)
Initializes the SDK. Call once at application startup. Returns the QuotaWatch instance. Calling init() more than once is a no-op (logs a warning).
| Option | Type | Default | Description |
|---|---|---|---|
| apiKey | string | — | Required. Your project API key from Settings. |
| apis | ApiConfig[] | — | Required. APIs to monitor (see below). |
| environment | string | 'production' | Tag events by environment (production/staging/dev). |
| ingestUrl | string | 'https://ingest.quotawatch.app' | Where to send events. Set to your ingest service URL (e.g. http://localhost:3001 for local dev). |
| bufferSize | number | 500 | Max events to buffer locally before dropping oldest. |
| flushIntervalMs | number | 5000 | How often to flush buffered events (ms). |
ApiConfig
| Field | Type | Description |
|---|---|---|
| name | string | Display name — must match alert config names exactly. |
| baseUrl | string | URL prefix to match. e.g. https://api.openai.com |
| limits.requestsPerMinute | number? | Used for threshold alert calculations. |
| limits.requestsPerHour | number? | |
| limits.requestsPerDay | number? | |
| limits.requestsPerMonth | number? |
QuotaWatch.getInstance()
Returns the initialized instance, or null if not yet initialized.
instance.flush()
Manually flush buffered events. Useful before graceful shutdown:
process.on('SIGTERM', async () => {
await QuotaWatch.getInstance()?.flush();
process.exit(0);
});