---
title: "Node.js/JavaScript SDK Integration Best Practices"
answer: "coolhand-node patches fetch and https.request at initialization time. Import order, module system (CJS vs native ESM), and when you construct AI client objects all affect whether that patch actually intercepts your calls. This guide covers the three most common ways Node/JS integrations silently stop being monitored, and how to verify interception in CI instead of trusting a 200 response."
category: "Logs \u0026 API"
tags: []
published: 2026-09-22T14:26:56Z
updated: 2026-09-13T16:14:32Z
canonical_url: https://coolhandlabs.com/help/nodejs-sdk-integration-best-practices
index_url: https://coolhandlabs.com/help.md
---

# Node.js/JavaScript SDK Integration Best Practices

coolhand-node patches fetch and https.request at initialization time. Import order, module system (CJS vs native ESM), and when you construct AI client objects all affect whether that patch actually intercepts your calls. This guide covers the three most common ways Node/JS integrations silently stop being monitored, and how to verify interception in CI instead of trusting a 200 response.

Coolhand's Node.js SDK (`coolhand-node`) monitors LLM calls by patching `fetch` and `https.request` when you call `initializeGlobalMonitoring()` (or load the `coolhand-node/auto-monitor` entry point). That's a runtime patch, not a build-time transformation — so *when* and *how* your app loads Coolhand relative to your AI SDKs determines whether monitoring actually captures anything. All three pitfalls below share the same symptom: your app works perfectly, returns correct responses, and Coolhand logs nothing — no error, no warning.

## Initialize monitoring before importing AI libraries

**Why it matters:** Some AI client libraries reference the patched functions (`https.request`, `fetch`) as soon as their module body runs, not just when you construct a client. If Coolhand hasn't patched them yet, the library binds to the original, unpatched versions.

```javascript
// ❌ Wrong order — AI library loads before monitoring patches anything
const { ChatOpenAI } = require('@langchain/openai');
const { initializeGlobalMonitoring } = require('coolhand-node');
initializeGlobalMonitoring({ apiKey: process.env.COOLHAND_API_KEY });
```

```javascript
// ✅ Correct order — monitoring patches first
const { initializeGlobalMonitoring } = require('coolhand-node');
initializeGlobalMonitoring({ apiKey: process.env.COOLHAND_API_KEY });
const { ChatOpenAI } = require('@langchain/openai');
```

## Native ESM projects need the `--import` flag

**Why it matters:** In a CommonJS project, `require()` calls run top-to-bottom, so the correct order above is enough. In a native ESM project (`"type": "module"` in `package.json`), static `import` statements are hoisted and evaluated *before any module body runs* — including a sibling `import 'coolhand-node/auto-monitor'` placed first in the file. The AI library's module can still capture the original `https.request` before Coolhand's patch applies, even though the import statements look correctly ordered in the source.

```javascript
// ❌ Broken in native ESM — imports are hoisted, so source order doesn't guarantee patch order
import 'coolhand-node/auto-monitor';
import { ChatOpenAI } from '@langchain/openai'; // may still capture the original https.request
```

```bash
# ✅ Fix (Node ≥ 20.6) — run the patch outside your app's module graph, before anything else loads
node --import 'coolhand-node/auto-monitor' app.mjs
```

```json
{ "scripts": { "start": "node --import 'coolhand-node/auto-monitor' dist/app.mjs" } }
```

```javascript
// ✅ Fallback for Node < 20.6 — dynamic import runs after the patch is applied
import { initializeGlobalMonitoring } from 'coolhand-node';

await initializeGlobalMonitoring({ apiKey: process.env.COOLHAND_API_KEY });
const { ChatOpenAI } = await import('@langchain/openai'); // loads after the patch
```

## Never construct AI clients at module scope

**Why it matters:** This is the subtlest of the three, because it isn't about import/require order at all. Some client libraries — including the `openai` SDK (v5+) — capture `fetch` **by value, inside their constructor**, instead of looking it up fresh on every request:

```javascript
// Inside the library's own constructor
this.fetch = options.fetch ?? globalThis.fetch;
```

If you construct that client *before* `initializeGlobalMonitoring()` runs, `this.fetch` permanently holds the original, unpatched `fetch` — even though `globalThis.fetch` itself gets reassigned to the patched version moments later. Every call that client makes from then on silently bypasses monitoring. This bites both CommonJS and ESM projects alike, and it bites you even if your `require`/`import` order is otherwise correct, because the failure happens at *construction* time, not *module load* time. Constructing a client at module scope — a extremely common, completely reasonable-looking pattern — is enough to trigger it.

```javascript
// ❌ Broken — client constructed before monitoring is initialized
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // captures unpatched fetch

await initializeGlobalMonitoring({ apiKey: process.env.COOLHAND_API_KEY });
// openai.chat.completions.create(...) never gets intercepted — no error, no warning
```

```javascript
// ✅ Fixed — construct the client lazily, after monitoring is guaranteed to be initialized
import OpenAI from 'openai';

let openai;
function getOpenAIClient() {
  openai ??= new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
  return openai;
}

await initializeGlobalMonitoring({ apiKey: process.env.COOLHAND_API_KEY });
await getOpenAIClient().chat.completions.create({ /* ... */ }); // ✅ intercepted
```

**General rule:** never construct an AI client instance at module scope in an app that relies on runtime monitoring patching. Construct clients lazily, on first use, so client construction can never race ahead of monitoring initialization — regardless of whether the client library caches `fetch` by value.

## Verify interception in CI, not just a 200 response

A demo or example app returning a correct response proves your AI provider integration works. It does **not** prove Coolhand is monitoring it — all three pitfalls above produce a fully working app with silently missing logs. If you maintain an example or integration test for your Coolhand setup, assert programmatically that a request was actually intercepted (e.g. check for the log line your monitoring emits, or stub the AI provider and assert Coolhand's capture hook fired), not just that the app returned success.

## Related

- [Python SDK Integration Best Practices](https://coolhandlabs.com/help/python-sdk-integration-best-practices.md)
- [Ruby SDK Integration Best Practices](https://coolhandlabs.com/help/ruby-sdk-integration-best-practices.md)


---

Related:
- [Python SDK Integration Best Practices](https://coolhandlabs.com/help/python-sdk-integration-best-practices.md)
- [Ruby SDK Integration Best Practices](https://coolhandlabs.com/help/ruby-sdk-integration-best-practices.md)
- [When should I use batch requests?](https://coolhandlabs.com/help/how-to-use-batch-requests.md)
- [LLMs and System One models: what's different, and when to use which](https://coolhandlabs.com/help/llms-vs-system-one-models.md)

[← All help articles](https://coolhandlabs.com/help.md) · [Logs & API](https://coolhandlabs.com/help/category/logs-and-api.md)
