Chapter 8: Middleware
1. The Gatekeepers
Your API needs CORS headers for the React frontend. Rate limiting for the public endpoints. Auth checking for admin routes. All without cluttering route handlers.
Middleware solves this. Each middleware is a gatekeeper. It does one job and passes control to the next layer. Chapter 2 introduced the concept. This chapter goes deep: built-in middleware, custom middleware, execution order, short-circuiting, and real-world patterns.
2. What Middleware Is
Middleware is code that runs before or after your route handler. It sits in the HTTP pipeline between the incoming request and the response. Every request can pass through multiple middleware layers before reaching the handler.
Tina4 Node.js supports two styles of middleware:
Function-based middleware receives req, res, and next. Call next() to continue. Skip it to short-circuit.
async function passthrough(req, res, next) {
return next(req, res);
}async function blockEverything(req, res, next) {
return res.status(503).json({ error: "Service unavailable" });
}Class-based middleware uses naming conventions. Static methods whose names start with before run before the handler (via MiddlewareRunner.runBefore). Methods starting with after run after it (via MiddlewareRunner.runAfter). Each method receives (req, res) and returns [req, res].
class MyMiddleware {
static beforeCheck(req, res) {
// Runs before the route handler
return [req, res];
}
static afterCleanup(req, res) {
// Runs after the route handler
return [req, res];
}
}If a before* method sets the response status to >= 400, the handler is skipped (short-circuit).
Register class-based middleware globally with Router.use():
import { Router, CorsMiddleware, RateLimiterMiddleware, RequestLogger } from "tina4-nodejs";
Router.use(CorsMiddleware);
Router.use(RateLimiterMiddleware);
Router.use(RequestLogger);3. Built-in CorsMiddleware
CORS (Cross-Origin Resource Sharing) controls which domains can call your API from a browser. When React at http://localhost:3000 calls your Tina4 API at http://localhost:7148, the browser sends a preflight OPTIONS request first. Wrong headers and the browser blocks everything.
Tina4 provides both a function-based cors() middleware and a class-based CorsMiddleware. Configure via .env:
TINA4_CORS_ORIGINS=http://localhost:3000,https://myapp.com
TINA4_CORS_METHODS=GET,POST,PUT,PATCH,DELETE,OPTIONS
TINA4_CORS_HEADERS=Content-Type,Authorization
TINA4_CORS_MAX_AGE=86400For development, allow all origins:
TINA4_CORS_ORIGINS=*Apply using the function-based form:
import { Router, cors } from "tina4-nodejs";
const app = Router();
app.use(cors()); // applies to all routesOr apply the class-based form to specific groups:
Router.group("/api", () => {
Router.get("/products", async (req, res) => {
return res.json({ products: [] });
});
}, "CorsMiddleware");Preflight OPTIONS requests return 204 No Content with the correct CORS headers. The browser caches the preflight based on TINA4_CORS_MAX_AGE.
4. Built-in RateLimiterMiddleware
The rate limiter prevents a single client from flooding your API. It uses a sliding-window algorithm that tracks requests per IP in memory. Configure via .env:
TINA4_RATE_LIMIT=60
TINA4_RATE_WINDOW=6060 requests per 60 seconds per IP. Apply it:
Router.group("/api/public", () => {
Router.get("/search", async (req, res) => {
return res.json({ results: [] });
});
}, "RateLimiterMiddleware");When a client exceeds the limit, they receive a 429 Too Many Requests response with rate limit headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711113060
Retry-After: 42Custom limits per group:
Router.group("/api/public", () => {
Router.get("/search", async (req, res) => {
return res.json({ results: [] });
});
}, "RateLimiterMiddleware:30");5. Built-in RequestLogger
The RequestLogger middleware logs every request with timing and coloured status codes. It uses two hooks:
beforeLogstamps the start time before the handler runsafterLogcalculates elapsed time and prints a coloured log line
Register it globally:
import { Router, RequestLogger } from "tina4-nodejs";
Router.use(RequestLogger);The console output looks like:
200 GET /api/users 12ms
201 POST /api/products 45ms
404 GET /api/missing 2msGreen for 2xx, yellow for 3xx, red for 4xx and 5xx.
Built-in SecurityHeadersMiddleware
The SecurityHeadersMiddleware adds standard security headers to every response. Register it globally:
import { Router, SecurityHeadersMiddleware } from "tina4-nodejs";
Router.use(SecurityHeadersMiddleware);It sets the following headers by default:
| Header | Default Value |
|---|---|
X-Frame-Options | DENY |
Content-Security-Policy | default-src 'self' |
Strict-Transport-Security | max-age=31536000; includeSubDomains |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | camera=(), microphone=(), geolocation=() |
X-Content-Type-Options | nosniff |
Override any header via environment variables in .env:
TINA4_FRAME_OPTIONS=SAMEORIGIN
TINA4_CSP=default-src 'self'; script-src 'self' https://cdn.example.com
TINA4_HSTS=max-age=63072000; includeSubDomains; preload
TINA4_REFERRER_POLICY=no-referrer
TINA4_PERMISSIONS_POLICY=camera=(), microphone=(), geolocation=(self)Combining All Four Built-In Middleware
A common production setup:
import { Router, CorsMiddleware, RateLimiterMiddleware, RequestLogger, SecurityHeadersMiddleware } from "tina4-nodejs";
Router.use(CorsMiddleware);
Router.use(RateLimiterMiddleware);
Router.use(RequestLogger);
Router.use(SecurityHeadersMiddleware);Order matters. CORS handles OPTIONS preflight first. The rate limiter only counts real requests (not preflight). The logger measures total time including the other middleware. Security headers are added to every response.
6. Writing Custom Middleware
Request Logging
async function logRequest(req, res, next) {
const start = Date.now();
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} from ${req.ip}`);
const result = await next(req, res);
const duration = Date.now() - start;
console.log(` Completed in ${duration}ms`);
return result;
}Request Timing
async function addTiming(req, res, next) {
const start = Date.now();
const result = await next(req, res);
const duration = Date.now() - start;
res.header("X-Response-Time", `${duration}ms`);
return result;
}IP Whitelist
async function ipWhitelist(req, res, next) {
const allowedIps = (process.env.ALLOWED_IPS ?? "127.0.0.1").split(",");
if (!allowedIps.includes(req.ip)) {
return res.status(403).json({ error: "Access denied", your_ip: req.ip });
}
return next(req, res);
}Request Validation
async function requireJson(req, res, next) {
if (["POST", "PUT", "PATCH"].includes(req.method)) {
const contentType = req.headers["content-type"] ?? "";
if (!contentType.includes("application/json")) {
return res.status(415).json({
error: "Content-Type must be application/json",
received: contentType
});
}
}
return next(req, res);
}Writing Class-Based Middleware
For middleware that needs both before and after hooks, use the class-based pattern with static before* and after* methods:
class InputSanitizer {
static beforeSanitize(req, res) {
if (req.body && typeof req.body === "object") {
req.body = InputSanitizer.sanitize(req.body);
}
return [req, res];
}
private static sanitize(data: Record<string, any>): Record<string, any> {
const clean: Record<string, any> = {};
for (const [key, value] of Object.entries(data)) {
if (typeof value === "string") {
clean[key] = value.replace(/[<>&"']/g, (c) =>
({ "<": "<", ">": ">", "&": "&", '"': """, "'": "'" })[c] ?? c
);
} else if (typeof value === "object" && value !== null) {
clean[key] = InputSanitizer.sanitize(value);
} else {
clean[key] = value;
}
}
return clean;
}
}JWT Authentication Middleware (Class-Based)
import { Auth } from "tina4-nodejs";
class JwtAuthMiddleware {
static beforeVerifyToken(req, res) {
const authHeader = req.headers["authorization"] ?? "";
if (!authHeader || !authHeader.startsWith("Bearer ")) {
res(JSON.stringify({ error: "Authorization header required" }), 401);
return [req, res];
}
const token = authHeader.slice(7);
const payload = Auth.validToken(token);
if (!payload) {
res(JSON.stringify({ error: "Invalid or expired token" }), 401);
return [req, res];
}
(req as any).user = payload;
return [req, res];
}
}Apply it to protected routes:
Router.group("/api/protected", () => {
Router.get("/profile", async (req, res) => {
return res.json({ user: (req as any).user });
});
Router.post("/settings", async (req, res) => {
const userId = (req as any).user.sub;
return res.json({ updated: true, user_id: userId });
});
}, "JwtAuthMiddleware");Request ID Middleware (Class-Based)
import { randomUUID } from "crypto";
class RequestIdMiddleware {
static beforeInjectId(req, res) {
(req as any).requestId = randomUUID();
return [req, res];
}
static afterAddHeader(req, res) {
res.header("X-Request-ID", (req as any).requestId);
return [req, res];
}
}7. Applying Middleware
Single middleware:
Router.get("/api/data", async (req, res) => {
return res.json({ data: [1, 2, 3] });
}, "logRequest");Multiple middleware:
Router.post("/api/data", async (req, res) => {
return res.status(201).json({ created: true });
}, ["logRequest", "requireJson"]);8. Route Groups with Shared Middleware
Router.group("/api/public", () => {
Router.get("/products", async (req, res) => {
return res.json({ products: [] });
});
Router.get("/categories", async (req, res) => {
return res.json({ categories: [] });
});
}, ["CorsMiddleware", "RateLimiter:30"]);
Router.group("/api/admin", () => {
Router.get("/users", async (req, res) => {
return res.json({ users: [] });
});
}, ["logRequest", "ipWhitelist", "authMiddleware"]);9. Middleware Execution Order
Middleware executes from outer to inner:
Router.get("/api/test", async (req, res) => {
console.log("Handler");
return res.json({ ok: true });
}, ["middlewareA", "middlewareB", "middlewareC"]);Output:
A: before
B: before
C: before
Handler
C: after
B: after
A: afterGroup middleware always runs before route middleware.
10. Short-Circuiting
When middleware does not call next, the chain dies:
async function requireAuth(req, res, next) {
const token = req.headers["authorization"] ?? "";
if (!token) {
return res.status(401).json({ error: "Authentication required" });
}
return next(req, res);
}Maintenance Mode
async function maintenanceMode(req, res, next) {
const isMaintenanceMode = process.env.MAINTENANCE_MODE === "true";
if (isMaintenanceMode) {
if (req.path === "/health") {
return next(req, res);
}
return res.status(503).json({ error: "Service is undergoing maintenance", retry_after: 300 });
}
return next(req, res);
}11. Modifying Requests in Middleware
async function addRequestId(req, res, next) {
const { randomUUID } = await import("crypto");
req.requestId = randomUUID();
const result = await next(req, res);
res.header("X-Request-Id", req.requestId);
return result;
}12. Real-World Middleware Stack
import { Router } from "tina4-nodejs";
Router.group("/api/v1", () => {
Router.get("/products", async (req, res) => {
return res.json({ products: [
{ id: 1, name: "Widget", price: 9.99 },
{ id: 2, name: "Gadget", price: 19.99 }
]});
});
Router.post("/products", async (req, res) => {
return res.status(201).json({
id: 3,
name: req.body.name ?? "Unknown",
price: parseFloat(req.body.price ?? 0)
});
});
}, ["addRequestId", "logRequest", "CorsMiddleware", "requireApiKey"]);13. Exercise: Build an API Key Middleware
Build validateApiKey middleware that checks X-API-Key header against API_KEYS env variable.
Requirements
- Missing key: return 401 with
{"error": "API key required"} - Invalid key: return 403 with
{"error": "Invalid API key"} - Valid key: attach to
req.apiKeyand continue - Apply to a route group with at least two endpoints
14. Solution
import { Router } from "tina4-nodejs";
async function validateApiKey(req, res, next) {
const apiKey = req.headers["x-api-key"] ?? "";
if (!apiKey) {
return res.status(401).json({ error: "API key required" });
}
const validKeys = (process.env.API_KEYS ?? "").split(",").map(k => k.trim());
if (!validKeys.includes(apiKey)) {
return res.status(403).json({ error: "Invalid API key" });
}
req.apiKey = apiKey;
return next(req, res);
}
Router.group("/api/partner", () => {
Router.get("/data", async (req, res) => {
return res.json({
authenticated_with: req.apiKey,
data: [{ id: 1, value: "alpha" }, { id: 2, value: "beta" }]
});
});
Router.get("/stats", async (req, res) => {
return res.json({
authenticated_with: req.apiKey,
stats: { total_requests: 1423, avg_response_ms: 42 }
});
});
}, "validateApiKey");15. Gotchas
1. Middleware Must Be a Named Function
Fix: Define as a named function and pass the name as a string.
2. Forgetting to Return next()
Fix: Always return await next(req, res).
3. Middleware Order Matters
Fix: Put addRequestId before logRequest: ["addRequestId", "logRequest"].
4. CORS Preflight Returns 404
Fix: Apply CorsMiddleware to the group. It handles OPTIONS automatically.
5. Rate Limiter Counts Preflight Requests
Fix: Put CorsMiddleware before RateLimiter.
6. Middleware File Not Auto-Loaded
Fix: Put middleware functions in a file inside src/routes/.
7. Short-Circuiting Skips Cleanup
Fix: Put timing/logging middleware at the outermost layer.