Middleware Architecture: The Real Power of Express

Front
Back
Right
Left
Top
Bottom
MIDDLEWARE

Middleware Is The Framework

If routing is the skeleton of an Express app, middleware is everything else — authentication, logging, validation, error handling, rate limiting. Every production Express app is, at its core, a carefully ordered chain of middleware functions.

Most developers use middleware. Few understand it deeply. That gap shows up in bugs that are hard to trace, security holes that are easy to miss, and code that’s impossible to reuse.

Let’s fix that.

HOW

How the Middleware Pipeline Actually Works

Every Express middleware function has this signature:
Copy to clipboard
(req: Request, res: Response, next: NextFunction) => void

The key is `next`. Calling `next()` passes control to the next middleware in the stack. Not calling it means the request gets stuck — the client waits forever.

Here's the mental model:
Copy to clipboard
Request
   ↓
[Middleware 1] → next() →
[Middleware 2] → next() →
[Route Handler] → res.json() → Response sent
Order matters absolutely.

Middleware is executed in the order it’s registered. This is a feature — use it intentionally.

Copy to clipboard
import express from 'express';

const app = express();

// 1. Body parsing — must come before routes that read req.body
app.use(express.json());

// 2. Global logging — runs on every request
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next(); // Must call next() or the request hangs
});

// 3. Routes
app.get('/users', getUsers);

// 4. 404 handler — catches any unmatched route
app.use((req, res) => {
  res.status(404).json({ error: 'Not found' });
});
BUILD IN

Built-in Middleware You Should Know

Express v5 ships with a small set of essential built-in middleware:
`express.json()`
Parses incoming JSON request bodies and populates `req.body`.
Copy to clipboard
app.use(express.json({ limit: '10kb' })); // Limit body size for security
`express.urlencoded()`
Parses URL-encoded form data (HTML form submissions):
Copy to clipboard
app.use(express.urlencoded({ extended: true }));
`express.static()`
Serves static files (images, CSS, JS):
Copy to clipboard
app.use('/public', express.static('public'));
// Files in ./public/ are served at /public/file.png
REUSABLE

Writing Your Own Reusable Middleware

Request Logger

Copy to clipboard
// src/middleware/logger.ts
import { Request, Response, NextFunction } from 'express';

export function requestLogger(req: Request, res: Response, next: NextFunction) {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} ${res.statusCode} — ${duration}ms`);
  });

  next();
}
Apply it to specific routes — not globally:
Copy to clipboard
router.get('/profile', requireAuth, getUserProfile);
router.put('/profile', requireAuth, updateProfile);
Copy to clipboard
// src/index.ts
import { requestLogger } from './middleware/logger.js';
app.use(requestLogger);
```

### 3b. Authentication Middleware

```typescript
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

export function requireAuth(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!);
    (req as any).user = decoded; // Attach user to request
    next();
  } catch {
    res.status(401).json({ error: 'Invalid or expired token' });
  }
}
ASYNC ERROR
The Most Critical Part

Async Error Handling

This is where most Express apps have bugs.

In Express v4, if an async function throws, Express doesn’t catch it:

Copy to clipboard
// Express v4 — unhandled rejection, server may crash
app.get('/users', async (req, res) => {
  const users = await db.getUsers(); // If this throws, Express v4 doesn't catch it
  res.json(users);
});

In Express v5, this is fixed — rejected promises are automatically caught.

But you still need a centralized error-handling middleware to return meaningful responses:

Copy to clipboard
// src/middleware/error.ts
import { Request, Response, NextFunction } from 'express';

export class AppError extends Error {
  constructor(
    public message: string,
    public statusCode: number = 500
  ) {
    super(message);
    this.name = 'AppError';
  }
}

// 4-argument signature = error middleware (Express convention)
export function errorHandler(
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction
) {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: err.message
    });
  }

  console.error(err); // Log unexpected errors
  res.status(500).json({ error: 'Internal server error' });
}

Register it last in your app:

Copy to clipboard
import { errorHandler } from './middleware/error.js';

// ... all routes ...

app.use(errorHandler); // ← Must be last
```

Now in any route or service, just throw:

```typescript
import { AppError } from '../middleware/error.js';

app.get('/users/:id', async (req, res) => {
  const user = await db.findUser(req.params.id);
  if (!user) throw new AppError('User not found', 404);
  res.json(user);
});
THIRD-PARTY

Essential Third-Party Middleware

These packages should be in every production Express app:
helmet — Security Headers
Copy to clipboard
// npm install helmet

import helmet from 'helmet';
app.use(helmet()); // Sets 14+ security HTTP headers automatically
cors — Cross-Origin Resource Sharing
Copy to clipboard
// npm install cors @types/cors

import cors from 'cors';
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') ?? '*',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
}));
express-rate-limit — API Rate Limiting
Copy to clipboard
// npm install express-rate-limit

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // 100 requests per window per IP
  message: { error: 'Too many requests, please slow down.' }
});

app.use('/api', limiter);
morgan — HTTP Request Logging
Copy to clipboard
// npm install morgan @types/morgan

import morgan from 'morgan';
app.use(morgan('combined')); // Apache-style logs
ORDER

The Correct Middleware Order

These packages should be in every production Express app:
Copy to clipboard
// src/index.ts — recommended middleware order
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import morgan from 'morgan';
import rateLimit from 'express-rate-limit';
import { errorHandler } from './middleware/error.js';
import apiRoutes from './routes/index.js';

const app = express();

// 1. Security headers (first, always)
app.use(helmet());

// 2. CORS (before any route that could be preflight-requested)
app.use(cors({ origin: process.env.ALLOWED_ORIGINS }));

// 3. Body parsing
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true }));

// 4. Request logging
app.use(morgan('dev'));

// 5. Rate limiting
app.use('/api', rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));

// 6. Routes
app.use('/api', apiRoutes);

// 7. Error handler (must be last)
app.use(errorHandler);

app.listen(3000);

Explore project snapshots or discuss custom web solutions.

Security is always excessive until it's not enough.

Robbie Sinclair

Thank You for Spending Your Valuable Time

I truly appreciate you taking the time to read blog. Your valuable time means a lot to me, and I hope you found the content insightful and engaging!
Front
Back
Right
Left
Top
Bottom
FAQ's

Frequently Asked Questions

The request hangs indefinitely. The client gets a timeout error. Always call `next()` unless you've sent a response.

Yes — pass middleware as an argument between the path and the handler: `app.get('/admin', requireAuth, adminHandler)`.

Express identifies error-handling middleware specifically by function arity (4 args: `err, req, res, next`). With 3 parameters, it won't be treated as an error handler.

Morgan is fine for development. In production, use a structured logger like **Pino** or **Winston** that outputs JSON for log aggregation tools (Datadog, CloudWatch, etc.).

Helmet is a great start — it sets 14 HTTP security headers. But security is layered: also use rate limiting, validate inputs, use HTTPS, and keep dependencies updated.

Comments are closed