framework-guard is an Express-first middleware toolkit that ships JWT auth, error handling, request context, logging, security headers, rate limiting, guarded async handlers, and Zod validation in one cohesive package. The library builds as native ESM (with CommonJS compatibility) and includes full TypeScript types.
- Request context + correlation IDs through
createRequestContextand enhancedrequestId, attached toreq.context,res.locals, logs, and errors. - Guard-aware rate limiting with
rateLimit, built-in memory store, and pluggable key derivation (user/API key/IP/request ID). - Async-safe handlers with
guarded(handler)so thrown errors becomeAppErrors and successful responses auto-wrap injsonSuccess. - JWT auth middleware (+ helpers
signJwt,verifyJwt) with customizable token extraction and request attachment. - Resilient error surface via
AppError,errorHandler,notFound, and JSON response helpers. - Security middleware wrappers (
withCors,withHelmet) and request observability (logRequests). - Zod-powered validation (
validate) forbody,query, andparams. - Dual ESM/CJS bundles, declaration files, and sourcemaps generated via
tsup.
- Node.js ≥ 24 (tested on Node 24.x in CI). An
.nvmrcis included for local parity. - npm ≥ 10 (or pnpm/yarn with the equivalent lifecycle scripts).
- TypeScript 5.6+ if you consume the source types directly.
npm install framework-guard express jsonwebtoken cors helmet zodFor contributors:
npm install
npm run verify| Variable | Description | Example |
|---|---|---|
NODE_ENV |
Enables production optimizations in Express and logging | production |
JWT_SECRET |
Symmetric secret (or base64) for signing/verifying tokens | super-secret-value |
LOG_LEVEL |
Propagated to your logger (pino, etc.) |
info |
REQUEST_ID_HEADER |
Override default X-Request-Id header name |
X-Correlation-Id |
TRUST_REQUEST_ID |
Set to false to always mint IDs instead of trusting headers |
false |
CORS_ORIGINS |
Supply comma-separated origins when composing withCors() |
https://app.example.com |
Document these in your README or .env.example when publishing downstream packages.
npm run build– clean + bundle ESM/CJS artifacts via tsup (targeting Node 24).npm run lint– ESLint with@typescript-eslintand import ordering rules.npm run type-check–tsc --noEmitfor strict typing.npm run test– Vitest (unit + integration). Split commands exist astest:unit/test:integration.npm run verify– Convenience script (lint + type-check + tests) used in CI andprepublishOnly.npm run example:express– Runs the Express sample atexamples/express-basic.ts.npm run release– Executessemantic-release(invoked by GitHub Actionsrelease.yml).npm run audit– Fails fast on high-severity vulnerabilities vianpm audit --audit-level=high.
import express from 'express';
import pino from 'pino';
import { z } from 'zod';
import {
createRequestContext,
errorHandler,
guarded,
jwtAuth,
logRequests,
notFound,
rateLimit,
signJwt,
validate,
withCors,
withHelmet,
} from 'framework-guard';
const logger = pino({ level: process.env.LOG_LEVEL ?? 'info' });
const JWT_SECRET = process.env.JWT_SECRET ?? 'change-me';
const app = express();
app.use(express.json());
app.use(withCors());
app.use(withHelmet());
app.use(
createRequestContext({
header: process.env.REQUEST_ID_HEADER ?? 'X-Request-Id',
trustHeader: process.env.TRUST_REQUEST_ID !== 'false',
}),
);
app.use(logRequests({ logger }));
app.post('/login', (req, res) => {
const { username } = req.body ?? {};
if (!username) {
return res.status(400).json({ success: false, error: { message: 'username required' } });
}
const token = signJwt({ sub: username }, JWT_SECRET, { expiresIn: '1h' });
res.json({ success: true, data: { token } });
});
app.use(
'/api',
rateLimit({
windowMs: 60_000,
max: 100,
key: (ctx) => (ctx.context?.user as { sub?: string } | undefined)?.sub ?? ctx.ip,
}),
);
app.use(
'/api',
jwtAuth({
secret: JWT_SECRET,
algorithms: ['HS256'],
requestProperty: 'user',
}),
);
app.get(
'/api/me',
guarded(async (req) => {
return { user: req.context?.user, requestId: req.context?.requestId };
}),
);
const echoBody = z.object({ message: z.string().min(1) });
app.post(
'/api/echo',
validate({ body: echoBody }),
guarded(async (req) => ({ body: req.body, user: req.context?.user })),
);
app.use(notFound());
app.use(errorHandler());
app.listen(3000, () => logger.info('listening on http://localhost:3000'));The same middleware composes nicely in serverless runtimes (Vercel, AWS Lambda, Cloudflare Workers with adapters). Example with Vercel’s @vercel/node entry:
import type { VercelRequest, VercelResponse } from '@vercel/node';
import express from 'express';
import serverlessHttp from 'serverless-http';
import { requestId, logRequests, withCors, errorHandler, notFound } from 'framework-guard';
const app = express();
app.use(express.json());
app.use(requestId());
app.use(logRequests({ logger: console }));
app.use(withCors());
// register routes + middleware
app.use(notFound());
app.use(errorHandler());
const handler = serverlessHttp(app);
export default (req: VercelRequest, res: VercelResponse) => handler(req, res);For AWS Lambda or Docker images, pair the middleware with your preferred adapter (e.g., aws-serverless-express, @apollo/server). Ensure NODE_ENV=production, and set LOG_LEVEL / JWT_SECRET through your secret manager of choice.
- Unit tests live under
tests/unitand mock Express primitives to focus on middleware behavior. - Integration tests (see
tests/integration/middleware-stack.spec.ts) spin up an actual Express app, run Supertest through JWT/validation stacks, and assert real HTTP responses. - Run
npm run test:unit,npm run test:integration, ornpm run test(all suites). CI executes both plus lint/type-check/build/audit on every push and PR.
docs/production-checklist.mdcaptures the full production-hardening checklist (Node/TypeScript setup, testing, security).docs/openapi.md(coming fromzod-openapi) documents how to generateopenapi.jsondirectly from your Zod schemas and validate requests/responses withexpress-openapi-validator.- Link to your spec (e.g.,
/openapi.json) in downstream READMEs and optionally serve Swagger UI or Redoc at/docs.
- Use structured logging (Pino) instead of
console.logto avoid synchronous stdio in production. ThelogRequestsmiddleware accepts any logger with aninfomethod and automatically includes correlation IDs whencreateRequestContextruns first. - Keep middleware async-friendly—do not add blocking operations or synchronous crypto in hot paths. Offload CPU-heavy work to job queues/workers.
- Run
npm run auditregularly and enable Dependabot/Snyk for dependency drift. - Set security headers via
withHelmet, configure strict CORS withwithCors, and keep JWT secrets in a secret manager (never committed). - Run behind a proxy (NGINX, Cloudflare) for TLS, caching, and gzip/Brotli compression. Document expected proxy headers (
X-Forwarded-*).
.github/workflows/ci.ymlruns lint, type-check, unit/integration tests, build, and audit on Node 24.x..github/workflows/release.ymllistens for successful CI runs onmainand executessemantic-release, which bumps versions, updatesCHANGELOG.md, publishes to npm, and tags Git.- Versioning follows SemVer with Conventional Commits. Start at
1.0.0for the first stable release, and document breaking changes plus migration notes in the changelog. Request context, guarded handlers, and rate limiting landed as SemVer-minor additions in the 1.x line.
- See CONTRIBUTING.md for workflow details.
- Security issues? Please open a private advisory via GitHub Security Advisories or email the maintainer listed in
package.json. - For a deeper production hardening guide, read docs/production-checklist.md.
import { createRequestContext, guarded, rateLimit } from 'framework-guard';
app.use(
createRequestContext({
header: 'X-Correlation-Id',
generate: 'uuid',
enrich: (req, ctx) => ({ metadata: { route: req.originalUrl } }),
}),
);
app.use(
rateLimit({
windowMs: 15 * 1000,
max: 50,
key: (ctx) => ctx.user?.id ?? ctx.apiKey ?? ctx.ip,
}),
);
app.get(
'/orders',
guarded(async (req) => fetchOrders({ userId: req.context?.user?.id })),
);All downstream middleware (errorHandler, logRequests, etc.) share the same context so correlation IDs and user data propagate everywhere.