Express is a minimalist, unopinionated web framework for Node.js, providing robust HTTP utility methods, extensible middleware architectures, and async handler support.
When to Use
- Fast Node.js REST API Development: Building flexible, lightweight HTTP microservices and web backends.
- Established Node.js Ecosystem Services: Leveraging thousands of battle-tested npm middleware packages.
- Web App Routing & Server-Side Rendering: Serving dynamic server-side templates with EJS, Pug, or Handlebars.
- API Gateways & Proxy Layers: Routing, inspecting, and transforming inbound microservice requests.
Quick Start
import express from "express";
const app = express();
// Express 5 handles async errors automatically! app.get("/", async (req, res) => { const user = await db.getUser(); // If this throws, Express catches it. res.json(user); });
app.listen(3000);
## Core Concepts
### Modular Route Handlers & Express Router
Structuring scalable APIs with decoupled sub-routers:
```typescript
import express, { Request, Response, NextFunction } from "express";
const userRouter = express.Router();
interface UserPayload {
username: string;
email: string;
}
userRouter.get(
"/:id",
async (req: Request, res: Response, next: NextFunction) => {
try {
const userId = req.params.id;
res.json({ id: userId, username: `user_${userId}`, status: "active" });
} catch (error) {
next(error);
}
},
);
userRouter.post(
"/",
async (
req: Request<{}, {}, UserPayload>,
res: Response,
next: NextFunction,
) => {
try {
const { username, email } = req.body;
if (!username || !email) {
res.status(400).json({ error: "Username and email are required" });
return;
}
res.status(201).json({ id: Date.now(), username, email });
} catch (error) {
next(error);
}
},
);
export default userRouter;
Custom Middleware & Security Pipeline
Hardening Express with Helmet, CORS, and compression:
import express from "express";
import helmet from "helmet";
import cors from "cors";
import compression from "compression";
import userRouter from "./userRouter";
const app = express();
app.use(helmet());
app.use(cors({ origin: "https://trusted-domain.com" }));
app.use(compression());
app.use(express.json({ limit: "1mb" }));
app.use(express.urlencoded({ extended: true }));
app.use("/api/v1/users", userRouter);
app.listen(3000, () => {
console.log("Express API listening on port 3000");
});
Centralized Error Handling Middleware
Uniform error trapping conforming to 4-argument signature:
import { Request, Response, NextFunction } from "express";
interface AppError extends Error {
statusCode?: number;
}
export function errorHandler(
err: AppError,
req: Request,
res: Response,
next: NextFunction,
): void {
const statusCode = err.statusCode || 500;
console.error(`[Error] ${req.method} ${req.path}:`, err.message);
res.status(statusCode).json({
error: {
message: err.message || "Internal Server Error",
status: statusCode,
timestamp: new Date().toISOString(),
},
});
}
Common Patterns
Centralized Async Error Handling Middleware
Problem: Unhandled promise rejections inside async route handlers cause server crashes or hanging requests.
Solution: Use an async handler wrapper and centralized error middleware:
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get(
"/api/users/:id",
asyncHandler(async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) throw new NotFoundError("User not found");
res.json(user);
}),
);
// Centralized error handler (must have 4 arguments)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.statusCode || 500).json({ error: err.message });
});
Best Practices
Do:
- Always mount the 4-argument error-handling middleware (
(err, req, res, next)) at the very end of the middleware stack. - Use
helmet()to secure HTTP headers (HSTS, CSP, X-Frame-Options) against common exploits. - Wrap async handler functions or use Express 5 to catch rejected promises automatically.
- Limit request body payload size (
express.json({ limit: '100kb' })) to defend against DoS attacks.
Don't:
- Use synchronous I/O operations (
fs.readFileSync) inside route handlers. - Omit
returnafter sending responses (res.json(...)) inside conditional branches. - Expose stack traces in production error responses (
NODE_ENV === 'production').
Troubleshooting
| Error | Cause | Solution |
|---|---|---|
Cannot set headers after they are sent to the client |
Attempted to call res.json() or res.send() multiple times in handler. |
Add return before response calls: return res.status(400).send(...). |
req.body is undefined |
Missing body-parsing middleware in application pipeline. | Add app.use(express.json()) and app.use(express.urlencoded({ extended: true })). |
EADDRINUSE: address already in use :::3000 |
Port 3000 occupied by previous node process. | Kill process with npx kill-port 3000 or change listening port. |