Loading content…
Loading content…
Master input validation, JWT token lifecycles, connection pooling, CORS, rate-limiting, and error-handling in Express production apps
import { Request, Response, NextFunction } from "express";
import { AnyZodObject, ZodError } from "zod";
export const validate = (schema: AnyZodObject) => {
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
// Validate body, query, and params against the schema
const parsed = await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params,
});
// Assign parsed values back to req
req.body = parsed.body;
req.query = parsed.query;
req.params = parsed.params;
next();
} catch (error) {
if (error instanceof ZodError) {
res.status(400).json({
status: "fail",
errors: error.errors.map(err => ({
field: err.path.join("."),
message: err.message
}))
});
return;
}
next(error);
}
};
};
import express from "express";
import { z } from "zod";
import { validate } from "./middleware/validate";
const router = express.Router();
const createUserSchema = z.object({
body: z.object({
name: z.string().min(2, "Name must be at least 2 characters long"),
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters long"),
}),
});
router.post("/users", validate(createUserSchema), async (req, res) => {
// Safe: req.body is validated and typed!
const { name, email, password } = req.body;
// Insert into PostgreSQL database...
res.status(201).json({ success: true });
});
JWT Authentication Token Rotation FlowlocalStorage) or session storage. Storing tokens in client memory makes them vulnerable to Cross-Site Scripting (XSS). Instead, store tokens in Secure, HttpOnly, SameSite cookies.import { Response } from "express";
import jwt from "jsonwebtoken";
export const sendTokens = (res: Response, userId: string) => {
const accessToken = jwt.sign({ userId }, process.env.JWT_ACCESS_SECRET!, { expiresIn: "15m" });
const refreshToken = jwt.sign({ userId }, process.env.JWT_REFRESH_SECRET!, { expiresIn: "7d" });
// Store access token in HttpOnly cookie
res.cookie("accessToken", accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production", // HTTPS only in prod
sameSite: "strict", // Prevent CSRF attacks
maxAge: 15 * 60 * 1000, // 15 mins
});
// Store refresh token in a separate HttpOnly cookie
res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});
};
Common Pitfall
sameSite: "strict" or sameSite: "lax", and use a anti-CSRF token check for modifying state endpoints (POST/PUT/DELETE) if serving a separate domain.pg.Pool) which creates a cache of reusable database connections.import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // Max number of concurrent clients in the pool
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
connectionTimeoutMillis: 2000, // Return an error if connection takes > 2s
});
export const query = (text: string, params?: any[]) => {
return pool.query(text, params);
};
// Check pool error handling
pool.on("error", (err) => {
console.error("Unexpected error on idle database client", err);
});
Senior Developer Wisdom
max) requires careful balance. A good starting formula is: ((Max Database Connections) / (Number of Server Instances)) - 5. Leaving room ensures other tools (e.g. database migrations, backup scripts) can connect without encountering errors.cors(*)) is a security hazard in production. Restrict cross-origin requests to trusted domains only.import cors from "cors";
const allowedOrigins = [
"https://codenivra.io",
"https://admin.codenivra.io"
];
const corsOptions: cors.CorsOptions = {
origin: (origin, callback) => {
if (!origin || allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error("Blocked by CORS policy"));
}
},
credentials: true, // Allow cookies to be sent along with requests
optionsSuccessStatus: 200,
};
app.use(cors(corsOptions));
express-rate-limit.import rateLimit from "express-rate-limit";
export const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window
message: {
status: "fail",
message: "Too many requests from this IP, please try again later."
},
standardHeaders: true, // Return rate limit info in the headers
legacyHeaders: false, // Disable the X-RateLimit-* headers
});
app.use("/api/", globalLimiter);
export class AppError extends Error {
public readonly statusCode: number;
public readonly isOperational: boolean;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
// Operational errors represent expected failures (validation, auth failures)
// Programmer errors represent bugs (syntax, database exceptions)
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
import { Request, Response, NextFunction } from "express";
import { AppError } from "./errors/AppError";
export const errorHandler = (
err: Error | AppError,
req: Request,
res: Response,
next: NextFunction
): void => {
const statusCode = "statusCode" in err ? err.statusCode : 500;
const isOperational = "isOperational" in err ? err.isOperational : false;
// 1. Log all errors internally
console.error("ERROR 💥:", err);
// 2. Format response based on error type
if (isOperational) {
res.status(statusCode).json({
status: "fail",
message: err.message,
});
} else {
// Programmer or Database error: Don't leak details to clients
res.status(500).json({
status: "error",
message: "Something went wrong on our end.",
});
}
};
import express from "express";
import { errorHandler } from "./middleware/errorHandler";
import { AppError } from "./errors/AppError";
const app = express();
app.get("/api/user-details", async (req, res, next) => {
try {
const user = await getUserFromDb(req.query.id);
if (!user) {
return next(new AppError("No user found with that ID", 404));
}
res.json(user);
} catch (error) {
next(error); // Forward programmer error to error handler middleware
}
});
// Register errorHandler at the very end of app middleware chain!
app.use(errorHandler);
Pro Tip
process.on("unhandledRejection", (err) => {
console.error("UNHANDLED REJECTION! Shutting down...");
console.error(err);
server.close(() => process.exit(1));
});Express + Postgres Production Checklist
Marking it complete updates your roadmap progress percentage.