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.
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:
# 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 reqkeyimport { ReqKey } from "reqkey"; // ESM
const { ReqKey } = require("reqkey"); // CommonJSThe 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:
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:
request.reqkey and request.reqkeyRequestId (type with ReqKeyExpressRequest or ReqKeyNodeRequest)request.reqkey and request.reqkeyRequestIdcontext.state.reqkey and context.state.reqkeyRequestId@ReqKeyDecision() and @ReqKeyRequestId() controller parametersgetReqKey(request) — and request.reqkey when the runtime allows itResponses also carry the decision back to your consumer when available:
X-ReqKey-Request-ID— traceable id for support and log correlation.X-ReqKey-Credits-LimitandX-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: "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 allIn 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:
// 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")Paths & credit costs
Excluded traffic is never validated, charged, or recorded — for anyone. Credit costs can be a constant or a per-request resolver:
// 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:
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"],
});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:
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.
projectKeystringrequiredYour project credential, sent to ReqKey as the Bearer token. Read it from the REQKEY_PROJECT_KEY environment variable — never hard-code it.
apiIdstringrequiredThe 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.
enabledbooleanOne switch to bypass ReqKey entirely — handy as a local-development toggle that leaves your middleware layout untouched.
rootKeystringBackward-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.
keyNamestringThe 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.
getConsumerKeyresolverFull control — a sync or async function that extracts the key from anywhere in the request.
creditsnumber | resolverThe 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.
shouldProtectresolverA 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.
captureQueryParamsbooleanRecord query strings with each analytics event. Off by default.
captureRequestHeadersbooleanRecord request headers — auth headers, cookies, and consumer keys are always stripped first.
captureResponseHeadersbooleanRecord response headers, with the same always-on redaction.
captureResponseBodybooleanRecord textual response bodies, capped at the first 1,000 characters.
captureClientIpbooleanRecord the caller’s IP address, proxy-aware out of the box.
excludedHeadersstring[]Strip additional headers from captured analytics, beyond the built-in redaction list.
consumerNameResolverresolverLabel events with a human-readable customer name from a header or another trusted source.
clientIpResolverresolverBehind a trusted proxy? Tell the SDK exactly which header holds the real client IP.
pathResolverresolverNormalize route names before they are recorded — collapse ids so endpoints group cleanly.
requestIdResolverresolverIn ingest-only mode: correlate events to a validation performed by another middleware.
ingestDeniedRequestsbooleanRecord 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.
onErrorcallbackCalled 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.
timeoutMsnumberMilliseconds 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.