NodeJSWriting Custom Middleware

Writing Custom Middleware

When no npm package fits — or the job is small enough not to warrant one — you write your own middleware. It's just a function with the (req, res, next) signature. This page covers the patterns that make custom middleware robust: doing one thing, attaching data to req, the configurable middleware factory pattern, async middleware, and the rules that keep the pipeline flowing.

The simplest custom middleware

JS
// Attach a high-resolution start time, then continue:
function requestTimer(req, res, next) {
  req.startTime = process.hrtime.bigint()
  next()                                  // ALWAYS hand off (or respond)
}

app.use(requestTimer)
A middleware does one thing, then calls `next()` or responds
Keep each middleware single-purpose — one concern, easy to test and reorder. It must end in exactly one of two ways: call `next()` to pass control on, or send a response (`res.json`/`res.send`/etc.) to end the cycle. [Do neither and the request hangs](/nodejs/middleware-intro); do both and you risk "headers already sent".
Attaching data to req for later handlers

JS
// Resolve the current user once; every later handler can read req.user:
async function attachUser(req, res, next) {
  const token = req.get('Authorization')?.replace('Bearer ', '')
  if (token) {
    req.user = await lookupUserByToken(token)   // may be undefined
  }
  next()
}

app.use(attachUser)

app.get('/me', (req, res) => {
  if (!req.user) return res.status(401).json({ error: 'Not logged in' })
  res.json(req.user)
})
`req` is the shared scratchpad that flows down the pipeline
The same `req` object is passed to every middleware in turn, so attaching a property (`req.user`, `req.requestId`) is the idiomatic way to pass computed data downstream. Namespace your additions to avoid clobbering Express's own properties, and treat them as a contract — downstream handlers depend on `req.user` being set by the middleware above them.
The factory pattern — configurable middleware

The most important pattern: a function that returns a middleware, so callers can configure it. This is exactly how express.json({ limit }) and rateLimit({ max }) work — and how you should write reusable middleware:

JS
// Outer function takes options; inner function is the middleware:
function requireRole(role) {
  return (req, res, next) => {
    if (!req.user) return res.status(401).json({ error: 'Login required' })
    if (req.user.role !== role) {
      return res.status(403).json({ error: 'Forbidden' })
    }
    next()
  }
}

// Each call produces a tailored middleware:
app.get('/admin',  requireRole('admin'),  adminHandler)
app.get('/editor', requireRole('editor'), editorHandler)
Return a closure to capture configuration
The outer `requireRole(role)` runs **once** at setup time and closes over `role`; the inner `(req, res, next)` it returns runs **per request**. This closure pattern lets one middleware serve many configurations without globals. Tell-tale sign you need it: you're tempted to copy-paste a middleware and change one constant — make that constant a parameter instead.
Async middleware and errors

JS
// Express 5: a rejected promise is forwarded to the error handler automatically.
function loadResource(req, res, next) {
  // Returning the promise (or async/await) lets Express catch rejections:
  return (async () => {
    const item = await db.items.find(req.params.id)
    if (!item) {
      const err = new Error('Not found')
      err.status = 404
      throw err                  // → routed to error middleware in Express 5
    }
    req.item = item
    next()
  })()
}
Express 4 does NOT auto-catch async errors — wrap or try/catch
In **Express 5**, a thrown error or rejected promise in an `async` middleware is automatically passed to `next(err)`. In **Express 4** it is *not* — an unhandled rejection escapes the pipeline, the request hangs, and the process may crash. On Express 4 you must `try/catch` and call `next(err)` yourself, or wrap handlers in a helper like `asyncHandler(fn)`. Check which major version you're on before relying on auto-forwarding.

The asyncHandler wrapper (Express 4 safety)

JS
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next)

app.get('/items/:id', asyncHandler(async (req, res) => {
  const item = await db.items.find(req.params.id)   // rejection → next(err)
  res.json(item)
}))
Conditionally skipping work

JS
// Short-circuit non-API paths without blocking them:
function apiOnly(req, res, next) {
  if (!req.path.startsWith('/api')) return next()   // skip, but continue
  res.set('X-API', 'true')
  next()
}

// 'route' skips the rest of THIS route's stack; 'router' skips the router:
app.get('/legacy', (req, res, next) => {
  if (req.query.v === '2') return next('route')      // jump to next matching route
  res.send('legacy v1')
})
`next('route')` is different from `next()` and `next(err)`
Three distinct calls: `next()` → next middleware; `next(err)` → error handler; and the special string `next('route')` → skip remaining handlers on the **current** route and try the next matching route (similarly `next('router')` exits the current router). The string form is rarely needed, but it's the clean way to bail out of a route stack without erroring.
A complete, reusable example

middleware/requestId.js

JS
import { randomUUID } from 'node:crypto'

// Tag each request with an id and echo it back in a header.
export function requestId(headerName = 'X-Request-Id') {
  return (req, res, next) => {
    const id = req.get(headerName) || randomUUID()
    req.id = id                       // available to all later middleware/logs
    res.set(headerName, id)           // returned to the client for tracing
    next()
  }
}

// usage: app.use(requestId())
Custom middleware checklist
  • Single responsibility — one concern per function.

  • Always end the cycle: next(), next(err), or a response — exactly one.

  • Use the factory pattern when behavior should be configurable.

  • Namespace anything you attach to req to avoid collisions.

  • On Express 4, catch async errors and forward with next(err).

  • Register it in the right order relative to what it depends on.

Next
The special four-argument middleware that catches everything: [Error-Handling Middleware](/nodejs/error-middleware).