Disclosure: I work on nRouter. This post was drafted with AI assistance and reviewed by me before publishing.

JavaScript SDKs for LLM APIs usually fail in the same place: everything is any until production teaches you otherwise. So when I read our own TypeScript / Node.js SDK guide, the parts I checked first weren't the happy path — they were the types, the metadata, and what an error looks like when it lands in a catch block. Here's what the guide actually specifies.

One package, more runtimes than you'd expect

npm install @nrouter_ai/sdk 

The client bindings cover Node.js, Bun, Deno and edge runtimes against the same gateway, https://api.nrouter.ai/v1. Setup is deliberately boring:

export NROUTER_API_KEY="sk-nrouter-xxxxxxxxxxxx" 
import { nRouter } from "@nrouter_ai/sdk"; // Reads NROUTER_API_KEY and targets https://api.nrouter.ai/v1 const client = new nRouter(); const response = await client.chat.completions.create({ model: "gpt-5.4-mini", messages: [{ role: "user", content: "Explain HTTP keepalive for LLM proxies." }], }); console.log(response.choices[0].message.content); 

No key in the constructor, no config object to pass around your codebase. The environment resolution is automatic, which matters more than it sounds like when the same code has to run in a Node service and an edge function.

The types cover both wire formats

The guide's strongest claim is complete type safety: native TypeScript definitions for Chat Completions, for Anthropic Messages, and for the gateway's own parameters. That's the difference between an SDK and a fetch wrapper. If the request shape is wrong, you find out in the editor, not in a 400 response at runtime — and because one client speaks both formats, switching the model string doesn't mean switching libraries or losing your types.

As with the Python client, response metadata is parsed for you: headers land on client.lastResponse with the request's trace ID and cost figure, so logging and support lookups don't start with "can you reproduce it?"

Streaming is a for-await loop

Streaming runs over Server-Sent Events, and the consumption shape is the one you'd write by hand anyway:

const stream = await client.chat.completions.create({ model: "claude-haiku-4-5-20251001", messages: [{ role: "user", content: "Write a poem about distributed systems." }], stream: true, }); for await (const chunk of stream) { process.stdout.write(chunk.choices[0]?.delta?.content || ""); } console.log(); 

Note the optional chaining on the delta — chunks that carry no content are a real thing in SSE streams, and the example doesn't pretend otherwise.

Errors: a typed guardrail error, then a status switch

The error section is structured in two layers. First, the specific case gets its own type:

import { nRouter, APIError, nRouterGuardrailBlockedError } from "@nrouter_ai/sdk"; try { const res = await client.chat.completions.create({ model: "gpt-5.4-mini", messages: [{ role: "user", content: "Run analysis" }], }); } catch (err) { if (err instanceof nRouterGuardrailBlockedError) { console.error(`Guardrail blocked request [${err.requestId}] (posture: ${err.guardrails}):`, err.message); } else if (err instanceof APIError) { switch (err.status) { case 401: console.error("Authentication failed: Check NROUTER_API_KEY"); break; case 429: console.error("Rate limit reached. Backing off before retry."); break; // 400, 402 and a default gateway case are in the guide } } else { console.error("Network or unexpected error:", err); } } 

Two details stand out. The guardrail error carries the request ID and the guardrail posture that blocked it, so a policy rejection is debuggable instead of mysterious. And the switch forces you to decide what each status means for your app — a 401 is a config bug, a 429 is a backoff, and neither is the other's retry policy. Anything that isn't an API error at all falls through to the network case, which is where it belongs.

Working examples ship with it

The guide closes with an examples table: a TypeScript quickstart, a complete showcase file (multi-turn memory, prompt templates, conflict resolution), a Vercel AI SDK integration, and an end-to-end test suite. Example files you can actually run beat prose about features, every time.

The guide

Full page, complete examples included: https://dev.nrouter.ai/docs/sdks/nodejs

If you're building on TypeScript: what does your current LLM client give you in the catch block — a typed error you can switch on, or a string you're about to regex? That gap is most of what this guide is about.