ReqKey.docs
SDKs

Node.js SDK

The official TypeScript and Node.js SDK for API key validation, credit metering, consumer rate limits, and correlated traffic analytics. One npm package contains the shared async client and framework adapters for Express, Next.js, NestJS, Fastify, Koa, and plain Node — shipped as both ESM and CommonJS with complete type declarations.

Prefer it filled in for you?
The dashboard's Setup Guide generates this exact integration from a few questions — with your real API id, key location, and a copy-ready brief for AI coding agents.

Install

Node.js 20 or newer is required. The core has no framework dependency; Fastify and Koa are optional peers, so installing reqkey never pulls in a web framework. Both module systems are supported:

terminal
# The package ships the shared client + every framework adapter
npm install reqkey

# Install the web framework alongside it when its adapter needs the peer
npm install reqkey express
npm install reqkey fastify
npm install reqkey koa

# NestJS projects already have @nestjs/common and @nestjs/core
npm install reqkey
modules
import { ReqKey } from "reqkey";        // ESM
const { ReqKey } = require("reqkey");  // CommonJS

The SDK reads your project credential from the REQKEY_PROJECT_KEY environment variable. Copy the real key from the dashboard, and keep it out of source control:

.env / shell
export REQKEY_PROJECT_KEY="reqkey_your_project_key"

Quickstart

Add the adapter once, where your application is created. Every route is guarded automatically — your handlers stay untouched, and the application never calls /key/validate or /ingest directly.

import express from "express";
import { reqkey } from "reqkey/express";
import type { ReqKeyExpressRequest } from "reqkey/express";

const app = express();

app.use(
  reqkey({
    projectKey: process.env.REQKEY_PROJECT_KEY,
    apiId: "api_payments",
    mode: "both",                 // validate keys AND record analytics
    keyName: "x-startup-key",     // where consumers send their key
    excludePaths: ["/health", "/docs/*"],
  }),
);

app.post("/payments", (request, response) => {
  const decision = (request as ReqKeyExpressRequest).reqkey;
  response.status(201).json({ created: true, creditsRemaining: decision?.creditsRemaining });
});

app.listen(3000);

Next.js also ships withReqKeyPages for the Pages Router. NestJS works on both its Express and Fastify platforms, and also exposes reqkeyNest(options) for app.use(). For scripts, workers, and custom frameworks, use the direct ReqKey client.

Reading the decision

After successful validation, the full decision — the consumer, credits remaining, and a traceable request id — is attached to the request:

Express / Node
request.reqkey and request.reqkeyRequestId (type with ReqKeyExpressRequest or ReqKeyNodeRequest)
Fastify
request.reqkey and request.reqkeyRequestId
Koa
context.state.reqkey and context.state.reqkeyRequestId
NestJS
@ReqKeyDecision() and @ReqKeyRequestId() controller parameters
Next.js
getReqKey(request) — and request.reqkey when the runtime allows it

Responses also carry the decision back to your consumer when available:

  • X-ReqKey-Request-ID — traceable id for support and log correlation.
  • X-ReqKey-Credits-Limit and X-ReqKey-Credits-Remaining — the consumer's balance.
  • X-ReqKey-Validation-Time-Ms — how long the key check took.

When fail-open is active, a service failure is exposed as reqkeyError in the same location, or via getReqKeyError(request) for Next.js.

Modes

One switch decides what the adapter does per request:

mode
mode: "both"       // validate keys AND record analytics (default)
mode: "validate"   // key checks and credit deduction only — nothing recorded
mode: "ingest"     // analytics only — no key checks at all

In mode: "both", denied requests — missing keys, exhausted credits, rate-limit hits — are recorded too, so abuse is visible in your dashboards. Set ingestDeniedRequests: false if you deliberately don't want those events. In ingest-only mode, another component's validation can be correlated via requestIdResolver.

Where the key comes from

Match however your consumers already send credentials:

key extraction
// Custom header (recommended)
keyLocation: "header", keyName: "x-startup-key", keyScheme: "raw"

// Authorization: Bearer <key>
keyLocation: "header", keyName: "Authorization", keyScheme: "bearer"

// ?api_key=<key> — easy to test, but URLs end up in logs
keyLocation: "query", keyName: "api_key"

// Cookie — sent automatically by browsers
keyLocation: "cookie", keyName: "api_key"

// Or take full control with a sync or async extractor
getConsumerKey: ({ headers }) => headers.get("X-Custom-Key")
Avoid reading keys from the request body
Authentication runs before your handler, and consuming the body can interfere with downstream parsing. Headers are recommended over query parameters — URLs are retained in browser history and proxy logs.

Paths & credit costs

Excluded traffic is never validated, charged, or recorded — for anyone. Credit costs can be a constant or a per-request resolver:

routing & pricing
// Exact paths or trailing-* prefixes: never validated, charged, or recorded
excludePaths: ["/health", "/openapi.json", "/docs/*", "/cron/*"]

// Or decide per request with a sync or async resolver
shouldProtect: ({ path }) => path.startsWith("/api/")

// Charge different endpoints differently (non-negative integers)
credits: ({ method, path }) => {
  if (method === "POST" && path === "/images") return 5;
  if (path.startsWith("/reports/")) return 2;
  return 1;
}

The SDK deliberately never auto-retries /key/validate, because validation deducts credits — safe retries would require server-side idempotency.

Analytics capture

By default each event records the method, normalized path, status, latency, and user agent — correlated to the validated consumer. Everything else is opt-in:

server.ts
reqkey({
  projectKey: process.env.REQKEY_PROJECT_KEY,
  apiId: "api_payments",
  mode: "both",

  // All off by default — turn on what your dashboards need
  captureQueryParams: true,
  captureRequestHeaders: true,
  captureResponseHeaders: true,
  captureResponseBody: true,     // textual content, first 1,000 chars
  captureClientIp: true,

  // Strip additional headers beyond the built-in redaction list
  excludedHeaders: ["X-RapidAPI-Proxy-Secret", "X-Vercel-OIDC-Token"],
});
Redaction is always on
Authorization, cookies, Set-Cookie, common API-key headers, and your configured consumer-key header are always stripped before anything leaves your server — even with header capture enabled. Text response bodies are capped at 1,000 characters; binary, compressed, and streaming bodies are omitted.

Failure behavior

Validation fails closed by default: if ReqKey times out or is unreachable, the adapter returns 503 without running your handler. Prefer availability? Fail open instead — transport failures let requests through, while explicitly invalid, exhausted, forbidden, and rate-limited keys are still always denied. Either way, onError tells you it happened:

server.ts
reqkey({
  projectKey: process.env.REQKEY_PROJECT_KEY,
  apiId: "api_payments",
  failureMode: "closed",          // or "open": let requests through
  onError: async (event) => {
    // Provider-neutral: post it to Slack, Discord, PagerDuty, ...
    await sendToYourAlertProvider({
      operation: event.operation,   // "validate" or "ingest"
      message: event.message,
      method: event.method,
      path: event.path,
    });
  },
});

The event intentionally excludes API keys, project credentials, request headers, query parameters, and bodies. Errors thrown by the callback are ignored, so alerting can never replace your application response.

Configuration reference

Every option below is a property on the options object passed to any adapter — reqkey(...), withReqKey(...), or ReqKeyModule.forRoot(...). NestJS also accepts forRootAsync for configuration services and secret managers.

Adapter options
projectKeystringrequired

Your project credential, sent to ReqKey as the Bearer token. Read it from the REQKEY_PROJECT_KEY environment variable — never hard-code it.

apiIdstringrequired

The ReqKey API this application serves — validation and analytics are scoped to it.

mode"both" | "validate" | "ingest"

What the middleware does per request: guard keys and record analytics (the default), key checks only, or analytics only.

enabledboolean

One switch to bypass ReqKey entirely — handy as a local-development toggle that leaves your middleware layout untouched.

rootKeystring

Backward-compatible alias for projectKey. Never pass both.

keyLocation"header" | "query" | "cookie"

Where the key travels. Headers are recommended — query strings end up in logs and browser history.

keyNamestring

The customer-facing header, query parameter, or cookie name. Make it yours: x-startup-key.

keyScheme"raw" | "bearer"

For headers: the raw key value, or a standard Authorization: Bearer token.

getConsumerKeyresolver

Full control — a sync or async function that extracts the key from anywhere in the request.

creditsnumber | resolver

The cost per successful request. Pass a function to charge heavy endpoints more than light ones.

excludePathsstring[]

Paths that stay public — never validated, charged, or recorded. Exact matches or trailing-* prefixes.

shouldProtectresolver

A per-request predicate for when patterns are not enough — protect exactly the routes you choose.

skipMethodsstring[]

HTTP methods that bypass ReqKey. Defaults to ["OPTIONS"] so CORS preflights stay free.

captureQueryParamsboolean

Record query strings with each analytics event. Off by default.

captureRequestHeadersboolean

Record request headers — auth headers, cookies, and consumer keys are always stripped first.

captureResponseHeadersboolean

Record response headers, with the same always-on redaction.

captureResponseBodyboolean

Record textual response bodies, capped at the first 1,000 characters.

captureClientIpboolean

Record the caller’s IP address, proxy-aware out of the box.

excludedHeadersstring[]

Strip additional headers from captured analytics, beyond the built-in redaction list.

consumerNameResolverresolver

Label events with a human-readable customer name from a header or another trusted source.

clientIpResolverresolver

Behind a trusted proxy? Tell the SDK exactly which header holds the real client IP.

pathResolverresolver

Normalize route names before they are recorded — collapse ids so endpoints group cleanly.

requestIdResolverresolver

In ingest-only mode: correlate events to a validation performed by another middleware.

ingestDeniedRequestsboolean

Record denied traffic too — invalid keys, exhausted credits, rate-limit hits — so abuse is visible. On by default.

failureMode"closed" | "open"

Fail closed (block with a 503, the default) or fail open (let requests through unvalidated). Invalid and exhausted keys are always denied either way.

onErrorcallback

Called on ReqKey service failures with a provider-neutral event — wire it to Slack, Discord, or PagerDuty. No request secrets included.

errorMessagesRecord<string, string>

Rewrite the customer-facing denial messages in your own voice, per error type.

timeoutMsnumber

Milliseconds allowed for each ReqKey operation before the failure mode kicks in. Defaults to 2000.

Direct client

ReqKey exposes verify() and ingest() for full manual control, plus ReqKey.fromEnv() to construct from REQKEY_PROJECT_KEY. The decision object carries allowed, valid, reason, creditsRemaining, and requestId. The client is async-only — the Node SDK's HTTP APIs and supported frameworks are all asynchronous.