Express.js interviews focus on the middleware pattern and routing conventions that sit on top of Node's raw HTTP module.
Express is a minimal, unopinionated web framework built on top of Node's `http` module that gives you a much friendlier abstraction for building servers and APIs. With raw `http`, you'd manually parse the URL, figure out routing by string-matching `req.url` and `req.method`, and hand-roll things like body parsing, cookies, and static file serving. Express wraps all of that in a clean routing API (`app.get`, `app.post`, and so on), a middleware pipeline for cross-cutting concerns, and a large ecosystem of plug-and-play modules like `cors` and `helmet`. It doesn't replace Node, it just sits on top of it, so you still have full access to the underlying request and response objects. In short, it turns boilerplate you'd otherwise rewrite on every project into a few lines of configuration.
Middleware is just a function with the signature `(req, res, next)` that sits in the request-processing pipeline and can inspect or modify the `req` and `res` objects before the request reaches its final handler. Calling `next()` passes control to the next middleware or route handler in the stack; if you don't call it, and you don't send a response either, the request just hangs. You use middleware for things like logging, authentication, parsing the request body, or attaching custom data to `req` before it hits your route logic. Express also treats route handlers themselves as middleware, which is why you can chain multiple functions on a single route. The order middleware is registered in matters a lot, since Express executes it top to bottom for each matching request.
Express lets you bind handler functions to specific HTTP methods and paths using methods like `app.get`, `app.post`, `app.put`, and `app.delete`, each taking a path and one or more callback functions. Route parameters are defined with a colon in the path, like `/users/:id`, and Express automatically parses the matching URL segment into `req.params.id` as a string. You can have multiple parameters in one route, like `/users/:userId/posts/:postId`, and each becomes a key on `req.params`. This is different from query strings, which come after a `?` and land on `req.query` instead. Combined with the different HTTP verbs, this gives you a fairly expressive routing system without needing a separate router library.
`express.Router` creates a mini, self-contained instance of an Express application that has its own routing and middleware, which you can then mount onto your main app with `app.use`. It's the standard way to split routes into separate files, so instead of dumping every route into one giant server.js, you might have a `usersRouter`, a `postsRouter`, and an `authRouter`, each in their own file. You mount them with something like `app.use('/api/users', usersRouter)`, which also means the router's internal routes don't need to repeat that prefix. Routers can have their own middleware scoped just to that router, which is handy for things like requiring authentication only on certain route groups. It's really just a modularity and organization tool, it doesn't add new capabilities beyond what `app` itself has.
Error-handling middleware in Express is defined with exactly four parameters, `(err, req, res, next)`, and Express identifies it as an error handler specifically because of that arity, not because of anything you name the variables. You trigger it by calling `next(err)` from any regular middleware or route handler, or by throwing inside a synchronous handler, and Express skips all remaining normal middleware to jump straight to the nearest error handler. It should be registered last, after all your routes, so it can catch anything that bubbles up from earlier in the stack. Inside it you typically log the error and send an appropriate status code and JSON error message rather than letting Express's default HTML error page leak stack traces in production. One common gotcha is that this pattern doesn't automatically catch errors thrown inside async functions unless you wrap them in a try/catch that calls `next(err)`, or use something like `express-async-errors`, since Express's built-in handling predates native promise support.
Express ships with a built-in middleware, `express.static`, that serves files directly from a given directory without you writing any route handlers for them. You'd typically write something like `app.use(express.static('public'))`, which makes every file in the public folder accessible at the root URL, so `public/style.css` becomes available at `/style.css`. You can also mount it under a path prefix, like `app.use('/assets', express.static('public'))`, if you want the URLs to be namespaced. It handles things like setting appropriate content-type headers and supporting conditional requests and caching headers out of the box. This is mainly useful for serving a frontend build, images, or downloadable files, though for an API-only backend you often skip it entirely or use it just for uploaded file access.
`express.json()` is built-in middleware that parses incoming requests with a Content-Type of `application/json` and populates `req.body` with the parsed JavaScript object. Without it, `req.body` would be undefined for JSON requests, because Express doesn't parse the request body by default, it just gives you the raw stream. You register it globally with `app.use(express.json())`, typically near the top of your middleware stack, so it runs before any route handler that needs to read `req.body`. There are companion middlewares for other formats too, like `express.urlencoded()` for traditional HTML form submissions and `express.raw()` or `express.text()` for other content types. It used to require the third-party `body-parser` package, but that functionality was folded directly into Express as of version 4.16.
A template engine like EJS, Pug, or Handlebars lets Express render dynamic HTML on the server by injecting data into templates and sending the finished markup to the browser, which makes sense for traditional server-rendered websites. You'd set it up with `app.set('view engine', 'ejs')` and then call `res.render('template', data)` from your routes instead of `res.json()`. For an API-only backend, especially one paired with a separate frontend built in React or similar, you skip template engines entirely and just return `res.json()` responses, letting the client handle all the rendering. The choice really comes down to architecture, server-rendered apps benefit from templates for SEO and simpler initial page loads, while decoupled frontends favor a pure JSON API. It's also common to mix the two in smaller projects, serving a couple of rendered pages while exposing a JSON API for AJAX calls.
CORS, or cross-origin resource sharing, is a browser security mechanism that blocks a frontend on one origin from calling an API on a different origin unless the server explicitly allows it via response headers. In Express, the standard approach is the `cors` package, where `app.use(cors())` adds permissive headers like `Access-Control-Allow-Origin` to every response, allowing any origin by default. In practice you usually configure it more strictly, passing an options object with a specific origin, allowed methods, and allowed headers so only your actual frontend domain can call the API. Browsers also send a preflight `OPTIONS` request for non-simple requests, like ones with custom headers or JSON bodies, and the `cors` middleware automatically responds to those preflight checks correctly. You could hand-roll the headers yourself with plain middleware, but the `cors` package handles edge cases like preflight and credentialed requests more reliably.
A common pattern is an MVC-style layout, with separate folders for routes, controllers, models, and middleware, so each concern lives in its own place rather than one massive file. Route files define the URL paths and HTTP methods and delegate the actual logic to controller functions, controllers contain the request-handling logic, and models handle data access, whether that's Mongoose schemas, Sequelize models, or raw query functions. You'd also typically have a config folder for environment-specific settings, a middleware folder for custom middleware like auth checks or error handlers, and a services or utils folder for logic that doesn't belong to any single controller. The entry point, often `app.js` or `server.js`, stays thin, mainly wiring up middleware and mounting routers rather than containing business logic itself. This separation makes the codebase easier to test and navigate as it grows, since you know exactly where to look for a given piece of behavior.
`app.use()` mounts middleware that runs for every HTTP method and, by default, for every path that starts with the given prefix, while `app.get()` registers a handler that only runs for GET requests matching a specific path pattern. Express processes middleware and routes in the exact order they're registered, checking each one against the incoming request and calling it if it matches, then moving to the next only if `next()` is called. This means if you put an authentication check with `app.use()` after your routes instead of before them, it won't protect anything, because the routes will already have handled the request first. Similarly, a catch-all error handler or a 404 handler needs to be the very last thing registered, since anything after it would never be reached for unmatched routes. Getting this ordering right is one of the most common sources of subtle bugs in Express apps, especially when someone adds a new route without noticing where the security middleware sits.
`helmet` is probably the most widely used one, it's a collection of smaller middleware functions that set various HTTP headers to protect against well-known vulnerabilities, like configuring `Content-Security-Policy`, enabling `Strict-Transport-Security`, and preventing clickjacking with `X-Frame-Options`. You'd typically just add `app.use(helmet())` near the top of your middleware stack to get sensible defaults. `express-rate-limit` is another common one, it throttles how many requests a single IP can make in a given time window, which helps protect login endpoints and public APIs from brute-force attacks and abuse. Beyond those, people commonly add `cors` for origin restrictions, and packages like `express-validator` or `joi` for input validation to prevent injection-style attacks. None of these are a substitute for good application-level practices like proper authentication and parameterized database queries, but they're a solid baseline layer of defense.
Route parameters are named segments embedded directly in the URL path, defined with a colon like `/users/:id`, and Express exposes them on `req.params`, they're typically used to identify a specific resource. Query strings are the key-value pairs after the `?` in a URL, like `/search?term=express&page=2`, and Express parses them onto `req.query`, they're generally used for optional filters, sorting, or pagination rather than identifying a resource. The request body carries data sent in the body of POST, PUT, or PATCH requests, and it ends up on `req.body` once you've applied the appropriate parsing middleware like `express.json()`. A quick way to think about it is params identify what you're acting on, query tweaks how you retrieve or filter it, and body carries the actual payload you're creating or updating. Mixing these up, like trying to read `req.body` on a GET request that doesn't have one, is a common beginner mistake.
Calling `next()` with no arguments tells Express that the current middleware finished successfully and it should move on to the next matching middleware or route handler in the normal stack. Calling `next(err)` with any argument tells Express that something went wrong, and instead of continuing normally, it skips straight past all remaining regular middleware and routes to find the nearest error-handling middleware, the one with four parameters. This is the standard way to propagate errors in Express, especially in async code where you can't just throw and have Express catch it automatically. If you forget to call `next()` at all in middleware that isn't sending its own response, the request will simply hang until it times out, which is a common debugging headache. So the presence or absence of that argument to `next()` is effectively how you fork the request between the happy path and the error path.
`res.locals` is an object scoped to the lifetime of a single request that lets middleware attach data for later middleware or the final route handler to use, without polluting `req` or passing extra function arguments around. A common pattern is an authentication middleware decoding a JWT and setting `res.locals.user = decodedUser`, so any downstream handler or template can read `res.locals.user` without re-parsing the token. It's also the standard place to expose variables to a template engine like EJS or Pug, since those engines automatically have access to `res.locals` when rendering a view, letting you set things like the current year or a nonce for every page without passing them explicitly to every `res.render()` call. Unlike `req`, which conventionally represents data about the incoming request, `res.locals` is meant for response-scoped values you're building up as the request is processed. It's cleared automatically at the end of each request, so there's no risk of leaking state between unrelated requests.