Back to Node.js tutorials
Intermediate18 min read

Express.js Framework

Build REST APIs with Express routing, middleware, validation, and centralized error handling.

Express Application Setup

Express wraps Node HTTP with a routing layer and middleware pipeline. Create an app with express(), register middleware, mount routers, and listen on a port.

Middleware runs in order. Each function receives req, res, and next. Call next() to pass control or next(err) to jump to error handlers.

Split routers by domain: usersRouter, ordersRouter. Mount at paths like app.use("/api/users", usersRouter).

  • Enable express.json() once globally with size limits
  • Use morgan or pino-http for structured request logging
  • Export app for Supertest without listening in tests
import express from 'express';

const app = express();
app.use(express.json());
app.get('/health', (_req, res) => res.json({ ok: true }));
app.listen(3000);

Middleware Patterns

Authentication middleware attaches user to req and calls next on success. Validation middleware checks schemas before handlers run.

Third-party middleware handles CORS, compression, security headers (helmet), and rate limiting. Order matters: parsers before routers, error handler last.

Write reusable middleware as functions returning configured handlers when options are needed.

  • Never mutate req with untrusted client fields without validation
  • Apply rate limiting on auth and write endpoints
  • Use async middleware with express-async-errors or wrapper
function requireAuth(req, res, next) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (!token) return res.status(401).json({ error: 'Unauthorized' });
  req.user = verifyToken(token);
  next();
}

Route Handlers and Errors

Route handlers should return consistent JSON and appropriate status codes. Use res.status(201).json(created) for resource creation with Location headers when helpful.

Central error middleware with four arguments (err, req, res, next) catches thrown errors and async rejections when wrapped.

Differentiate operational AppError instances from unexpected exceptions. Hide internal details in production 500 responses.

  • Validate input with zod or joi at route boundaries
  • Use HTTP 204 for successful deletes with no body
  • Document APIs with OpenAPI generated from schemas
app.use((err, req, res, next) => {
  const status = err.status ?? 500;
  res.status(status).json({ error: err.message ?? 'Internal error' });
});

Get In Touch


Ready to discuss your next project? Drop me a message.