Episode 3 — NodeJS MongoDB Backend Architecture / 3.4 — Express JS
3.4 — Interview Questions: Express.js
Common interview questions about Express.js fundamentals, routing, middleware, HTTP methods, request/response handling, and serving static files.
< Exercise Questions | Quick Revision >
How to use this material (instructions)
- Read lessons first --
README.md, then3.4.athrough3.4.f. - Answer aloud before reading the model answer -- this simulates interview pressure.
- Pair with
3.4-Exercise-Questions.mdfor hands-on practice. - Quick review --
3.4-Quick-Revision.mdfor last-minute revision.
Beginner Level
Q1: What is Express.js and why would you use it instead of the raw Node.js http module?
Why interviewers ask: Verifies you understand what Express actually does and that it is not a replacement for Node.
Model answer:
Express.js is a minimal, unopinionated web framework for Node.js that provides a clean API on top of the built-in http module. It does not replace http -- it wraps it. You would use Express because the raw http module requires manual routing (if/else chains on req.url and req.method), manual body parsing (reading streams and calling JSON.parse), and manual header management. Express gives you declarative routing (app.get(), app.post()), a middleware pipeline (app.use()), built-in body parsing (express.json()), response helpers (res.json(), res.status()), and static file serving (express.static()). It is the de facto standard for Node.js web servers, with the largest middleware ecosystem.
Q2: What does const app = express() return?
Why interviewers ask: Tests whether you know what you are actually working with -- many beginners treat app as a black box.
Model answer:
express() returns an Express application object, which is a JavaScript function designed to be passed as a callback to Node's http.createServer(). The app object has methods for routing (app.get, app.post, app.put, app.delete, app.use), configuration (app.set, app.enable), and listening (app.listen). Under the hood, app.listen(3000) is shorthand for http.createServer(app).listen(3000). The app is also an EventEmitter, so it can emit and listen for events.
Q3: What is middleware in Express? Write the function signature.
Why interviewers ask: Middleware is the core concept of Express -- if you do not understand it, you do not understand Express.
Model answer:
Middleware is any function that has access to the request object (req), the response object (res), and the next function (next) in the application's request-response cycle. Middleware can modify req or res, end the cycle by sending a response, or call next() to pass control to the next middleware.
// Standard middleware signature
(req, res, next) => {
// do something
next(); // or send a response
}
// Error-handling middleware has 4 parameters
(err, req, res, next) => {
// handle the error
}
Middleware is registered with app.use() and executes in the order it is registered. This sequential pipeline is the backbone of Express.
Q4: What is the difference between res.send() and res.json()?
Why interviewers ask: Tests awareness of subtle but important differences in response handling.
Model answer:
Both send a response body to the client, but they differ in behavior:
-
res.send(body)is a general-purpose method. It auto-detects the content type: strings gettext/html, objects and arrays getapplication/json, Buffers getapplication/octet-stream. It also setsContent-Lengthand handlesHEADand304responses. -
res.json(body)explicitly serializes the body withJSON.stringify()and setsContent-Type: application/json. It also appliesjson replacerandjson spacessettings configured withapp.set().
For APIs, always use res.json() because it makes intent explicit, ensures consistent JSON formatting, and respects app-level JSON settings. res.send({ x: 1 }) happens to produce the same result, but res.json() is semantically correct.
Q5: Why do we separate app.js and server.js?
Why interviewers ask: Tests understanding of best practices and testability.
Model answer:
Separating Express configuration (app.js) from the server startup (server.js) is a best practice for testability and separation of concerns. app.js creates the Express app, registers middleware, mounts routes, and exports the app object. server.js imports app, connects to the database, and calls app.listen().
This separation lets testing frameworks (like Supertest) import the app object and make HTTP requests against it without starting a real server or binding to a port. It also keeps configuration clean -- app.js is about Express, server.js is about infrastructure (port, database connection, unhandled rejection handlers).
// app.js
const express = require('express');
const app = express();
app.use(express.json());
// ... routes
module.exports = app;
// server.js
const app = require('./app');
app.listen(process.env.PORT || 3000);
Intermediate Level
Q6: Explain req.params vs req.query. When do you use each?
Why interviewers ask: Mixing these up is a common source of bugs -- interviewers want to confirm you understand URL structure.
Model answer:
req.params contains values extracted from named route segments. For the route /users/:id, a request to /users/42 produces req.params = { id: '42' }. Use params for resource identifiers -- the thing you are looking up.
req.query contains key-value pairs from the query string. A request to /users?role=admin&page=2 produces req.query = { role: 'admin', page: '2' }. Use query for optional filters, sorting, pagination, and search terms.
Both always return strings, even for numeric values. You must parse them: parseInt(req.params.id) or Number(req.query.page).
// Route: GET /api/products/:category?sort=price&order=asc
app.get('/api/products/:category', (req, res) => {
const category = req.params.category; // "electronics"
const sort = req.query.sort; // "price"
const order = req.query.order; // "asc"
});
Rule of thumb: If removing the value breaks the URL meaning, it is a param. If it refines the result, it is a query.
Q7: What are the main HTTP methods and how do they map to CRUD operations?
Why interviewers ask: Fundamental REST knowledge required for any backend role.
Model answer:
| Method | CRUD | Purpose | Idempotent? | Has Body? |
|---|---|---|---|---|
GET | Read | Retrieve a resource or collection | Yes | No |
POST | Create | Create a new resource | No | Yes |
PUT | Update (full) | Replace an entire resource | Yes | Yes |
PATCH | Update (partial) | Modify specific fields of a resource | Yes | Yes |
DELETE | Delete | Remove a resource | Yes | Optional |
Idempotent means making the same request multiple times produces the same result. POST is not idempotent because calling it twice creates two resources. PUT is idempotent because replacing a resource with the same data twice yields the same state. In Express: app.get(), app.post(), app.put(), app.patch(), app.delete().
Q8: What middleware do you need to parse JSON request bodies? What happens without it?
Why interviewers ask: Tests practical knowledge -- req.body being undefined is one of the most common Express debugging issues.
Model answer:
You need express.json() registered before any routes that read req.body:
app.use(express.json());
Without it, req.body is undefined for JSON payloads. Express does not parse request bodies by default -- the raw http module delivers the body as a stream, and express.json() reads that stream, runs JSON.parse(), and attaches the result to req.body.
For URL-encoded form data (HTML forms), you also need express.urlencoded({ extended: true }). The extended: true option uses the qs library which supports nested objects; extended: false uses the built-in querystring module which only supports flat key-value pairs.
app.use(express.json()); // JSON bodies
app.use(express.urlencoded({ extended: true })); // Form bodies
Q9: How does routing work in Express? What is express.Router()?
Why interviewers ask: Tests ability to organize a growing codebase -- a single file with 50 routes is a red flag.
Model answer:
Express matches incoming requests to handlers based on HTTP method and URL path, in the order they are registered. When a match is found, the handler executes. If no match is found, Express falls through to the next middleware or returns its default 404.
express.Router() creates a mini-application (a modular, mountable route handler). You define routes on the router, then mount it at a prefix:
// routes/userRoutes.js
const router = require('express').Router();
router.get('/', getAllUsers); // GET /api/users
router.get('/:id', getUser); // GET /api/users/42
router.post('/', createUser); // POST /api/users
module.exports = router;
// app.js
app.use('/api/users', require('./routes/userRoutes'));
Routers support their own middleware (router.use()), which only applies to routes defined on that router. This enables per-resource middleware like admin-only checks.
Q10: What is the difference between app.use() and app.all()?
Why interviewers ask: Tests nuanced understanding of Express internals -- most candidates mix these up.
Model answer:
Both accept all HTTP methods, but they differ in path matching:
-
app.use('/api', handler)uses prefix matching. It matches/api,/api/users,/api/users/42, and any path that starts with/api. -
app.all('/api', handler)uses exact matching (likeapp.get,app.post). It matches only/apiand/api/(with trailing slash).
Additionally, app.use() is for middleware -- it modifies req.path by stripping the matched prefix before passing to the next handler. app.all() is for route handlers -- it does not modify req.path.
app.use('/api', logger); // runs for /api, /api/users, /api/anything
app.all('/api', handler); // runs ONLY for /api
Advanced Level
Q11: Explain the full lifecycle of an HTTP request through an Express application.
Why interviewers ask: Senior-level question that tests understanding of the entire Express pipeline, not just individual pieces.
Model answer:
- TCP connection: Client establishes a TCP connection to the server port.
- Node
httpmodule: Parses the raw HTTP request intoreq(IncomingMessage) andres(ServerResponse) objects, then calls the Express app function. - Express initialization: Express enhances
reqandreswith additional properties and methods (req.query,res.json(), etc.). - Middleware pipeline: Express walks through the middleware stack in registration order:
- Application-level middleware (
app.use()): body parsers, loggers, CORS, auth. - Router-level middleware (
router.use()): per-resource middleware. - Route handlers: matched by method + path.
- Application-level middleware (
- Response sent: A middleware or route handler calls
res.send(),res.json(), or similar. Express serializes the body, sets headers, and writes to the socket. - If no match: If no middleware sends a response and no route matches, Express reaches the end of the stack. If you have a 404 catch-all, it handles it; otherwise the request hangs.
- Error path: If any middleware calls
next(error)or throws, Express skips all normal middleware and jumps to the first error-handling middleware (4-parameter function).
Q12: How do you handle errors in Express? What is the error-handling middleware pattern?
Why interviewers ask: Error handling separates production-quality code from tutorial code -- interviewers want to see you do it properly.
Model answer:
Express uses a centralized error handler pattern. The error-handling middleware has four parameters -- (err, req, res, next). Express identifies it as an error handler specifically because of the four-parameter signature.
// In route or middleware -- forward errors to the error handler
app.get('/api/users/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
const error = new Error('User not found');
error.statusCode = 404;
return next(error); // skip to error handler
}
res.json(user);
} catch (err) {
next(err); // pass unexpected errors to error handler
}
});
// Global error handler -- MUST be registered LAST
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.message || 'Internal Server Error';
res.status(statusCode).json({
status: 'error',
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});
Key rules: (1) Always call next(error) to forward errors, never send ad-hoc error responses in every route. (2) The error handler must have exactly 4 parameters or Express treats it as regular middleware. (3) Register it after all routes and other middleware.
Q13: How does Express serve static files? What security considerations exist?
Why interviewers ask: Tests practical deployment knowledge and security awareness.
Model answer:
Express serves static files with the built-in express.static() middleware:
app.use(express.static(path.join(__dirname, 'public')));
A file at public/css/style.css becomes accessible at /css/style.css -- the public/ prefix is stripped. You can add a virtual prefix: app.use('/assets', express.static('public')) makes the same file available at /assets/css/style.css.
Security considerations:
- Use
path.join(__dirname, 'public')with an absolute path -- relative paths resolve fromprocess.cwd(), which varies by deployment. - Set
dotfiles: 'deny'(the default) to prevent access to hidden files like.envor.gitignore. - Set
Cache-Controlheaders with themaxAgeoption for production performance. - Place
express.static()before route handlers so static file requests are served without hitting your application logic. - Never put sensitive files (
.env, database backups) in the public directory.
app.use(express.static(path.join(__dirname, 'public'), {
maxAge: '1d', // cache for 1 day
dotfiles: 'deny', // reject .env, .gitignore, etc.
index: false // disable directory index
}));
Q14: What happens if you call res.send() or res.json() twice in the same handler?
Why interviewers ask: Tests debugging experience -- the "headers already sent" error is one of the most common Express issues.
Model answer:
You get the error: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client. Once a response is sent, the HTTP response stream is finalized. Any subsequent attempt to set headers or write a body throws this error.
The most common cause is a missing return statement:
// BUG -- missing return
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
res.status(404).json({ error: 'Not found' });
// execution continues! next line also sends a response
}
res.json(user); // CRASH: headers already sent
});
// FIX -- add return
if (!user) {
return res.status(404).json({ error: 'Not found' });
}
res.json(user);
Other causes: calling res.send() inside a loop, or using callbacks that fire after a response has already been sent.
Q15: Compare Express with other Node.js frameworks. When would you choose an alternative?
Why interviewers ask: Tests breadth of knowledge and ability to evaluate trade-offs -- senior candidates should know the ecosystem.
Model answer:
| Framework | Key Trait | When to Choose |
|---|---|---|
| Express | Minimal, unopinionated, largest ecosystem | Default choice; simple APIs, most projects |
| Fastify | Performance-focused, schema-based validation, 2-3x faster | High-throughput APIs where latency matters |
| NestJS | Opinionated, TypeScript-first, Angular-inspired (modules, DI, decorators) | Large teams, enterprise apps, TypeScript projects needing structure |
| Koa | Modern, async/await-native, no built-in routing | When you want Express-like minimalism with cleaner async patterns |
| Hapi | Configuration-driven, built-in validation and auth | Enterprise projects valuing convention over code |
Choose Express when: you need the largest ecosystem, you want maximum flexibility, or the team is already familiar with it. Choose Fastify when: raw performance matters and you want built-in schema validation. Choose NestJS when: you have a large TypeScript team that benefits from enforced architectural patterns. Express remains the most popular choice because its simplicity and ecosystem are hard to beat for most projects.
Quick-Fire Table
| # | Question | One-Line Answer |
|---|---|---|
| 1 | What is Express? | Minimal web framework for Node.js that wraps the http module |
| 2 | express() returns | An app object (function) that can be passed to http.createServer() |
| 3 | Middleware signature | (req, res, next) => {} |
| 4 | Error middleware signature | (err, req, res, next) => {} -- exactly 4 params |
| 5 | res.send() vs res.json() | send auto-detects type; json always sets application/json |
| 6 | Parse JSON bodies | app.use(express.json()) before routes |
| 7 | req.params | Route segment values: /users/:id -> { id: '42' } |
| 8 | req.query | Query string values: ?page=2 -> { page: '2' } |
| 9 | Values are always | Strings -- cast with Number() or parseInt() |
| 10 | app.use() vs app.all() | use = prefix match; all = exact match |
| 11 | Serve static files | app.use(express.static('public')) |
| 12 | 404 catch-all placement | After all routes, before error handler |
| 13 | Double res.send() | ERR_HTTP_HEADERS_SENT -- fix with return |
| 14 | Separate app.js / server.js | Testability -- import app without starting server |
| 15 | Express is unopinionated | No enforced structure; you choose your own architecture |
<- Back to 3.4 -- Express.js (README)