Episode 3 — NodeJS MongoDB Backend Architecture / 3.6 — Middleware in Express

3.6 — Interview Questions: Middleware in Express

Common interview questions about middleware concepts, the pipeline, error handling, security middleware, and custom middleware patterns.


< Exercise Questions | Quick Revision >


How to use this material (instructions)

  1. Read lessons first -- README.md, then 3.6.a through 3.6.f.
  2. Answer aloud before reading the model answer -- this simulates interview pressure.
  3. Pair with 3.6-Exercise-Questions.md for hands-on practice.
  4. Quick review -- 3.6-Quick-Revision.md for last-minute revision.

Beginner Level

Q1: What is middleware in Express? What are the three things it can do?

Why interviewers ask: Middleware is the core concept of Express -- if you cannot explain it, you do not understand the framework.

Model answer:

Middleware is any function that has access to the request object (req), the response object (res), and the next function. Middleware forms a sequential pipeline that every request passes through before a response is sent.

A middleware function can do three things:

  1. Modify req or res -- add properties, parse data, set headers.
  2. End the request-response cycle -- send a response with res.send(), res.json(), etc.
  3. Call next() -- pass control to the next middleware in the stack.

If a middleware does neither send a response nor call next(), the request hangs and the client eventually receives a timeout.


Q2: What does next() do? What happens if you forget to call it?

Why interviewers ask: Tests understanding of the middleware pipeline -- forgetting next() is a common source of bugs.

Model answer:

next() passes control from the current middleware to the next middleware in the stack. Express processes middleware in the order it is registered, and next() is the mechanism that moves the request along this chain.

If you forget to call next() and do not send a response, the request hangs indefinitely. The client sees no response and eventually times out. This is one of the hardest bugs to debug because there is no error message -- the server simply does nothing.

// BUG -- missing next(), request hangs
app.use((req, res, next) => {
  console.log('Request received');
  // forgot next() and forgot res.send()
});

// CORRECT
app.use((req, res, next) => {
  console.log('Request received');
  next();  // pass to next middleware
});

next() can also be called with an error: next(new Error('something failed')). This skips all remaining normal middleware and jumps to the first error-handling middleware.


Q3: Why does the order of middleware registration matter?

Why interviewers ask: Tests practical understanding -- incorrect ordering is one of the most common middleware bugs.

Model answer:

Express executes middleware in the exact order it is registered with app.use(). If you place express.json() after your route handlers, req.body will be undefined because the body has not been parsed yet. If you place your 404 handler before your routes, every request returns 404.

// WRONG order -- req.body is undefined in the route
app.post('/api/users', createUser);
app.use(express.json());  // too late!

// CORRECT order
app.use(express.json());  // parse body first
app.post('/api/users', createUser);  // body is available

The correct order for a typical Express app is:

  1. Body parsers (express.json(), express.urlencoded())
  2. Security middleware (helmet, cors)
  3. Logging middleware (morgan)
  4. Authentication middleware
  5. Route handlers
  6. 404 catch-all
  7. Error-handling middleware (last)

Q4: What are the three built-in middleware functions in Express?

Why interviewers ask: Tests whether you know what Express provides out of the box versus what requires third-party packages.

Model answer:

Express 4.x+ ships with three built-in middleware functions:

MiddlewarePurposeUsage
express.json()Parses incoming JSON request bodies and populates req.bodyapp.use(express.json())
express.urlencoded({ extended })Parses URL-encoded form data (HTML form submissions) and populates req.bodyapp.use(express.urlencoded({ extended: true }))
express.static(root)Serves static files (HTML, CSS, JS, images) from a directoryapp.use(express.static('public'))

Everything else -- CORS, helmet, logging, sessions, rate limiting -- requires third-party packages. In Express 3.x, many of these were built-in (like bodyParser, cookieParser), but they were extracted into separate packages in Express 4 to keep the core minimal.


Q5: What is the difference between application-level and router-level middleware?

Why interviewers ask: Tests ability to organize middleware in a real application -- knowing when to use each is a practical skill.

Model answer:

Application-level middleware is registered on the app object with app.use() or app.METHOD(). It runs for all requests (or all requests matching a path prefix) across the entire application.

Router-level middleware is registered on an express.Router() instance with router.use() or router.METHOD(). It runs only for routes defined on that specific router.

// Application-level -- runs for ALL requests
app.use(express.json());
app.use(morgan('dev'));

// Router-level -- runs only for /api/admin/* routes
const adminRouter = express.Router();
adminRouter.use(requireAdmin);  // only applies to this router
adminRouter.get('/dashboard', getDashboard);
adminRouter.get('/users', manageUsers);
app.use('/api/admin', adminRouter);

Router-level middleware is useful for applying per-resource or per-section policies (admin auth, rate limiting) without affecting the rest of the application.


Intermediate Level

Q6: How does error-handling middleware work in Express? Why must it have exactly four parameters?

Why interviewers ask: Error handling separates production code from tutorial code. This is a critical concept.

Model answer:

Error-handling middleware has the signature (err, req, res, next) -- exactly four parameters. Express uses the function's .length property (number of declared parameters) to distinguish error handlers from regular middleware. If you write three parameters, Express treats it as regular middleware and never routes errors to it.

When any middleware calls next(err) (passing an argument to next), Express skips all normal middleware and jumps to the first error-handling middleware in the stack.

// Route that triggers an error
app.get('/api/users/:id', async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    if (!user) return next(new AppError('Not found', 404));
    res.json(user);
  } catch (err) {
    next(err);
  }
});

// Error handler -- MUST have exactly 4 params
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    status: 'error',
    message: err.message
  });
});

Common mistake: writing (err, req, res) => {} with only three parameters. Express sees three params and treats it as regular middleware -- errors are never caught.


Q7: What is CORS and why do you need cors middleware?

Why interviewers ask: CORS issues are among the most common frontend-backend integration problems. Interviewers expect you to understand the mechanism.

Model answer:

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that blocks JavaScript from making HTTP requests to a different origin (domain, port, or protocol) than the one that served the page. If your frontend runs on http://localhost:3000 and your API runs on http://localhost:5000, the browser blocks the API call by default.

The cors middleware adds the required Access-Control-Allow-Origin and related headers to your responses, telling the browser the request is permitted.

const cors = require('cors');

// Allow all origins (development only)
app.use(cors());

// Allow specific origins (production)
app.use(cors({
  origin: ['https://myapp.com', 'https://admin.myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  credentials: true  // allow cookies
}));

For non-simple requests (PUT, DELETE, custom headers), the browser sends a preflight request using the OPTIONS method before the actual request. The server must respond with the correct CORS headers. The cors middleware handles this automatically.

Never use cors({ origin: '*' }) with credentials: true in production -- the CORS specification forbids this combination, and browsers reject it.


Q8: What is helmet and what does it protect against?

Why interviewers ask: Security awareness is expected for any backend developer. Helmet is the standard answer for HTTP security headers.

Model answer:

helmet is a middleware that sets various HTTP security headers to protect against common web vulnerabilities. It is a collection of 15+ smaller middleware functions, each setting one or more headers.

const helmet = require('helmet');
app.use(helmet());

Key headers it sets:

HeaderWhat It DoesAttack Prevented
Content-Security-PolicyControls which resources the browser can loadXSS, data injection
X-Content-Type-Options: nosniffPrevents browser from MIME-sniffingMIME confusion attacks
X-Frame-Options: SAMEORIGINPrevents the page from being embedded in iframesClickjacking
Strict-Transport-SecurityForces HTTPS connectionsMan-in-the-middle, SSL stripping
X-XSS-Protection: 0Disables buggy browser XSS filter (CSP is better)Relies on CSP instead

helmet() with no arguments enables sensible defaults. You can configure individual headers: helmet({ contentSecurityPolicy: false }) to disable specific ones.


Q9: What is rate limiting and how do you implement it in Express?

Why interviewers ask: Rate limiting is a fundamental API protection mechanism -- interviewers want to know you think about abuse prevention.

Model answer:

Rate limiting restricts the number of requests a client can make within a time window. It protects against brute-force attacks, DDoS, and API abuse.

The most common package is express-rate-limit:

const rateLimit = require('express-rate-limit');

// General API rate limit
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 100,                   // 100 requests per window
  message: { error: 'Too many requests, please try again later' },
  standardHeaders: true,      // Return rate limit info in headers
  legacyHeaders: false
});
app.use('/api', apiLimiter);

// Stricter limit for login (prevent brute force)
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: 'Too many login attempts' }
});
app.use('/api/auth/login', loginLimiter);

By default, express-rate-limit uses an in-memory store, which resets on server restart and does not work across multiple server instances. For production, use a shared store like Redis (rate-limit-redis).


Q10: How do you write a custom middleware factory (higher-order middleware)?

Why interviewers ask: Tests ability to write reusable, configurable middleware -- a pattern used constantly in production.

Model answer:

A middleware factory is a function that returns a middleware function. It accepts configuration parameters and uses closures to make them available to the returned middleware. This pattern lets you create reusable middleware with different configurations.

// Factory: accepts allowed roles, returns middleware
const requireRole = (...roles) => {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Not authenticated' });
    }
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
};

// Usage: different configurations for different routes
app.get('/api/admin/dashboard', requireRole('admin'), getDashboard);
app.delete('/api/users/:id', requireRole('admin', 'superadmin'), deleteUser);
app.get('/api/profile', requireRole('user', 'admin', 'superadmin'), getProfile);

Other common factories: cacheControl(maxAge), validateBody(schema), requireFields(...fields). The pattern is: outer function takes config, inner function is the actual middleware.


Advanced Level

Q11: Explain the difference between next(), next('route'), and next(error). When do you use each?

Why interviewers ask: Tests deep understanding of the Express routing internals -- most developers only know next().

Model answer:

CallBehaviorUse Case
next()Passes control to the next middleware or handler in the current stackNormal flow -- move to next middleware
next('route')Skips remaining handlers on the current route and moves to the next route definitionConditional routing -- skip to alternative handler
next(err)Skips all normal middleware and jumps to the first error-handling middlewareError forwarding -- centralized error handling
// next('route') example -- skip to next route if user is not admin
app.get('/api/data',
  (req, res, next) => {
    if (req.user.role !== 'admin') return next('route');
    next(); // continue to admin handler below
  },
  (req, res) => {
    res.json({ data: 'admin-only data' });
  }
);

// This route catches the 'route' skip
app.get('/api/data', (req, res) => {
  res.json({ data: 'public data' });
});

next('route') only works with app.METHOD() or router.METHOD() handlers -- it does not work with app.use().


Q12: What is the "async middleware problem" in Express 4, and how do you solve it?

Why interviewers ask: Unhandled promise rejections crashing production servers is a real and serious issue.

Model answer:

In Express 4, if an async middleware throws an error (or a promise rejects), Express does not catch it. The error bypasses the error-handling middleware entirely and either crashes the process or triggers an unhandledRejection warning.

// DANGEROUS in Express 4 -- unhandled rejection
app.get('/api/users', async (req, res) => {
  const users = await User.find();  // if this throws, Express never catches it
  res.json(users);
});

Solution 1: try/catch in every handler

app.get('/api/users', async (req, res, next) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (err) {
    next(err);  // forward to error handler
  }
});

Solution 2: asyncHandler wrapper (DRY)

const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Clean -- no try/catch needed
app.get('/api/users', asyncHandler(async (req, res) => {
  const users = await User.find();
  res.json(users);
}));

Note: Express 5 fixes this -- rejected promises from async handlers are automatically forwarded to the error handler. Until Express 5 is stable, use the asyncHandler pattern.


Q13: How would you implement a request logging middleware that tracks response time?

Why interviewers ask: Tests understanding of the full request lifecycle -- logging happens both when the request arrives and after the response is sent.

Model answer:

The trick is hooking into the res.on('finish') event, which fires after the response has been sent to the client. This lets you measure the full round-trip time.

const logger = (req, res, next) => {
  const start = Date.now();
  const { method, originalUrl } = req;

  // 'finish' fires when the response is fully sent
  res.on('finish', () => {
    const duration = Date.now() - start;
    const { statusCode } = res;
    console.log(`${method} ${originalUrl} ${statusCode} - ${duration}ms`);
  });

  next();
};

app.use(logger);

This is essentially what morgan does under the hood. The res.on('finish') pattern is critical because calling console.log before next() would only capture the arrival time, not the processing time. For production, you would write to a log file or log aggregation service instead of console.log.


Q14: Explain res.locals. How does it differ from adding properties to req?

Why interviewers ask: Tests knowledge of Express conventions for passing data between middleware -- choosing the right mechanism matters.

Model answer:

res.locals is an object on the response that is available throughout the lifetime of a single request. It is the official Express mechanism for passing data between middleware functions.

// Authentication middleware sets user on res.locals
app.use((req, res, next) => {
  res.locals.user = { id: 1, name: 'Alice', role: 'admin' };
  res.locals.requestId = crypto.randomUUID();
  next();
});

// Route handler accesses it
app.get('/api/dashboard', (req, res) => {
  const { user, requestId } = res.locals;
  res.json({ user, requestId });
});

res.locals vs req.customProperty:

res.localsreq.user (custom prop)
Official Express APIYesNo (convention only)
Available in templatesYes (auto-passed to res.render())No (must pass manually)
Scoped to requestYes (new object per request)Yes
Commonly used forTemplate data, request metadataAuth user (by convention: req.user)

In practice, req.user is used for the authenticated user (set by Passport.js and similar libraries), and res.locals is used for everything else -- template data, request IDs, computed values. Both approaches work, but res.locals is the Express-recommended way.


Q15: Design a middleware stack for a production Express API. What middleware do you include and in what order?

Why interviewers ask: Senior-level question -- tests real-world experience and ability to reason about middleware interactions.

Model answer:

const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
const app = express();

// 1. Security headers (first -- protect all responses)
app.use(helmet());

// 2. CORS (before any route handlers)
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(','),
  credentials: true
}));

// 3. Rate limiting (before body parsing to reject early)
app.use('/api', rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
app.use('/api/auth/login', rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }));

// 4. Body parsers
app.use(express.json({ limit: '10kb' }));  // limit body size
app.use(express.urlencoded({ extended: true, limit: '10kb' }));

// 5. Logging (after body parsing so we can log body size)
app.use(morgan('combined'));

// 6. Static files (served without hitting auth or routes)
app.use(express.static('public'));

// 7. Request enrichment (request ID, timing)
app.use((req, res, next) => {
  req.requestId = crypto.randomUUID();
  next();
});

// 8. Routes
app.use('/api/auth', authRouter);
app.use('/api/users', userRouter);
app.use('/api/products', productRouter);

// 9. 404 catch-all (after all routes)
app.use((req, res) => {
  res.status(404).json({ status: 'fail', message: 'Route not found' });
});

// 10. Global error handler (must be LAST)
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    status: statusCode >= 500 ? 'error' : 'fail',
    message: err.isOperational ? err.message : 'Something went wrong',
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
});

The order follows a principle: security first, parsing second, enrichment third, routing fourth, error handling last. Each middleware should be placed at the earliest point where it can do its job, and before anything that depends on it.


Quick-Fire Table

#QuestionOne-Line Answer
1Middleware signature(req, res, next) => {}
2Error middleware signature(err, req, res, next) => {} -- exactly 4 params
3What if you forget next()?Request hangs, client sees timeout
4Three built-in middlewareexpress.json(), express.urlencoded(), express.static()
5next(err) does what?Skips all normal middleware, jumps to error handler
6Why 4 params for error handler?Express checks function.length to identify error handlers
7What is CORS?Browser blocks cross-origin requests; cors middleware adds allow headers
8What is helmet?Sets security HTTP headers (CSP, HSTS, X-Frame-Options, etc.)
9Rate limiting packageexpress-rate-limit -- configure windowMs and max
10Middleware factory patternFunction that returns middleware: (config) => (req, res, next) => {}
11next('route')Skips remaining handlers on current route, jumps to next route
12Async middleware fixWrap with asyncHandler or use try/catch + next(err)
13res.locals purposePass data between middleware; auto-available in templates
14Error handler placementAfter all routes -- must be the last app.use()
15app.use() vs router.use()app.use = global; router.use = scoped to that router's routes

<- Back to 3.6 -- Middleware in Express (README)