Skip to content

Node.js SDK

npm install reqly-node

Node 20+ and Bun. No runtime dependencies. ESM and CommonJS. npm · Changelog

Instrument your framework

import express from "express";
import { reqlyExpress } from "reqly-node";

const app = express();
const reqly = reqlyExpress({
  serviceName: "checkout-api",
  collectorUrl: "https://reqly.example.com",
  apiKey: process.env.REQLY_API_KEY,
});
app.use(reqly);               // before your routes
// ...routes and routers...
app.use(reqly.errorHandler);  // optional, after your routes: records the error's type
import Fastify from "fastify";
import { reqlyFastify } from "reqly-node";

const app = Fastify();
await app.register(reqlyFastify({ serviceName: "checkout-api" }));
import { Hono } from "hono";
import { reqlyHono } from "reqly-node";

const app = new Hono();
app.use(reqlyHono({ serviceName: "checkout-api" }));
import Koa from "koa";
import Router from "@koa/router";
import { reqlyKoa } from "reqly-node";

const app = new Koa();
app.use(reqlyKoa({ serviceName: "checkout-api" }));  // first, before your routers
app.use(router.routes()).use(router.allowedMethods());

Routes are @koa/router templates, router prefixes included (/api/users/:id). A thrown error is recorded with its status (500, unless it carries one like ctx.throw(404)) and, for 5xx, its type; then it's rethrown for Koa to handle.

import { NestFactory } from "@nestjs/core";
import { reqlyNest } from "reqly-node";

const app = await NestFactory.create(AppModule);   // Express or Fastify adapter
reqlyNest(app, { serviceName: "checkout-api" });   // before app.listen()
await app.listen(3000);

Routes include the global prefix and controller path. Nest's catch-all 404 is recorded as __unmatched__. A global interceptor records the type of exceptions that become 5xx; HttpExceptions below 500 (NotFoundException, BadRequestException, …) aren't counted as errors. The returned object has client for shutdown().

For node:http, or a framework without an integration, wrap the (req, res) handler and give a routeResolver that returns the route template. Only the router knows it, so without one every request is recorded as __unmatched__, never as the raw path.

import http from "node:http";
import { reqlyHttp } from "reqly-node";

const handler = reqlyHttp(app, {
  serviceName: "checkout-api",
  routeResolver: (req) => req.matchedRoute,  // read when the response finishes
});
http.createServer(handler).listen(3000);

An error the handler throws, or a promise it returns rejects with, is recorded with its type and rethrown.

const { reqlyExpress } = require("reqly-node");

Routes are recorded as templates (/api/users/:id, including router mount paths), never as raw paths. Requests that no route matched are recorded as __unmatched__.

Who is calling

reqlyExpress({ consumerHeader: "x-api-key", consumerSalt: process.env.REQLY_CONSUMER_SALT });

// or any logic:
reqlyExpress({ consumer: (info) => info.headers["x-tenant-id"] });

Ids are hashed with HMAC-SHA256 and your salt before they leave the process, so raw API keys never reach the collector. hashConsumer: false sends them as they are; use it only for non-secret ids such as tenant names. See API consumers.

LLM cost per route

import { recordLlmResponse, recordLlmUsage } from "reqly-node";

const completion = await openai.chat.completions.create({ model: "gpt-4o-mini", messages });
recordLlmResponse(completion);             // OpenAI- or Anthropic-style responses
recordLlmUsage("gpt-4o-mini", 1200, 240);  // or explicitly

Usage is tied to the request through AsyncLocalStorage, so it works after awaits. See LLM cost.

Configuration

Options passed to reqlyExpress() / reqlyFastify() / reqlyHono() (or to a shared new ReqlyClient(options)), else environment variables, else defaults:

Option Environment variable Default
serviceName REQLY_SERVICE_NAME npm_package_name, else unnamed-service
collectorUrl REQLY_COLLECTOR_URL http://localhost:8000
apiKey REQLY_API_KEY none
release REQLY_RELEASE, then GITHUB_SHA, CI_COMMIT_SHA, RENDER_GIT_COMMIT, VERCEL_GIT_COMMIT_SHA, … auto-detected
environment REQLY_ENVIRONMENT none
sampleRate REQLY_SAMPLE_RATE 1
flushIntervalMs REQLY_FLUSH_INTERVAL_MS (or REQLY_FLUSH_INTERVAL_SECONDS) 5000 (at least 100); a full batch is sent at once
maxBatchSize REQLY_MAX_BATCH_SIZE 200 (kept within 1–1,000, the collector's limit)
maxQueueSize REQLY_MAX_QUEUE_SIZE 2000 (at least 1)
ignoreRoutes REQLY_IGNORE_ROUTES (comma-separated) /health,/metrics
consumerHeader REQLY_CONSUMER_HEADER none
consumer — none: (info) => string | undefined
consumerSalt REQLY_CONSUMER_SALT none: set it
hashConsumer REQLY_HASH_CONSUMER true
pushOpenapi REQLY_PUSH_OPENAPI false. See OpenAPI drift

Shutting down

Events still queued when the process exits on its own are sent automatically. On SIGTERM, or before calling process.exit(), flush them first:

process.on("SIGTERM", async () => {
  await reqly.client.shutdown();
  process.exit(0);
});

shutdown() is bounded so a collector outage can't hold up a deploy: it makes one attempt per batch, stops at the first failure and returns within 5 seconds. Events it couldn't send are counted in client.stats.dropped.

Guarantees

  • Small overhead: about 6 µs per request on Hono, 10 µs on Fastify and 27 µs on Express (benchmark).
  • Fail-open: nothing the SDK does can throw into your request path. An internal error disables instrumentation and logs once.
  • Non-blocking: events are queued in memory and sent in batches by a timer that never keeps the process alive. The queue is bounded, and the oldest events are dropped first.
  • Retries: 408, 429, 5xx and network errors are retried with backoff. Other 4xx responses drop the batch.
  • Abandoned requests: a request the client gives up on before the response finishes is recorded as 499 (client closed request), not as an error.

Runtimes

  • Node.js 20+: every integration.
  • Bun: Hono on Bun.serve and reqlyHttp on Bun's node:http are tested in CI.
  • Cloudflare Workers, Deno Deploy and other edge runtimes: not supported yet. A Worker can't keep a background flush timer between requests, so use OpenTelemetry there.