Skip to main content

What are the different types of middleware in Express.js?

Middleware in Express.js - functions that run between receiving an HTTP request and sending a response, each with access to req, res, and next().

Theory

TL;DR

  • Express has 5 middleware types: application-level, router-level, error-handling, built-in, third-party
  • Think of it as an assembly line: each station processes the request, then passes it forward with next()
  • Application-level runs globally; router-level is scoped to a path prefix
  • Error-handling middleware uses a 4-parameter signature (err, req, res, next) and must be mounted last
  • No next() call in a handler means the request hangs forever

Quick example

js
const express = require('express'); const app = express(); // Application-level: runs on all requests app.use((req, res, next) => { console.log(`${req.method} ${req.path}`); // GET /api/users next(); }); // Router-level: scoped to /api const router = express.Router(); router.use((req, res, next) => { console.log('Router hit'); // only fires for /api/* requests next(); }); app.use('/api', router);

On GET /api/users, both logs fire in order. On GET /, only the first one fires. That one difference explains the whole app vs router split.

Middleware types

1. Application-level

Bound to the app instance via app.use() or app.METHOD(). Runs globally or for a specific path.

js
app.use(express.json()); // all routes app.get('/users', (req, res, next) => { next(); }); // GET /users only

2. Router-level

Bound to express.Router(). Identical behavior to application-level, but scoped to the mount path. This is the standard way to split a large API into modules.

js
const userRouter = express.Router(); userRouter.use(authCheck); // runs only for /api/users/* app.use('/api/users', userRouter);

3. Error-handling

Express identifies this type by the 4-parameter signature (err, req, res, next). When any middleware calls next(err), Express skips to this handler. Always mount it last.

js
app.use((err, req, res, next) => { res.status(err.status || 500).json({ error: err.message }); });

4. Built-in

Shipped with Express since version 4.16. Three functions cover the most common parsing needs:

js
app.use(express.json()); // parses JSON bodies app.use(express.urlencoded({ extended: true })); // parses form data app.use(express.static('public')); // serves static files

5. Third-party

npm packages that plug into the same middleware system:

js
app.use(require('helmet')()); // security headers app.use(require('cors')()); // CORS headers app.use(require('morgan')('dev')); // request logging

Execution order

Express keeps a stack of middleware functions per app or router, executed in mount order. When a request arrives, Express walks the stack top to bottom. Each function calls next() to continue, sends a response to stop, or calls next(err) to jump to the error handler.

js
app.use(cors()); // 1 app.use(helmet()); // 2 app.use(express.json()); // 3 app.use('/api', routes); // 4 - route handlers app.use(notFound); // 5 - 404 catch-all app.use(errorHandler); // 6 - must be last

Router stacks are sub-stacks merged into the app stack at the mount path. So userRouter.use(authCheck) combined with app.use('/api', userRouter) means authCheck only runs when the path starts with /api.

Common mistakes

Forgetting next() in async middleware

js
// Wrong - request hangs forever app.use(async (req, res, next) => { await db.connect(); // no next() here }); // Right app.use(async (req, res, next) => { try { await db.connect(); next(); } catch (err) { next(err); } });

Mounting the error handler too early

js
// Wrong - error handler defined before routes app.use((err, req, res, next) => { res.status(500).send('Error'); }); app.get('/', (req, res) => res.send('Hi')); // Right app.get('/', (req, res) => res.send('Hi')); app.use((err, req, res, next) => { res.status(500).send('Error'); });

Registering express.json() after the router

The body-parser ordering issue catches almost everyone at some point. It's the first thing to check when req.body shows up as undefined.

js
// Wrong - req.body is undefined inside userRouter app.use('/api', userRouter); app.use(express.json()); // too late // Right app.use(express.json()); // body parser first app.use('/api', userRouter);

Uncaught async errors in Express 4

An async function that throws does not automatically call next(err) in Express 4. The error bypasses the error handler entirely.

js
// Wrong in Express 4 - error never reaches the error handler app.get('/data', async (req, res, next) => { throw new Error('Boom'); }); // Right - explicit try/catch app.get('/data', async (req, res, next) => { try { const data = await fetchData(); res.json(data); } catch (err) { next(err); } });

Express 5 (currently in beta) handles async throws natively.

Overlapping router paths shadow each other

js
app.use('/users', userRouter); // matches, runs app.use('/users/:id', idRouter); // never runs - shadowed by line above

Fix: register routers from specific to general, or consolidate the logic into one router.

Real-world usage

  • morgan for HTTP logging in almost every Express app: app.use(morgan('combined'))
  • helmet as a default security baseline: app.use(helmet())
  • Router-level auth for /api/v1 route groups in REST APIs
  • NestJS uses application-level middleware for guards and interceptors under the hood
  • NextAuth.js wraps its /api/auth handler group in express.Router()

Follow-up questions

Q: What is the execution order with nested routers?
A: App-level middleware runs first, then the outer router's middleware, then the inner router's, then the route handler. next() chains them linearly top to bottom.

Q: How does app.METHOD() differ from app.use()?
A: app.get() matches only GET requests and an exact path. app.use() matches all HTTP methods and any path that starts with the given prefix. That is why global middleware always uses app.use().

Q: What happens if middleware calls next() twice?
A: The second call is ignored. No error is thrown, but behavior becomes unpredictable if a response was already sent by then.

Q: What is the difference between express.json() and body-parser?
A: No practical difference. body-parser was folded into Express core in version 4.16. express.json() calls the same underlying code.

Q (senior): In a microservice with 50+ middleware functions, how do you keep the stack manageable without losing execution order guarantees?
A: Group related functions into composed arrays: const apiPipeline = [cors(), helmet(), express.json()], then app.use('/api', apiPipeline, routes). Profile bottlenecks with clinic.js or 0x. Avoid synchronous blocking work anywhere in the stack.

Examples

Basic: global logging and JSON parsing

js
const express = require('express'); const app = express(); // Built-in: parse JSON bodies before any route reads req.body app.use(express.json()); // Application-level: log every incoming request app.use((req, res, next) => { console.log(`${req.method} ${req.path}`); // GET /users next(); }); app.get('/users', (req, res) => { res.json([{ id: 1, name: 'Alice' }]); }); app.listen(3000);

Every request hits the logger first, then the route handler. Swap the order of those two app.use() calls and the logger still works, but if you add a POST route that reads req.body, the body will be undefined.

Intermediate: router-level auth guard

js
const express = require('express'); const app = express(); const userRouter = express.Router(); app.use(express.json()); // Router-level: token check only for /api/users userRouter.use((req, res, next) => { if (req.headers.authorization !== 'Bearer secret') { return res.status(401).json({ error: 'Unauthorized' }); } next(); }); userRouter.post('/', (req, res) => { res.status(201).json({ id: 1, name: req.body.name }); // { id: 1, name: 'Alice' } }); app.use('/api/users', userRouter); app.listen(3000); // POST /api/users without token -> 401 // POST /api/users with Bearer secret -> 201

The auth check runs only on /api/users/*. A public route like GET /health never touches it. That is the concrete benefit of keeping middleware at the router level instead of global.

Senior: async error handling across the full pipeline

js
const express = require('express'); const app = express(); app.use(express.json()); const findUser = (id) => { if (id !== '1') { const err = new Error('User not found'); err.status = 404; throw err; } return { id: 1, name: 'Alice' }; }; app.get('/users/:id', async (req, res, next) => { try { const user = findUser(req.params.id); res.json(user); } catch (err) { next(err); // routes to error handler below } }); // 4 parameters, mounted last app.use((err, req, res, next) => { console.error(err.stack); // Error: User not found\n at ... res.status(err.status || 500).json({ error: err.message }); }); app.listen(3000); // GET /users/1 -> { id: 1, name: 'Alice' } // GET /users/99 -> 404 { error: 'User not found' }

Drop the try/catch and throw directly in the async handler in Express 4, and the error never reaches the error middleware. The process crashes in production instead. Always wrap async route logic explicitly until Express 5 becomes stable.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?