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
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.
app.use(express.json()); // all routes
app.get('/users', (req, res, next) => { next(); }); // GET /users only2. 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.
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.
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:
app.use(express.json()); // parses JSON bodies
app.use(express.urlencoded({ extended: true })); // parses form data
app.use(express.static('public')); // serves static files5. Third-party
npm packages that plug into the same middleware system:
app.use(require('helmet')()); // security headers
app.use(require('cors')()); // CORS headers
app.use(require('morgan')('dev')); // request loggingExecution 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.
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 lastRouter 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
// 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
// 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.
// 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.
// 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
app.use('/users', userRouter); // matches, runs
app.use('/users/:id', idRouter); // never runs - shadowed by line aboveFix: register routers from specific to general, or consolidate the logic into one router.
Real-world usage
morganfor HTTP logging in almost every Express app:app.use(morgan('combined'))helmetas a default security baseline:app.use(helmet())- Router-level auth for
/api/v1route groups in REST APIs - NestJS uses application-level middleware for guards and interceptors under the hood
- NextAuth.js wraps its
/api/authhandler group inexpress.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
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
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 -> 201The 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
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 readyA concise answer to help you respond confidently on this topic during an interview.