This example shows a production-style Express API using framework-guard with:
- request context and request IDs
- CORS and Helmet
- cookie-based JWT auth
- Redis-backed rate limiting
- structured logging
- guarded async handlers and consistent JSON errors
It assumes your app installs its own Redis client, such as redis or ioredis.
import express from 'express';
import pino from 'pino';
import { createClient } from 'redis';
import {
createRequestContext,
errorHandler,
guarded,
jwtAuth,
logRequests,
notFound,
rateLimit,
RedisRateLimitStore,
withCors,
withHelmet,
} from 'framework-guard';
const app = express();
const logger = pino({ level: process.env.LOG_LEVEL ?? 'info' });
const JWT_SECRET = process.env.JWT_SECRET ?? 'change-me';
const JWT_ISSUER = process.env.JWT_ISSUER ?? 'framework-guard';
const JWT_AUDIENCE = process.env.JWT_AUDIENCE ?? 'framework-guard-clients';
const REQUEST_ID_HEADER = process.env.REQUEST_ID_HEADER ?? 'X-Request-Id';
const COOKIE_NAME = process.env.COOKIE_NAME ?? 'session';
const redis = createClient({ url: process.env.REDIS_URL });
await redis.connect();
const redisStore = new RedisRateLimitStore({
client: redis,
prefix: 'framework-guard:rate-limit:',
});
app.set('trust proxy', 1);
app.use(express.json());
app.use(
withCors({
credentials: true,
requestIdHeader: REQUEST_ID_HEADER,
}),
);
app.use(withHelmet({ preset: 'api' }));
app.use(
createRequestContext({
header: REQUEST_ID_HEADER,
trustHeader: process.env.TRUST_REQUEST_ID !== 'false',
}),
);
app.use(logRequests({ logger }));
// Limit unauthenticated auth endpoints by IP before JWT middleware runs.
app.use(
'/auth',
rateLimit({
windowMs: 60_000,
max: 20,
store: redisStore,
key: (ctx) => ctx.ip,
}),
);
// Authenticate API routes from an HTTP-only cookie.
app.use(
'/api',
jwtAuth({
secret: JWT_SECRET,
algorithms: ['HS256'],
cookieName: COOKIE_NAME,
verifyOptions: {
issuer: JWT_ISSUER,
audience: JWT_AUDIENCE,
},
requestProperty: 'user',
}),
);
// Limit authenticated API routes by user when available, then fall back to IP.
app.use(
'/api',
rateLimit({
windowMs: 60_000,
max: 100,
store: redisStore,
key: (ctx) => (ctx.context?.user as { sub?: string } | undefined)?.sub ?? ctx.ip,
}),
);
app.get(
'/api/me',
guarded(async (req) => {
return {
user: req.context?.user,
requestId: req.context?.requestId,
};
}),
);
app.use(notFound());
app.use(errorHandler());The ordering above is intentional:
withCorsandwithHelmetrun early so headers are present on all responses.createRequestContextruns before logging, auth, and errors so they can all reuse the same request ID.- The
/authlimiter runs beforejwtAuthbecause those routes are unauthenticated. - The
/apilimiter runs afterjwtAuthbecause its key function prefers authenticated users over IPs. notFoundanderrorHandlerremain last.
- Run behind a trusted reverse proxy or load balancer and configure
app.set('trust proxy', ...)correctly. - Use
RedisRateLimitStoreinstead ofMemoryRateLimitStorewhen more than one API instance handles traffic. - Keep JWT secrets, issuer, audience, and Redis URLs in a secret manager or environment variables.
- If you use cookie auth, issue cookies with
HttpOnly,Secure, and an appropriateSameSitevalue. - If you enable
credentials: truein CORS, configure explicit origins instead of wildcard origins.