| name | api-hardening |
| description | API security hardening patterns. Use when implementing rate limiting, input validation, CORS configuration, API key management, request throttling, or protecting endpoints from abuse. Covers defense-in-depth strategies for REST APIs with practical implementations for Express, FastAPI, and serverless. |
API hardening
Defense-in-depth patterns for protecting APIs from abuse, injection attacks, and data leakage.
Rate limiting
Why it matters
Without rate limiting:
- Brute force attacks succeed
- APIs get DDoS'd by accident or intent
- One bad actor affects all users
- You get a surprise bill from your cloud provider
Express.js with express-rate-limit
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis').default;
const { createClient } = require('redis');
const redisClient = createClient({ url: process.env.REDIS_URL });
redisClient.connect();
const apiLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redisClient.sendCommand(args) }),
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
message: { error: 'Too many requests, please try again later' },
skip: (req) => {
return req.path === '/health';
}
});
const authLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redisClient.sendCommand(args) }),
windowMs: 15 * 60 * 1000,
max: 5,
message: { error: 'Too many login attempts, please try again in 15 minutes' },
keyGenerator: (req) => {
return `${req.ip}-${req.body?.email || 'unknown'}`;
}
});
const passwordResetLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redisClient.sendCommand(args) }),
windowMs: 60 * 60 * 1000,
max: 3,
message: { error: 'Too many password reset requests' }
});
app.use('/api/', apiLimiter);
app.use('/auth/login', authLimiter);
app.use('/auth/forgot-password', passwordResetLimiter);
Sliding window implementation (custom)
class SlidingWindowRateLimiter {
constructor(redisClient, options = {}) {
this.redis = redisClient;
this.windowMs = options.windowMs || 60000;
this.maxRequests = options.maxRequests || 100;
this.keyPrefix = options.keyPrefix || 'ratelimit';
}
async isAllowed(identifier) {
const now = Date.now();
const windowStart = now - this.windowMs;
const key = `${this.keyPrefix}:${identifier}`;
const multi = this.redis.multi();
multi.zRemRangeByScore(key, 0, windowStart);
multi.zCard(key);
multi.zAdd(key, { score: now, value: `${now}-${Math.random()}` });
multi.(key, .(. / ));
results = multi.();
requestCount = results[];
{
: requestCount < .,
: .(, . - requestCount - ),
: now + .
};
}
}
() {
(req, res, next) => {
identifier = req.;
result = limiter.(identifier);
res.(, limiter.);
res.(, result.);
res.(, result.);
(!result.) {
res.().({ : });
}
();
};
}
Per-user rate limiting with API keys
const tierLimits = {
free: { windowMs: 60000, max: 10 },
pro: { windowMs: 60000, max: 100 },
enterprise: { windowMs: 60000, max: 1000 }
};
async function apiKeyRateLimiter(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(401).json({ error: 'API key required' });
}
const keyData = await db.query(
'SELECT user_id, tier, revoked FROM api_keys WHERE key_hash = $1',
[hashApiKey(apiKey)]
);
if (keyData.rows.length === 0 || keyData.rows[0].revoked) {
return res.status(401).json({ error: 'Invalid API key' });
}
const { user_id, tier } = keyData.[];
limits = tierLimits[tier] || tierLimits.;
limiter = (redisClient, {
...limits,
:
});
result = limiter.(user_id);
res.(, limits.);
res.(, result.);
res.(, result.);
(!result.) {
res.().({ : });
}
req. = user_id;
();
}
Input validation
Validation with Zod (TypeScript/JavaScript)
const { z } = require('zod');
const createUserSchema = z.object({
email: z.string().email().max(255),
password: z.string().min(12).max(128),
name: z.string().min(1).max(100).optional()
});
const updateProfileSchema = z.object({
name: z.string().min(1).max(100).optional(),
bio: z.string().max(500).optional(),
website: z.string().url().optional().or(z.literal(''))
});
const paginationSchema = z.object({
page: z.coerce.number().int().min().(),
: z..().().().().()
});
() {
{
result = schema.(req.);
(!result.) {
res.().({
: ,
: result...( ({
: issue..(),
: issue.
}))
});
}
req. = result.;
();
};
}
app.(, (createUserSchema), (req, res) => {
{ email, password, name } = req.;
});
Sanitization
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const validator = require('validator');
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
function sanitizeHtml(dirty) {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href'],
ALLOW_DATA_ATTR: false
});
}
function sanitizeString(str) {
if (typeof str !== 'string') return '';
return str
.trim()
.slice(0, )
.(, );
}
() {
(!.(str)) {
();
}
str;
}
() {
filename
.(, )
.(, )
.(, );
}
Preventing SQL injection
const query = `SELECT * FROM users WHERE id = ${userId}`;
const query = 'SELECT * FROM users WHERE id = ' + userId;
const query = `SELECT * FROM users WHERE name = '${name}'`;
const result = await db.query(
'SELECT * FROM users WHERE id = $1',
[userId]
);
const result = await db.query(
'SELECT * FROM users WHERE id = ?',
[userId]
);
const users = await knex('users')
.where('id', userId)
.first();
const user = await prisma.user.findUnique({
where: { id: userId }
});
const allowedColumns = ['name', 'email', 'created_at'];
const sortColumn = allowedColumns.includes(req.query.sort)
? req.query.
: ;
query = ;
Preventing XSS
res.send(`<h1>Hello ${userName}</h1>`);
res.render('greeting', { name: userName });
const escapeHtml = require('escape-html');
res.send(`<h1>Hello ${escapeHtml(userName)}</h1>`);
res.json({ name: userName });
CORS configuration
Express.js
const cors = require('cors');
const developmentOrigins = [
'http://localhost:3000',
'http://localhost:5173',
'http://127.0.0.1:3000'
];
const productionOrigins = [
'https://yourapp.com',
'https://www.yourapp.com',
'https://app.yourapp.com'
];
const allowedOrigins = process.env.NODE_ENV === 'production'
? productionOrigins
: [...productionOrigins, ...developmentOrigins];
const corsOptions = {
origin: (origin, callback) => {
if (!origin) {
return callback(null, true);
}
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
: [, ],
: ,
:
};
app.((corsOptions));
app.( {
(err. === ) {
res.().({ : });
}
(err);
});
Common CORS mistakes
app.use(cors());
app.use(cors({ origin: '*', credentials: true }));
app.use(cors({
origin: (origin, cb) => cb(null, origin)
}));
const origin = /yourapp\.com/;
const origin = /^https:\/\/(www\.)?yourapp\.com$/;
API key management
Secure key generation and storage
const crypto = require('crypto');
function generateApiKey() {
const prefix = 'sk_live';
const randomPart = crypto.randomBytes(24).toString('base64url');
return `${prefix}_${randomPart}`;
}
function hashApiKey(key) {
return crypto.createHash('sha256').update(key).digest('hex');
}
app.post('/api-keys', requireAuth, async (req, res) => {
const { name } = req.body;
const plainKey = generateApiKey();
const keyHash = hashApiKey(plainKey);
await db.query(
`INSERT INTO api_keys (user_id, key_hash, name, created_at)
VALUES ($1, $2, $3, NOW())`,
[req.userId, keyHash, name]
);
res.({
: plainKey,
:
});
});
() {
keyHash = (key);
result = db.(
,
[keyHash]
);
(result.. === ) {
;
}
keyData = result.[];
(keyData.) {
;
}
db.(
,
[keyData.]
);
keyData;
}
app.(, requireAuth, (req, res) => {
db.(
,
[req.., req.]
);
res.({ : });
});
API key middleware
async function apiKeyAuth(req, res, next) {
const apiKey = req.headers['x-api-key']
|| req.headers['authorization']?.replace('Bearer ', '')
|| req.query.api_key;
if (!apiKey) {
return res.status(401).json({
error: 'API key required',
hint: 'Pass API key in X-API-Key header'
});
}
const keyData = await verifyApiKey(apiKey);
if (!keyData) {
return res.status(401).json({ error: 'Invalid API key' });
}
req.apiKeyId = keyData.id;
req.userId = keyData.user_id;
next();
}
Request size limits
const express = require('express');
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ limit: '100kb', extended: true }));
app.post('/api/upload', express.json({ limit: '10mb' }), (req, res) => {
});
const multer = require('multer');
const upload = multer({
limits: {
fileSize: 5 * 1024 * 1024,
files: 5
},
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} {
( ());
}
}
});
app.(, upload.(), {
});
Response security
Don't leak information
app.use((err, req, res, next) => {
res.status(500).json({
error: err.message,
stack: err.stack
});
});
app.use((err, req, res, next) => {
console.error(err);
if (process.env.NODE_ENV === 'production') {
res.status(500).json({ error: 'Internal server error' });
} else {
res.status(500).json({ error: err.message, stack: err.stack });
}
});
res.status(400).json({
error: 'duplicate key value violates unique constraint "users_email_key"'
});
res.status(400).json({
error: 'An account with this email already exists'
});
Security headers
const helmet = require('helmet');
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.yourapp.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
upgradeInsecureRequests: []
}
}));
app.use(helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: true
}));
Timeout protection
function timeout(ms) {
return (req, res, next) => {
res.setTimeout(ms, () => {
res.status(408).json({ error: 'Request timeout' });
});
next();
};
}
app.use(timeout(30000));
async function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
return response;
} finally {
clearTimeout(timeoutId);
}
}
const result = await db.query({
text: 'SELECT * FROM large_table WHERE condition = $1',
values: [value],
:
});
FastAPI (Python) equivalents
from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from slowapi import Limiter
from slowapi.util import get_remote_address
from pydantic import BaseModel, EmailStr, Field
import hashlib
import secrets
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourapp.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["*"],
)
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
@app.post("/api/login")
@limiter.limit("5/minute")
async def login(request: Request, credentials: LoginRequest):
pass
class CreateUserRequest(BaseModel):
email: EmailStr
password: str = Field(min_length=12, max_length=128)
name: str = Field(max_length=100, default=None)
@app.post("/users")
async ():
() -> :
() -> :
hashlib.sha256(key.encode()).hexdigest()
Security checklist for APIs