Skip to content

Latest commit

 

History

History

sdk-node

README.md

Reqly

reqly-node

API monitoring for Node.js that tells you what broke, when, and which deploy did it.
Express · Fastify · Hono · Koa · NestJS · plain node:http, in one line. Zero dependencies.

npm version Downloads Node 20+ Zero dependencies License: MIT

Documentation · Quickstart · Live demo · GitHub


Install

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

const app = express();
app.use(reqlyExpress({
  serviceName: "checkout-api",
  collectorUrl: "https://reqly.example.com",
  apiKey: process.env.REQLY_API_KEY,
}));

That's it: every route now reports its latency, errors, status codes and release to your own Reqly collector. No agent, no vendor account. ESM and CommonJS (require("reqly-node")) both work.

What you get

📈 Real percentiles p50 / p95 / p99 per route and per service, merged exactly across routes and time
🚀 Deploy-aware The release is picked up from your CI or host (GITHUB_SHA, RENDER_GIT_COMMIT, …); every alert says which release was running
🔔 Alerts that explain Hourly checks against each route's weekday × hour baseline, naming the host, error type and clients behind a spike, with sample trace ids; 401/403/422/429 waves too; to Slack, Discord, Teams, PagerDuty, Opsgenie, email or a webhook
💬 Ask Reqly "Why did /orders start failing?" answered from your own data, every number checked
🎯 SLOs Availability and latency objectives with error budgets and burn-rate alerts
🧾 OpenAPI drift Undocumented, unused and deprecated-but-called endpoints (pushOpenapi)
👥 API consumers Who calls your API and who an incident hit, with ids hashed in the SDK
💸 LLM cost per route Tokens and cost per route and model, counted straight from your OpenAI or Anthropic client, and an alert when it spikes

Your framework

Framework What to write
Express 4 / 5 app.use(reqlyExpress(options)) before your routes; app.use(reqly.errorHandler) after them records error types
Fastify 4 / 5 await app.register(reqlyFastify(options))
Hono app.use(reqlyHono(options)) (Node.js and Bun)
Koa 2 / 3 app.use(reqlyKoa(options)) first, before your routers
NestJS reqlyNest(app, options) before app.listen() (Express or Fastify adapter)
node:http, anything else http.createServer(reqlyHttp(handler, { routeResolver }))
// NestJS
const app = await NestFactory.create(AppModule);
reqlyNest(app, { serviceName: "checkout-api" });
await app.listen(3000);

// plain node:http: tell Reqly where the route template is
http.createServer(reqlyHttp(handler, {
  serviceName: "checkout-api",
  routeResolver: (req) => req.matchedRoute,
})).listen(3000);

Routes are recorded as templates (/api/users/:id, router prefixes included), never raw paths; requests no route matched become __unmatched__, so 404 scanners can't flood your data.

Serverless and edge

import { reqlyFetch } from "reqly-node/edge";

// Cloudflare Workers
export default { fetch: reqlyFetch(handler, (env) => ({ serviceName: "api", apiKey: env.REQLY_API_KEY })) };
// Next.js route handler
export const GET = reqlyFetch(getOrder, { serviceName: "web", route: "/api/orders/[id]" });

Web APIs only, no node: modules; each request's event is sent with waitUntil. See Serverless & edge.

Who is calling, and what it costs

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

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

const openai = instrumentOpenAI(new OpenAI()); // every call counted (instrumentAnthropic too), or:
const completion = await openai.chat.completions.create({ model: "gpt-4o-mini", messages });
recordLlmResponse(completion);              // OpenAI / Anthropic responses, or:
recordLlmUsage("gpt-4o-mini", 1200, 240);

Consumer ids are HMAC-SHA256-hashed with your salt before they leave the process; the collector never sees an API key. LLM usage follows the request through AsyncLocalStorage, so it works after any number of awaits.

Your OpenAPI spec, uploaded for you

await app.register(reqlyFastify({ serviceName: "checkout-api", pushOpenapi: true }));  // @fastify/swagger
reqlyNest(app, { serviceName: "checkout-api", pushOpenapi: SwaggerModule.createDocument(app, config) });
reqlyExpress({ serviceName: "checkout-api", pushOpenapi: () => spec });                 // any spec

Sent once, on the first request; the dashboard then shows where your spec and your real traffic disagree.

An alert looks like this

🔴 Anomaly — flask-demo /orders (Friday 15:00-16:00 UTC, z=5.37)
• error rate 30.0% vs 2.2% usual · p95 6588ms vs 1576ms usual
• running release v2 — vs v1: errors 2.6% → 33.1%, p95 2072ms → 4501ms
• 100% of errors came from host pod-3, which served 23% of requests

Configuration

Options passed to the middleware (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 CI variables auto-detected
environment REQLY_ENVIRONMENT none
sampleRate REQLY_SAMPLE_RATE 1
flushIntervalMs REQLY_FLUSH_INTERVAL_MS (or _SECONDS) 5000; a full batch is sent at once
maxBatchSize / maxQueueSize REQLY_MAX_BATCH_SIZE / REQLY_MAX_QUEUE_SIZE 200 / 2000
ignoreRoutes REQLY_IGNORE_ROUTES /health,/metrics
consumerHeader / consumer REQLY_CONSUMER_HEADER / — none
consumerSalt / hashConsumer REQLY_CONSUMER_SALT / REQLY_HASH_CONSUMER none / true
pushOpenapi REQLY_PUSH_OPENAPI false

Events still queued when the process exits on its own are sent automatically; on SIGTERM or before process.exit(), await reqly.client.shutdown() first.

Built to stay out of your way

  • ⚡ ~6 µs per request on Hono, ~10–13 µs on Fastify, ~22–27 µs on Express (benchmark)
  • 🛡️ Fail-open: nothing the SDK does can throw into your request path; an internal error turns instrumentation off and logs once
  • 🧵 Off the request path: events are batched in memory and sent by a timer that never keeps your process alive
  • 📦 Bounded: a fixed-size queue (oldest dropped first) and route templates only
  • 🔁 Safe retries on 408, 429, 5xx and network errors, deduplicated by the collector
  • ⏱️ Bounded exit: shutdown() returns within 5 s, so a collector outage never holds up a deploy
  • 📏 Streamed responses measured: server-sent events and LLM token streams get their real size (Express, Fastify, Koa, NestJS, node:http)

Compatibility

Runtimes Node.js 20+, Bun. Edge runtimes (Cloudflare Workers): use OpenTelemetry
Frameworks Express 4/5 · Fastify 4/5 · Hono · Koa 2/3 · NestJS · node:http
Modules ESM and CommonJS, TypeScript types included
Collector consumer and LLM views need 0.8.0+, pushOpenapi 0.7.0+, gzip and per-model LLM cost 0.13.0+ (older ones get plain JSON), trace ids 0.14.0+

On Python too? There's a Python SDK, and any language can report through OpenTelemetry.

Run the collector

The SDK sends to a Reqly collector you host (TimescaleDB + collector + dashboard):

git clone https://github.com/tanisheesh/reqly && cd reqly && docker compose up -d
# or on Kubernetes
helm install reqly oci://ghcr.io/tanisheesh/charts/reqly -n reqly --create-namespace

Quickstart · Deploy to production · Changelog · License: MIT (the collector is AGPL-3.0)


Tanish Poddar

Website LinkedIn GitHub Email