High-Throughput Node.js & Express REST APIs
Building enterprise-ready REST microservices with modular middleware pipelines, JWT refresh token rotation, and strict RBAC guards.
Overview
A production Node.js + Express API must be fast, secure, maintainable, and observable. This guide covers project structure, authentication, RBAC, rate limiting, and error handling.
Project Structure
src/
├── config/ ← environment, db, logger
├── middlewares/ ← auth, rbac, rate-limit, error-handler
├── modules/
│ └── users/
│ ├── user.controller.ts
│ ├── user.service.ts
│ ├── user.repository.ts
│ └── user.routes.ts
├── shared/
│ ├── errors.ts
│ └── response.ts
└── app.ts
JWT Auth with Refresh Rotation
// middlewares/auth.ts
import jwt from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";
export function authenticate(req: Request, res: Response, next: NextFunction) {
const header = req.headers.authorization;
if (!header?.startsWith("Bearer ")) return res.status(401).json({ message: "Unauthorized" });
const token = header.slice(7);
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; role: string };
req.user = payload;
next();
} catch {
res.status(401).json({ message: "Invalid token" });
}
}
// Refresh rotation — invalidate old token on each use
export async function refreshTokens(oldToken: string) {
const record = await db.refreshToken.findUnique({ where: { token: oldToken } });
if (!record || record.revoked) throw new Error("Token reuse detected — session revoked");
// Rotate
await db.refreshToken.update({ where: { id: record.id }, data: { revoked: true } });
const newRefresh = crypto.randomUUID();
await db.refreshToken.create({ data: { token: newRefresh, userId: record.userId } });
const access = jwt.sign({ sub: record.userId }, process.env.JWT_SECRET!, { expiresIn: "15m" });
return { access, refresh: newRefresh };
}
RBAC Middleware
// middlewares/rbac.ts
type Role = "admin" | "editor" | "viewer";
const HIERARCHY: Record<Role, number> = { admin: 3, editor: 2, viewer: 1 };
export function requireRole(minimum: Role) {
return (req: Request, res: Response, next: NextFunction) => {
const userLevel = HIERARCHY[req.user?.role as Role] ?? 0;
if (userLevel < HIERARCHY[minimum]) {
return res.status(403).json({ message: "Forbidden" });
}
next();
};
}
// Usage
router.delete("/posts/:id", authenticate, requireRole("editor"), deletePost);
Rate Limiting with Redis
import { RateLimiterRedis } from "rate-limiter-flexible";
import { createClient } from "redis";
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
const limiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: "rl",
points: 100, // 100 requests
duration: 60, // per 60s
});
export async function rateLimitMiddleware(req: Request, res: Response, next: NextFunction) {
try {
await limiter.consume(req.ip ?? "anon");
next();
} catch {
res.status(429).json({ message: "Too many requests" });
}
}
Global Error Handler
// shared/errors.ts
export class AppError extends Error {
constructor(public message: string, public statusCode: number = 500) {
super(message);
}
}
// app.ts — must be last middleware
app.use((err: unknown, req: Request, res: Response, _next: NextFunction) => {
if (err instanceof AppError) {
return res.status(err.statusCode).json({ message: err.message });
}
console.error(err);
res.status(500).json({ message: "Internal server error" });
});
Performance: Cluster Mode
// server.ts
import cluster from "cluster";
import os from "os";
if (cluster.isPrimary) {
const cpus = os.cpus().length;
for (let i = 0; i < cpus; i++) cluster.fork();
cluster.on("exit", () => cluster.fork()); // auto-restart
} else {
startApp(); // your Express listen()
}
Key Takeaways
- Rotate refresh tokens on every use and revoke entire families on reuse detection
- RBAC via numeric hierarchy scales cleanly across roles
- Rate limiting in Redis keeps counters consistent across cluster workers
- Always have a typed global error handler — never let unhandled errors reach the client