Skip to content

Middleware

Middleware runs in order. Put your auth check after your data handler and wonder why everyone sees everything
Every request walks through a chain of functions before it reaches your route handler. Each one can modify the request , end the response , or pass to the next. Order defines behavior. One misplaced middleware and your entire security model collapses

what middleware is

A function that receives req , res , and next and either:

  • Modifies req or res and calls next() to continue the chain
  • Ends the response with res.send() , res.json() , res.end() etc
  • Throws or passes an error to next(err) which skips to error handlers
function myMiddleware(req , res , next) {
  // do something
  console.log(`${req.method} ${req.path}`)
  next() // pass control to next middleware
}

app.use() - global middleware

Middleware applied with app.use() runs on every request to every route

app.use(helmet())         // security headers - every route
app.use(cors())           // CORS - every route
app.use(express.json())   // parse JSON bodies - every route
app.use(morgan('dev'))    // logging - every route

Order in app.use() is the order middleware executes

built-in middleware

Express ships with several middleware functions built in (no extra install needed):

// JSON body parser - replaces deprecated body-parser
app.use(express.json())
// Options: limit , inflate , reviver , strict , type , verify

// URL-encoded body parser
app.use(express.urlencoded({ extended: true }))
// extended: true uses qs library (nested objects)
// extended: false uses query-string library (flat)

// Static file serving
app.use(express.static('public'))

// Cookie parsing - no longer built-in in Express 4.x+
// Need: npm install cookie-parser

third-party middleware

The Express ecosystem is drowning in middleware packages. Here are the ones you actually need:

npm install helmet cors morgan express-rate-limit cookie-parser
const helmet = require('helmet')
const cors = require('cors')
const morgan = require('morgan')
const rateLimit = require('express-rate-limit')

app.use(helmet())                                         // security headers
app.use(cors({ origin: 'https://yourapp.com' }))          // CORS policy
app.use(morgan('combined'))                                // HTTP request logging
app.use(rateLimit({ windowMs: 15 * 60 * 1000 , max: 100 })) // rate limiting

custom middleware

Writing your own middleware is trivial - and dangerous if you don't understand the chain

// request timer
app.use((req , res , next) => {
  req.startTime = Date.now()
  res.on('finish' , () => {
    const duration = Date.now() - req.startTime
    console.log(`${req.method} ${req.path} took ${duration}ms`)
  })
  next()
})

// auth check
const requireAuth = (req , res , next) => {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: 'No auth token' })
  }
  next()
}

// apply to specific routes
app.get('/protected' , requireAuth , handler)

middleware execution order

This is where people fuck up. Hard

app.use(express.json())          // 1 - body parser
app.use(cors())                  // 2 - CORS headers

app.get('/data' , handler)       // 3 - route

app.use((err , req , res , next) => {  // 4 - error handler
  res.status(500).json({ error: 'Oops' })
})

What happens:

  1. Request hits Express
  2. express.json() runs - parses body if JSON
  3. cors() runs - sets headers , handles preflight
  4. Routes checked - /data matches , handler runs
  5. If handler throws , error handler catches it

Now the broken version:

app.use((err , req , res , next) => {  // ERROR HANDLER FIRST
  res.status(500).json({ error: 'Oops' })
})

app.use(express.json())          // BODY PARSER SECOND
app.use(cors())                  // CORS THIRD
app.get('/data' , handler)       // ROUTE LAST

Error handler is just a middleware with 4 parameters. If you define it first it runs first - before the route , before body parsing , before everything. It'll catch nothing because nothing has happened yet

middleware execution order rules

  1. Error handlers must be last - after all routes and middleware
  2. Security middleware first - Helmet , CORS before anything
  3. Body parsers before routes - routes need parsed bodies
  4. Auth middleware before protected routes - obviously

multiple middleware on a route

const validate = (req , res , next) => {
  if (!req.body.name) return res.status(400).json({ error: 'Name required' })
  next()
}

const sanitize = (req , res , next) => {
  req.body.name = req.body.name.trim()
  next()
}

const log = (req , res , next) => {
  console.log('Creating user:', req.body.name)
  next()
}

router.post('/users' , validate , sanitize , log , createUserHandler)

This runs validate -> sanitize -> log -> createUserHandler in that order

common middleware pitfalls

// PITFALL 1: Forgetting to call next()
app.use((req , res , next) => {
  console.log('I run but nothing happens after')
  // forgot next() - request hangs until timeout
})

// PITFALL 2: Mixing up req and res parameter order
// (req , res , next) NOT (req , next , res)
// You'd be surprised how common this is

// PITFALL 3: Async errors not caught
app.use(async (req , res , next) => {
  const data = await db.query('SELECT ...') // if this throws
  // Express doesn't catch promise rejections
  // Use express-async-errors or wrap manually
  next()
})

prerequisites

express_03_routing.md - route params , query strings , express.Router()


next → express_05_static.md