curity boundaries. The implementation below uses TypeScript with Fastify, Zod, and standard cryptographic libraries to demonstrate production-grade patterns.
1. Threat Modeling & Attack Surface Mapping
Before writing routes, map data flows, trust boundaries, and privilege escalation paths. Identify:
- Public endpoints vs internal service mesh routes
- Data classification (PII, financial, operational)
- Authentication boundaries (user, service, partner)
- Idempotency requirements for write operations
2. Authentication & Authorization Strategy
Use short-lived access tokens with refresh rotation. Implement RBAC or ABAC at the route level. Never embed business logic in token validation.
import Fastify from 'fastify';
import fastifyJwt from '@fastify/jwt';
import fastifyHelmet from '@fastify/helmet';
import fastifyRateLimit from '@fastify/rate-limit';
import fastifyCors from '@fastify/cors';
import { z } from 'zod';
const app = Fastify({ logger: true });
await app.register(fastifyJwt, {
secret: process.env.JWT_SECRET!,
sign: { expiresIn: '15m' },
verify: { algorithms: ['RS256'] }
});
await app.register(fastifyHelmet, {
contentSecurityPolicy: {
directives: { defaultSrc: ["'self'"], scriptSrc: ["'none'"] }
},
hsts: { maxAge: 31536000, includeSubDomains: true }
});
await app.register(fastifyCors, {
origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true
});
await app.register(fastifyRateLimit, {
max: 100,
timeWindow: '1 minute',
keyGenerator: (req) => req.ip || req.headers['x-forwarded-for'] as string
});
Validate at the boundary. Never trust serialized payloads. Use strict schemas that reject unknown fields.
const CreateUserSchema = z.object({
email: z.string().email(),
role: z.enum(['admin', 'editor', 'viewer']),
metadata: z.record(z.string(), z.string()).optional()
}).strict();
app.post('/api/users', {
schema: { body: CreateUserSchema },
preValidation: [app.jwt.verify],
handler: async (req, reply) => {
const { email, role, metadata } = CreateUserSchema.parse(req.body);
// Business logic with validated data
return { status: 'created', id: crypto.randomUUID() };
}
});
4. Transport & Data Security
Enforce TLS 1.3 at the load balancer or reverse proxy. Encrypt sensitive fields at rest using envelope encryption. Strip stack traces and internal headers from responses.
app.addHook('onSend', async (req, reply, payload) => {
reply.headers['x-content-type-options'] = 'nosniff';
reply.headers['x-frame-options'] = 'DENY';
reply.headers['cache-control'] = 'no-store';
return payload;
});
5. Rate Limiting & Abuse Prevention
Implement tiered limits: global, per-IP, per-user, and per-endpoint. Use token bucket algorithms for burst tolerance. Block abusive patterns, not just raw requests.
6. Logging, Monitoring & Incident Response
Structure logs with correlation IDs. Log authentication attempts, authorization failures, and schema violations. Forward to SIEM. Alert on anomaly spikes, not absolute thresholds.
Architecture Rationale
- Centralized Gateway vs Per-Service Auth: A gateway handles TLS termination, rate limiting, and token validation. Services receive pre-validated identities. This reduces cryptographic overhead and standardizes policy enforcement.
- Strict Validation Over Sanitization: Sanitization is error-prone and context-dependent. Strict schema rejection eliminates injection vectors at the boundary.
- Short-Lived Tokens + Refresh Rotation: Limits blast radius of token theft. Refresh tokens are rotated and bound to client fingerprints.
Pitfall Guide
-
Relying on Client-Side Validation Only
Client validation improves UX but provides zero security. Attackers bypass UI constraints using raw HTTP clients. Always validate server-side with strict schemas. Production practice: treat client payloads as untrusted binary streams until parsed and validated.
-
Hardcoded or Weak Secret Management
Embedding secrets in code or environment files exposes them to version control and container inspection. Use hashicorp vault, AWS Secrets Manager, or cloud KMS. Production practice: rotate secrets automatically, enforce least-privilege IAM roles, and never log secret material.
-
Overly Permissive CORS & Preflight Handling
Wildcard origins (*) with credentials enabled allow cross-site request forgery and data exfiltration. Validate origins against a allowlist. Production practice: implement dynamic origin validation, restrict methods/headers, and cache preflight responses with short TTLs.
-
Ignoring JWT Lifecycle Management
Long-lived tokens, missing expiry, or algorithm confusion (alg: none) create persistent access vectors. Production practice: enforce RS256/ES256, validate exp, iss, aud, implement token revocation lists or short TTLs with refresh rotation, and bind tokens to client fingerprints.
-
Exposing Internal Error Details & Stack Traces
Verbose errors leak framework versions, file paths, and database schemas. Production practice: return generic error codes to clients, log full traces internally with correlation IDs, and implement custom error handlers that strip stack information.
-
Neglecting Idempotency & Replay Attacks
Non-idempotent write operations allow duplicate charges or state corruption when clients retry. Production practice: require Idempotency-Key headers for POST/PUT, store keys with TTLs, and return cached responses for duplicate requests.
-
Inadequate Audit Logging & Traceability
Missing logs prevent forensic analysis and compliance reporting. Production practice: log authentication events, authorization decisions, data access patterns, and configuration changes. Use structured JSON with trace_id, user_id, resource, action, and outcome.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Public B2C API | Gateway + JWT + Tiered Rate Limiting | High traffic, unknown clients, abuse resistance required | Moderate infrastructure cost, low breach cost |
| Internal Microservices | mTLS + Service Mesh RBAC | Trusted network, low latency, zero-trust compliance | High initial setup, near-zero operational overhead |
| Partner/B2B Integration | OAuth2 Client Credentials + IP Allowlist + Webhooks | External trust boundaries, audit requirements, controlled access | Medium setup cost, predictable compliance spend |
| High-Compliance (HIPAA/PCI) | End-to-End Encryption + ABAC + Immutable Audit Logs | Regulatory mandates, data classification, forensic requirements | High engineering cost, avoids regulatory penalties |
Configuration Template
// security.config.ts
import { FastifyInstance } from 'fastify';
import fastifyJwt from '@fastify/jwt';
import fastifyHelmet from '@fastify/helmet';
import fastifyRateLimit from '@fastify/rate-limit';
import fastifyCors from '@fastify/cors';
export async function secureApiSetup(app: FastifyInstance) {
await app.register(fastifyJwt, {
secret: process.env.JWT_SECRET!,
sign: { expiresIn: '15m', algorithm: 'RS256' },
verify: { algorithms: ['RS256'], maxAge: '15m' }
});
await app.register(fastifyHelmet, {
contentSecurityPolicy: {
directives: { defaultSrc: ["'self'"], imgSrc: ["'self'"], styleSrc: ["'self'"] }
},
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
noSniff: true,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
});
await app.register(fastifyCors, {
origin: (origin, cb) => {
const allowed = process.env.ALLOWED_ORIGINS?.split(',') || [];
if (!origin || allowed.includes(origin)) cb(null, true);
else cb(new Error('Not allowed by CORS'));
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
});
await app.register(fastifyRateLimit, {
max: 100,
timeWindow: '1 minute',
keyGenerator: (req) => req.ip || req.headers['x-forwarded-for'] as string,
errorResponseBuilder: () => ({ error: 'Rate limit exceeded', retryAfter: 60 })
});
app.addHook('onSend', async (req, reply) => {
reply.headers['x-content-type-options'] = 'nosniff';
reply.headers['x-frame-options'] = 'DENY';
reply.headers['cache-control'] = 'no-store, no-cache, must-revalidate';
reply.headers['permissions-policy'] = 'geolocation=(), microphone=(), camera=()';
});
app.setErrorHandler((error, req, reply) => {
req.log.error({ err: error, trace_id: req.id });
reply.status(error.statusCode || 500).send({
error: 'Request failed',
code: error.statusCode || 500,
trace_id: req.id
});
});
}
Quick Start Guide
- Initialize project:
npm init -y && npm i fastify @fastify/jwt @fastify/helmet @fastify/rate-limit @fastify/cors zod
- Create
server.ts, paste the Configuration Template, and register secureApiSetup(app) before routes.
- Set environment variables:
JWT_SECRET, ALLOWED_ORIGINS, and configure TLS termination at your reverse proxy.
- Run
npx tsx server.ts and test with curl -H "Authorization: Bearer <token>" http://localhost:3000/api/health.
- Verify security headers with
curl -I http://localhost:3000 and confirm rate limiting by sending 101 requests in 60 seconds.