Skip to content

Latest commit

 

History

History
132 lines (113 loc) · 3.53 KB

File metadata and controls

132 lines (113 loc) · 3.53 KB

Production Stack Example

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());

Ordering Notes

The ordering above is intentional:

  1. withCors and withHelmet run early so headers are present on all responses.
  2. createRequestContext runs before logging, auth, and errors so they can all reuse the same request ID.
  3. The /auth limiter runs before jwtAuth because those routes are unauthenticated.
  4. The /api limiter runs after jwtAuth because its key function prefers authenticated users over IPs.
  5. notFound and errorHandler remain last.

Deployment Assumptions

  • Run behind a trusted reverse proxy or load balancer and configure app.set('trust proxy', ...) correctly.
  • Use RedisRateLimitStore instead of MemoryRateLimitStore when 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 appropriate SameSite value.
  • If you enable credentials: true in CORS, configure explicit origins instead of wildcard origins.