15 Express Middleware Interview Questions and Answers

·12 min read
By ·Updated
expressinterview-questionsmiddlewarenodejsbackenderror-handling

Express is a routing and middleware framework whose behavior depends heavily on registration order and control flow. A strong interview answer explains not only next(), but also route matching, returned Promises in Express 5, error boundaries, response completion, and trust boundaries.

These 15 questions use Express 5 as the current baseline and keep Express 4 wrappers only as migration context.

Table of Contents

  1. Middleware Fundamentals Questions
  2. Middleware Execution Questions
  3. Error Handling Questions
  4. Middleware Types Questions
  5. Data Passing Questions
  6. Quick Reference

Middleware Fundamentals Questions

These questions test your understanding of what middleware is and how it works.

What is middleware in Express.js?

Middleware functions are functions that execute during the request-response cycle. They have access to the request object, response object, and a next function. Middleware can execute code, modify the request and response, end the cycle by sending a response, or call next() to pass control to the next middleware in the stack.

Express middleware works like a pipeline. Each request flows through a series of middleware functions in the order they're defined. Each function can process the request, modify it, or decide whether to pass it along.

How does the middleware pipeline work?

const express = require('express');
const app = express();
 
// 1. Logging middleware (runs on every request)
app.use((req, res, next) => {
    console.log(`${req.method} ${req.path}`);
    next(); // Pass to next middleware
});
 
// 2. Parse bounded JSON only for API routes
app.use('/api', express.json({limit: '100kb'}));
 
// 3. Authentication middleware
const auth = async (req, res, next) => {
    const [scheme, token, extra] = (req.get('authorization') ?? '').split(/\s+/);
    if (scheme?.toLowerCase() !== 'bearer' || !token || extra) {
        return res.status(401).json({error: 'Unauthorized'});
    }
 
    try {
        req.user = await verifyAccessToken(token, {
            issuer: EXPECTED_ISSUER,
            audience: EXPECTED_AUDIENCE,
            algorithms: ['RS256']
        });
        return next();
    } catch {
        return res.status(401).json({error: 'Unauthorized'});
    }
};
 
// 4. Protected route with middleware
app.get('/api/profile', auth, (req, res) => {
    res.json({user: req.user});
});
 
// 5. A 404 fallback comes after routes
app.use((req, res) => {
    res.status(404).json({error: 'Not found'});
});
 
// 6. Error handling comes after the layers it covers
app.use((err, req, res, next) => {
    if (res.headersSent) return next(err);
 
    const candidate = Number(err.status ?? err.statusCode);
    const status = candidate >= 400 && candidate <= 599 ? candidate : 500;
    console.error({err, method: req.method, path: req.path});
    res.status(status).type('application/problem+json').json({
        type: 'about:blank',
        title: status >= 500 ? 'Internal Server Error' : 'Request failed',
        status
    });
});

Walk through the flow:

  1. Request arrives at /api/profile
  2. Logging middleware runs, logs the request, calls next()
  3. express.json() parses the body, calls next()
  4. auth middleware checks token - if missing, sends 401 and stops
  5. If token is valid, attaches user to req and calls next()
  6. Route handler sends the response
  7. A synchronous throw or rejected Promise returned by Express 5 middleware enters the error path

Errors thrown later by detached callbacks, event emitters, or Promises that were not returned still need handling at their own asynchronous boundary.

Middleware Execution Questions

These questions test your understanding of middleware execution order and common pitfalls.

What happens if you don't call next() in middleware?

If middleware does not call next(), complete the response, or intentionally keep it open for streaming, Express will not continue to the next layer. The connection remains pending until another component or timeout closes it.

app.use((req, res, next) => {
    console.log('A');
    next();
});
 
app.use((req, res, next) => {
    console.log('B');
    // Oops, forgot to call next()
});
 
app.get('/test', (req, res) => {
    console.log('C');
    res.send('Hello');
});

When you request /test, it logs A, then B, and the route handler never runs. In a production system, define server and proxy timeouts so this bug does not consume a connection without a bound. Also return after sending a response so code cannot accidentally call next() or send twice.

Why does middleware order matter?

Express evaluates matching layers in registration order. A route registered before a parser will not see that parser's result. An error handler registered before a later route will not receive an error propagated forward from that route. Authentication and resource-level authorization must run before the protected action they govern.

// WRONG - route defined before body parser
app.post('/data', (req, res) => {
    console.log(req.body); // undefined!
    res.send('OK');
});
app.use(express.json({limit: '100kb'}));
 
// CORRECT
app.use(express.json({limit: '100kb'}));
app.post('/data', (req, res) => {
    console.log(req.body); // { ... }
    res.send('OK');
});

Error Handling Questions

These questions test your understanding of error middleware and async error patterns.

How does error handling middleware work in Express?

Error middleware must keep four parameters: (err, req, res, next). Passing a value to next() other than the routing sentinels enters the error path; synchronous throws and rejected Promises returned from Express 5 handlers do the same. Matching ordinary handlers are skipped until an error handler receives control.

// Note the four parameters - this is what makes it error middleware
app.use((err, req, res, next) => {
    if (res.headersSent) return next(err);
 
    const proposed = Number(err.status ?? err.statusCode);
    const status = proposed >= 400 && proposed <= 599 ? proposed : 500;
    console.error({err, method: req.method, path: req.path});
    res.status(status).type('application/problem+json').json({
        type: 'about:blank',
        title: status >= 500 ? 'Internal Server Error' : 'Request failed',
        status
    });
});

If headers were already sent, delegate to the default handler so Express can close the connection appropriately. Map expected domain errors deliberately, do not trust an arbitrary status or expose stack/message details, and avoid logging authorization headers, cookies, tokens, or sensitive bodies.

How does Express 5 handle async errors?

Express 5 automatically calls next(value) when a route handler or middleware returns a Promise that rejects or an async function throws. No wrapper is needed for that returned Promise.

app.get('/users/:id', async (req, res) => {
    const user = await findUser(req.params.id);
    if (!user) {
        const error = new Error('User not found');
        error.status = 404;
        throw error; // Rejected handler Promise enters Express 5 error flow
    }
    res.json(user);
});

Express cannot observe an asynchronous task that the handler does not return or await. Errors from timers, event emitters, streams, or detached work need handling at that boundary, and background work should not depend on an already completed request lifecycle.

How do you handle async errors when maintaining Express 4?

Express 4 does not automatically forward rejected handler Promises. Return a wrapper that attaches .catch(next), or catch and call next(error) explicitly. This is migration guidance, not the default recommendation for new Express 5 code.

// Option 1: Try/catch with next(err)
app.get('/async-error', async (req, res, next) => {
    try {
        throw new Error('Async error');
    } catch (err) {
        next(err); // Manually pass to error handler
    }
});
 
// Option 2: Async wrapper function
const asyncHandler = (fn) => (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
};
 
app.get('/async-error', asyncHandler(async (req, res) => {
    throw new Error('Async error');
}));

When migrating, remove wrappers only after verifying that handlers return their Promise and testing error paths. Express 5 also changes route path syntax and some API behavior, so follow the migration guide rather than treating the upgrade as only an error-handling change.

Middleware Types Questions

Express documentation groups middleware into five categories; the boundaries describe where middleware comes from or is mounted, not five different function mechanisms.

What are the five types of middleware in Express?

Express documents application-level, router-level, error-handling, built-in, and third-party middleware. Application and router middleware differ mainly by mounting scope; error middleware is recognized by its four-argument signature.

What is application-level middleware?

Application-level middleware is bound to the app instance using app.use() or app.METHOD(). It can run on every request or only on specific paths.

// Runs on every request
app.use((req, res, next) => {
    req.requestTime = Date.now();
    next();
});
 
// Runs only on specific path
app.use('/api', (req, res, next) => {
    console.log('API request');
    next();
});

What is router-level middleware?

Router-level middleware works like application-level middleware but is bound to an express.Router() instance. This allows you to create modular, mountable route handlers.

const router = express.Router();
 
router.use((req, res, next) => {
    console.log('Router middleware');
    next();
});
 
router.get('/users', (req, res) => {
    res.json([]);
});
 
app.use('/api', router);

What are the built-in middleware functions in Express?

Express documents five built-in middleware functions: express.json(), express.raw(), express.text(), express.urlencoded(), and express.static().

app.use('/api/json', express.json({limit: '100kb'}));
app.use('/forms', express.urlencoded({extended: false, limit: '50kb'}));
app.use('/assets', express.static('public', {fallthrough: false}));

Parsed bodies are untrusted input. Set appropriate content types and limits, validate a schema after parsing, and use an absolute static path when process working directories can vary.

What are common third-party middleware packages?

Common packages include cors for CORS response policy, helmet for security-related headers, and morgan for access logging. Confirm current maintenance, compatibility, defaults, transitive dependencies, and threat model before adding one.

const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
 
app.use(cors({origin: ALLOWED_ORIGINS, credentials: true}));
app.use(helmet());
app.use(morgan('combined', {
    skip: (req) => req.path === '/healthz'
}));

CORS is a browser read-control mechanism, not authentication or authorization, and cors() with permissive defaults is not a considered policy. Security headers also do not replace input validation, output encoding, CSRF defenses where applicable, or resource-level authorization. Configure logs to avoid tokens, cookies, query secrets, and sensitive personal data.

Data Passing Questions

These questions test your understanding of how data flows through middleware.

How do you pass data between middleware functions?

Use request-scoped state, commonly a documented property on req or res.locals. res.locals is intended for variables available during the current request/response and is not shared between requests. In TypeScript, augment the Express types instead of hiding the field behind casts.

app.use((req, res, next) => {
    res.locals.requestContext = {
        requestId: req.get('x-request-id') ?? crypto.randomUUID()
    };
    next();
});
 
app.get('/test', (req, res) => {
    res.json({requestId: res.locals.requestContext.requestId});
});

Do not put request state in module-level globals. Validate data before trusting it, avoid collisions with framework/package fields, and do not return an untrusted client-supplied request ID without format and length controls.

What is the difference between app.use() and app.get()?

app.use() mounts middleware for all methods at a path prefix: app.use('/api', router) can receive /api and descendant paths, and Express adjusts req.url while preserving req.originalUrl and exposing req.baseUrl. app.get() registers GET handlers for its route path, including supported parameters, arrays, or regular expressions.

Express 5 uses current path-to-regexp route syntax, which differs from Express 4 for wildcard and optional patterns. Test migrated routes rather than assuming “exact path” captures every case.

How do you skip to the next route in Express?

Call next('route') to skip remaining middleware in the current route and move to the next matching route. This only works in middleware loaded using app.METHOD() or router.METHOD().

app.get('/user/:id',
    (req, res, next) => {
        if (req.params.id === '0') {
            return next('route'); // Skip to next route
        }
        next();
    },
    (req, res) => {
        res.send('Regular user');
    }
);
 
app.get('/user/:id', (req, res) => {
    res.send('Special user 0');
});

Quick Reference

ConceptWhat to Remember
Middleware signature(req, res, next) => {}
Error middleware(err, req, res, next) => {} (4 params)
Pass controlCall next()
Pass errorCall next(error)
Skip routeCall next('route')
Request remains pending ifNo next(), completed response, or intentional stream
Order ruleRegister parsing/policy before protected handlers and error handlers after covered layers
Pass dataUse documented request-scoped fields or res.locals

Frequently Asked Questions

What is middleware in Express.js?

Express middleware participates in the request-response cycle with access to req, res, and next. It can run code, change request or response state, end the cycle, or pass control. Middleware that neither ends or intentionally keeps the response open nor calls next() leaves the request without further handling.

What are the different types of middleware in Express?

Express documents five categories: application-level, router-level, error-handling, built-in, and third-party middleware. Built-ins include express.json(), express.raw(), express.text(), express.urlencoded(), and express.static(). Error middleware must keep the four-argument signature err, req, res, next.

What is the order of middleware execution in Express?

Express evaluates matching layers in registration order. A layer may end or keep the response open, call next(), skip a route or router, or enter the error path. Register parsing only where needed, authentication and authorization before protected handlers, a 404 fallback after routes, and error handlers after the layers whose errors they should receive.

How does error handling middleware work in Express?

Error middleware uses the four-argument signature (err, req, res, next). next(error), a synchronous throw, or a rejected Promise returned by Express 5 middleware enters the error path and skips ordinary handlers. An error handler should delegate when headers were already sent, normalize status safely, avoid leaking internals, and appear after the layers it covers.

What happens if you don't call next() in middleware?

If middleware neither calls next() nor completes or intentionally keeps the response open, Express does not continue to later layers. The connection remains pending until some component ends it or a client, proxy, or server timeout closes it. Return after sending a response and avoid calling next() again.

What is the difference between app.use() and app.get()?

app.use() mounts middleware for every HTTP method at a path prefix, while app.get() registers handlers for GET requests that match its route path. Express 5 route patterns follow current path-to-regexp syntax. Use req.baseUrl, req.url, and req.originalUrl deliberately in mounted middleware.


Official Sources


Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides