Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Express.js Deep Dive - Complete Technical Guide
Express.js is a fast, unopinionated, minimalistic web framework for Node.js, designed for building web applications and APIs. It is the de facto standard backend framework in the Node ecosystem, powering millions of production services from startups to Fortune 500 companies.1. Express.js Fundamentals
Why Express.js Over Node.js?
| Aspect | Node.js (raw http module) | Express.js |
|---|---|---|
| Code Length | Lengthy, verbose (manual URL parsing, header setting) | Concise, minimal (declarative route definitions) |
| Built-in Functions | Limited to low-level req/res streams | Rich helpers: res.json(), res.sendFile(), res.redirect() |
| Routing | Manual if/else chains on req.url | Declarative routing with app.get(), app.post(), Router() |
| Middleware | Complex to implement (must manually compose) | Built-in middleware pipeline with app.use() and next() |
| Development Speed | Slower (build everything from scratch) | Faster (focus on business logic, not plumbing) |
| Ecosystem | No plugin system | 50,000+ npm middleware packages (helmet, cors, morgan, passport) |
Key Characteristics
- Fast: Optimized request handling via a radix-tree router and efficient middleware dispatch. Express 4.x handles ~15,000 req/s on a single core for simple JSON responses.
- Unopinionated: Developer has full freedom to structure code (no enforced MVC, no mandatory ORM). You choose your own folder layout, database layer, and templating engine.
- Minimalistic: The core framework is ~200KB. Built-in methods like
res.json()andres.send()reduce boilerplate, but Express deliberately ships without opinions on auth, validation, or ORM. - Extensible: The middleware pattern means any functionality (logging, auth, rate limiting, compression) plugs in as a composable function in the request pipeline.
2. Project Structure & Setup
package.json configuration
Thepackage.json file manages your project’s dependencies and metadata.
| Type | Purpose | Production (npm install --production) |
|---|---|---|
| dependencies | Required to run the app (e.g., express, mongoose, jsonwebtoken) | Installed |
| devDependencies | Only for development (e.g., nodemon, jest, eslint, supertest) | Not installed |
Recommended Production Structure
3. HTTP Methods & CRUD Operations
| Method | CRUD | Purpose | Idempotent? | Safe? | Has Body? |
|---|---|---|---|---|---|
| GET | Read | Fetch/retrieve data | Yes | Yes | No |
| POST | Create | Create new resource | No | No | Yes |
| PUT | Update | Replace entire resource | Yes | No | Yes |
| PATCH | Update | Update partial resource | Yes | No | Yes |
| DELETE | Delete | Remove resource | Yes | No | Optional |
PUT vs PATCH
- PUT: Replaces the entire document. If fields are missing in the request, they will be set to
null/default or removed entirely depending on your handler logic. Semantically it means “here is the complete new representation.” - PATCH: Updates only the specified fields, preserving the rest. Semantically it means “apply these changes to the existing resource.” In practice, most APIs use PATCH more than PUT because clients rarely want to re-send the entire object.
Idempotency Explained
Idempotent means making the same request N times produces the same result as making it once. PUT is idempotent because sendingPUT /users/123 with the same body always results in the same state. POST is not idempotent because sending POST /users twice creates two users.
4. Request-Response Cycle
The Request Object (req)
Contains all incoming client data:
req.body: Data from POST/PUT/PATCH (requiresexpress.json()middleware to parse).req.params: Dynamic segments in URL (e.g.,/users/:idmakesreq.params.idavailable).req.query: Optional filters after?(e.g.,?sort=asc&page=2givesreq.query.sortandreq.query.page).req.headers: Authentication tokens (Authorization), content-types, custom headers.req.cookies: Parsed cookies (requirescookie-parsermiddleware).req.ip: Client IP address (respectstrust proxysetting).req.method: HTTP method string (GET, POST, etc.).req.path: URL path without query string.req.hostname: Hostname from theHostheader.
The Response Object (res)
Used to send data back:
res.status(code): Set HTTP status code (chainable).res.json(data): Send JSON response (setsContent-Type: application/jsonautomatically).res.send(data): Send plain text/HTML/Buffer (auto-detects content type).res.redirect(url): Redirect client (defaults to 302, useres.redirect(301, url)for permanent).res.sendFile(path): Stream a file to the client.res.set(header, value): Set response headers.res.cookie(name, value, options): Set a cookie on the response.
Status Code Cheat Sheet
| Range | Category | Common Codes |
|---|---|---|
| 2xx | Success | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirection | 301 Moved Permanently, 304 Not Modified |
| 4xx | Client Error | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests |
| 5xx | Server Error | 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable |
5. URL Structure & Routing
- Route Parameters (
req.params): Used for required identifiers. Defined with:paramNamein the route. Example:app.get('/users/:userId/posts/:postId', handler). - Query Strings (
req.query): Used for optional filters (sorting, pagination, search). Not part of the route definition. Example:/users?role=admin&page=2. - Fragment (
#section): Never sent to the server. Handled entirely by the browser for in-page navigation.
Router Module Pattern
ExpressRouter lets you create modular, mountable route handlers:
6. Middleware Architecture
Middleware are functions that execute between the incoming request and the final response. They follow a pipeline/chain pattern where each middleware can read/modifyreq and res, then either respond or pass control downstream via next().
Types of Middleware
- Application-level: Bound to the app instance via
app.use()orapp.METHOD(). Runs for every matching request. - Router-level: Bound to
express.Router()viarouter.use(). Scoped to that router’s mount point. - Built-in:
express.json()(parses JSON bodies),express.urlencoded()(parses form data),express.static()(serves static files). - Third-party:
cors()(Cross-Origin Resource Sharing),morgan()(HTTP logging),helmet()(security headers),compression()(gzip),express-rate-limit(throttling). - Error-handling: Special middleware with 4 arguments
(err, req, res, next). Must be defined after all other middleware and routes.
Middleware Execution Order
Middleware runs in the exact order it is registered withapp.use(). This order is critical:
7. Interview Questions & Answers
1. What is middleware in Express.js and how does it actually work under the hood?
1. What is middleware in Express.js and how does it actually work under the hood?
req), the response object (res), and the next function that passes control to the next middleware in the stack.How the pipeline works internally: Express maintains an ordered array (a “stack”) of middleware layer objects. When a request comes in, Express iterates through this array sequentially. Each layer checks if its path/method matches the request. If it matches, the middleware function is invoked. The next() call increments the internal index and invokes the next matching layer.What middleware can do:- Execute any code (logging, timing, analytics).
- Mutate
reqandresobjects (attachreq.userafter auth, add custom headers). - End the request-response cycle by sending a response (
res.json(),res.send()). - Call
next()to pass control downstream, ornext(err)to jump to error-handling middleware.
next(), the request hangs indefinitely. The client eventually gets a timeout error (typically after 30-120 seconds depending on the reverse proxy). This is one of the most common Express bugs in production.Real-world middleware stack example: A typical production Express app might have 8-12 middleware layers: helmet (security headers) -> cors -> morgan (logging) -> express.json() -> express-rate-limit -> authMiddleware -> routes -> 404 handler -> errorHandler. Each request passes through all of them in order.What interviewers are really testing: Whether you understand that middleware is not just “a function that runs before your route” but a composable pipeline architecture. Senior candidates explain the stack, ordering implications, and the next() mechanism. Staff-level candidates discuss how this pattern compares to other frameworks (Koa’s async/await middleware, Hapi’s lifecycle extensions) and its limitations (no built-in backpressure, error handling in async code requires explicit next(err) or a wrapper).Red flag answer: “Middleware is a function that runs before the route handler.” This is technically not wrong but shows zero depth. It misses the pipeline concept, next() mechanics, error propagation, and ordering significance.Follow-up:- What happens if you forget to call
next()and also do not send a response? How would you detect this in production? - How would you write a middleware that measures the response time of every request and logs it? Walk me through the implementation including how you capture the finish event.
- In Express 4, async errors in middleware do not automatically propagate. How do you handle that? What changes in Express 5?
2. What is the difference between req.params, req.query, and req.body?
2. What is the difference between req.params, req.query, and req.body?
req.params - Extracted from named route segments defined with :paramName. Used for identifying a specific resource. These are always strings.- Route:
app.get('/users/:userId/posts/:postId', handler) - URL:
/users/42/posts/7 - Result:
req.params = { userId: '42', postId: '7' }(note: both are strings, not numbers) - Use case: Resource identification. The URL structure implies hierarchy (user 42’s post 7).
req.query - Extracted from the query string after ?. Used for optional filtering, sorting, pagination. Also always strings.- URL:
/users?role=admin&sort=name&page=2&limit=20 - Result:
req.query = { role: 'admin', sort: 'name', page: '2', limit: '20' } - Use case: Modifying how results are returned without changing which resource you are addressing.
req.body - Parsed from the request body (requires express.json() middleware). Used for sending data payloads in POST, PUT, PATCH requests. Can contain any JSON-serializable data including nested objects, arrays, numbers, booleans.- Sent via: POST/PUT/PATCH with
Content-Type: application/json - Result:
req.body = { name: 'Jane', email: 'jane@example.com', roles: ['admin', 'editor'] } - Use case: Creating or updating resources with complex data.
req.params and req.query values are always strings. If your route is /users/:id and someone hits /users/42, req.params.id is '42' (string), not 42 (number). You must parse/validate before using in database queries: const id = parseInt(req.params.id, 10) or use a validation library like Joi/Zod.Security consideration: Never trust any of these inputs. req.query is trivially manipulated, req.body can contain anything, and even req.params can be injected with unexpected values. Always validate and sanitize.What interviewers are really testing: Whether you understand the semantic difference (identification vs filtering vs payload), know that params/query are always strings (a common bug source), and think about validation. Senior candidates mention the security angle and type coercion issues.Red flag answer: Confusing params with query, or not knowing that req.body requires express.json() middleware to work. Another red flag is not mentioning that params and query are strings.Follow-up:- If a user sends
GET /users?admin=true, isreq.query.adminthe booleantrueor the string'true'? How would you handle this safely? - When would you use route params vs query params for filtering? For example, should “get all posts by user 42” be
GET /users/42/postsorGET /posts?userId=42? What are the trade-offs? - How do you handle arrays in query strings? What does
?tags=js&tags=nodeproduce inreq.query?
3. How do you handle errors in Express? Walk me through a production-grade error handling strategy.
3. How do you handle errors in Express? Walk me through a production-grade error handling strategy.
async route handlers. You need a wrapper:(err, req, res, next):try/catch block. They want to see: custom error classes, async error propagation, centralized handling, the operational vs programming error distinction, and not leaking stack traces to clients in production.Red flag answer: “I use try/catch in every route” without mentioning centralized error handling, or showing an error handler that sends err.stack to the client in production (security risk: leaks internal paths, library versions, code structure).Follow-up:- What is the difference between an operational error and a programming error? Give examples of each and explain why the distinction matters for your error handling strategy.
- How would you handle errors from third-party API calls (e.g., a payment gateway timing out) differently from database errors?
- Your Express app in production starts returning 500 errors at 3 AM. Walk me through your debugging process, from alert to resolution.
4. What is express.json() and why is it needed? What happens without it?
4. What is express.json() and why is it needed? What happens without it?
express.json() is a built-in middleware that parses incoming request bodies with JSON content (Content-Type: application/json). It reads the raw request stream, parses the JSON string into a JavaScript object, and attaches it to req.body.Without it: req.body is undefined. This is the number one “bug” new Express developers encounter. The request comes in with data, but the server cannot read it because no body parser is registered.How it works internally: It is based on the body-parser library (which was external in Express 3, built-in since Express 4.16). The middleware:- Checks the
Content-Typeheader. If it is notapplication/json, it skips parsing. - Reads the raw request stream into a buffer.
- Calls
JSON.parse()on the buffer. - Attaches the result to
req.body. - Calls
next().
limit matters: Without a body size limit, an attacker can send a 1GB JSON payload and exhaust your server’s memory. Setting limit: '10kb' or a reasonable value for your use case is a basic security measure. In production at scale, this is typically combined with a reverse proxy limit (e.g., Nginx client_max_body_size).express.urlencoded(): The sibling middleware for HTML form submissions (Content-Type: application/x-www-form-urlencoded). You typically need both in a full-stack app:limit option unprompted.Red flag answer: “It parses JSON” without explaining why it is needed (that req.body is undefined without it) or knowing about the limit option. Another red flag is not knowing the difference between express.json() and express.urlencoded().Follow-up:- What happens if a client sends a malformed JSON body? How does Express handle the parse error and how should you handle it?
- Why would you set a body size limit? What is the default limit and what would you set it to for a file upload endpoint vs a regular API endpoint?
- If your API needs to accept both JSON and form-encoded data, how do you set that up? What about multipart form data for file uploads?
5. Explain the execution order of middleware. What is the output of this code?
5. Explain the execution order of middleware. What is the output of this code?
GET / request, the output is: A, B, C.Detailed walkthrough:- Request enters the middleware stack.
- First
app.use()matches all routes. LogsA, callsnext(). - Second
app.use()matches all routes. LogsB, callsnext(). app.get('/')matchesGET /specifically. LogsC, sends response withres.send('Done').- The last
app.use()(the 404 handler withD) is never reached because the route handler already sent a response. Onceres.send()is called, the response is finished.
GET /nonexistent request, the output would be: A, B, D.
The route handler app.get('/') does not match /nonexistent, so Express skips it and falls through to the catch-all 404 middleware.Key principles:- Middleware executes in registration order (the order you call
app.use()matters enormously). - Once a response is sent (
res.send(),res.json(), etc.), no further middleware runs for that request. - Route handlers are just middleware that also match on HTTP method and path.
- 404 handlers work by being the last middleware registered. If no route matched, control falls through to them.
D would also execute, or not being able to explain why changing the order of app.use() calls changes behavior.Follow-up:- What would happen if the route handler called
next()instead ofres.send('Done')? WouldDexecute? - How does Express decide which middleware to run for
POST /vsGET /? Walk through the matching algorithm. - In a large app with 50+ routes and 10+ middleware, how would you debug which middleware is running and in what order? What tools would you use?
6. What is the Express Router and why should you use it?
6. What is the Express Router and why should you use it?
express.Router() creates a modular, mountable set of route handlers. It is essentially a “mini Express application” that only handles routing and middleware, without the full app’s settings and lifecycle.Why it matters: In any non-trivial application, putting all routes in a single app.js file becomes unmanageable. Router lets you split routes by resource/domain:- Separation of concerns: Each resource has its own file with its own middleware.
- Scoped middleware:
router.use(auth)only applies to that router’s routes, not the entire app. - Composability: Routers can be nested. A
v2router can mount ausersrouter at/v2/users. - Testability: You can test router modules in isolation with
supertestwithout booting the full app.
app.use(cors()) applies to every request. router.use(authenticate) only applies to routes mounted on that router. This is how you make some routes public and others authenticated without complex conditional logic.router.param() for shared parameter logic:- How would you implement API versioning (v1, v2) using Express routers?
- How do you share common middleware (like auth) across some routers but not others?
- What is
router.route()and when would you use it instead of separaterouter.get(),router.post()calls?
7. How does Express handle static files? What is express.static()?
7. How does Express handle static files? What is express.static()?
express.static() is built-in middleware that serves files (HTML, CSS, JS, images) directly from a directory on disk without you writing route handlers for each file.express.static checks if a file matching the URL path exists in the specified directory. If found, it streams the file to the client with appropriate Content-Type headers (determined by file extension). If not found, it calls next() and the request continues down the middleware stack.Production considerations:- Virtual path prefix:
app.use('/static', express.static('public'))serves files at/static/style.cssinstead of/style.css. Useful for cache-busting and CDN configuration. - Cache headers:
app.use(express.static('public', { maxAge: '1y' }))setsCache-Controlheaders. Critical for performance. Serve assets with content hashes in filenames and long cache TTLs. - Security: Never serve your project root as static. Only serve a dedicated
public/directory. Otherwise you risk exposingpackage.json,.env, source code. - In production, use a reverse proxy: Nginx or a CDN (CloudFront, Cloudflare) should serve static files, not Express. Express handles ~2,000 static file requests/sec, Nginx handles ~50,000+. Use Express static only for development or as a fallback.
- Why would you use Nginx or a CDN in front of Express for static files instead of letting Express handle them?
- How would you set up cache-busting for static assets in an Express app?
- What happens if both
express.static()and a route handler match the same path? Which one wins?
8. What is CORS and how do you handle it in Express?
8. What is CORS and how do you handle it in Express?
http://localhost:3000 calls your Express API at http://localhost:5000, the browser blocks it unless the server explicitly allows it via CORS headers.How it works under the hood: For “simple” requests (GET, POST with certain content types), the browser sends the request and checks the Access-Control-Allow-Origin response header. For “complex” requests (PUT, DELETE, custom headers, JSON content type), the browser first sends a preflight OPTIONS request to ask the server “are you okay with this?”. The server responds with allowed methods, headers, and origins. Only then does the browser send the actual request.Implementation in Express:credentials: true gotcha: When credentials: true is set, origin cannot be '*'. You must specify exact origins. This trips up many developers when they add authentication cookies to an existing API that was using cors() with no options.Performance note: Every complex cross-origin request triggers a preflight OPTIONS request before the actual request. That is two HTTP round trips instead of one. Setting maxAge caches the preflight response so subsequent requests from the same origin skip the preflight.What interviewers are really testing: Whether you understand that CORS is a browser-enforced policy (server-to-server calls are unaffected), know about preflight requests, and can configure it securely for production (not just cors() with no arguments).Red flag answer: “I just add cors() and it works” without understanding what CORS actually is, or not knowing about preflight requests. Another red flag is not knowing that CORS does not apply to server-to-server communication.Follow-up:- If your API works fine from Postman but fails from the browser with a CORS error, why? What does that tell you about where CORS is enforced?
- What is a preflight request? What triggers it? How would you minimize the performance impact of preflight requests?
- How would you configure CORS differently for a public API vs an internal API that only your frontend should access?
9. How would you implement authentication middleware in Express?
9. How would you implement authentication middleware in Express?
- Authentication (401): “Who are you?” Verifying identity.
- Authorization (403): “Are you allowed to do this?” Checking permissions.
- 401 vs 403: 401 means “I do not know who you are” (missing/invalid token). 403 means “I know who you are, but you are not allowed” (valid token, wrong role).
- Store JWT secrets in environment variables, never in code.
- Use short-lived access tokens (15 min) with refresh tokens for better security.
- Consider token blacklisting for logout (Redis-backed set of revoked tokens).
- Rate-limit login endpoints to prevent brute-force attacks.
- How would you implement a refresh token flow? Where do you store refresh tokens and why?
- What are the security trade-offs of storing JWTs in localStorage vs httpOnly cookies?
- How would you handle authentication for WebSocket connections in an Express app?
10. How do you structure a large Express application? What patterns do you follow?
10. How do you structure a large Express application? What patterns do you follow?
- Testability: You can unit test services without spinning up Express or a database.
- Reusability: Services can be called from REST routes, GraphQL resolvers, CLI scripts, or cron jobs.
- Team scalability: Different developers can work on different layers without conflicts.
- Migration: If you switch from Express to Fastify, only the routes/controllers layer changes. Services and models are untouched.
config/ directory or .env files) and inject dependencies rather than importing them directly. This makes testing and environment switching cleaner.What interviewers are really testing: Whether you have built and maintained a real Express application at scale, or only written toy apps. The layered architecture is the standard answer, but senior candidates explain the “why” (testability, team scaling, framework migration) and acknowledge alternatives (hexagonal architecture, functional composition).Red flag answer: Describing an app where route handlers contain database queries, business logic, and response formatting all in one function. Or not knowing what a “service layer” is.Follow-up:- How do you handle cross-cutting concerns like logging, error handling, and request ID tracking across all layers?
- What is the difference between the controller layer and the service layer? Why not combine them?
- How would you refactor a monolithic Express app (everything in one file) into this layered structure without breaking existing functionality?
11. How do you validate request data in Express? Compare different approaches.
11. How do you validate request data in Express? Compare different approaches.
| Library | TypeScript | Bundle Size | Schema Reuse | Learning Curve |
|---|---|---|---|---|
| Joi | Via @types | ~150KB | Excellent | Medium |
| Zod | Native | ~50KB | Excellent | Low |
| express-validator | Via @types | ~30KB | Limited | Low |
stripUnknown / abortEarly options. Senior candidates discuss the security implications of not validating (injection, type confusion, prototype pollution via __proto__ in JSON).Red flag answer: “I validate in the route handler with if-statements” or worse, “I trust the frontend to send correct data.”Follow-up:- How do you validate different parts of the request (body, params, query) with the same schema library?
- What is prototype pollution and how can input validation prevent it?
- How do you handle validation error messages for internationalized (i18n) APIs?
12. What is `app.listen()` vs creating an HTTP server with `http.createServer()`?
12. What is `app.listen()` vs creating an HTTP server with `http.createServer()`?
app.listen(port) is a convenience method that internally calls http.createServer(app).listen(port). It is sufficient for most use cases:http.createServer(app) gives you a reference to the underlying HTTP server object, which you need when:- WebSockets (Socket.io, ws): Must attach to the HTTP server, not the Express app.
- Graceful shutdown:
server.close()stops accepting new connections while finishing in-flight requests. - HTTPS:
https.createServer({ key, cert }, app)for TLS termination at the app level. - Server-Sent Events (SSE): Need access to the underlying connection.
http.createServer() and store the server reference. Even if you do not need WebSockets today, you will need graceful shutdown, and refactoring app.listen() to http.createServer() later is an unnecessary change.What interviewers are really testing: Whether you understand that Express is just a request handler function, not a server itself. The server is Node’s http.Server. Express plugs into it. This distinction matters for WebSockets, graceful shutdown, and HTTPS.Red flag answer: Not knowing the difference, or saying app.listen() is the only way to start Express.Follow-up:- How would you implement graceful shutdown in a production Express app? What happens to in-flight requests?
- Why would you use HTTPS at the Express level vs terminating TLS at a reverse proxy like Nginx?
- How do you share the same port between an Express REST API and a WebSocket server?
13. How do you implement rate limiting in Express and why does it matter?
13. How do you implement rate limiting in Express and why does it matter?
express-rate-limit:express-rate-limit uses in-memory storage. This breaks in multi-instance deployments (each instance has its own counter). Solutions:- Redis store (
rate-limit-redis): Shared counter across all instances. The standard production choice. - Memcached store: Alternative to Redis.
- Database-backed: Possible but slow; not recommended for rate limiting.
X-RateLimit-Limit (max requests), X-RateLimit-Remaining (requests left), X-RateLimit-Reset (when the window resets). Good API design returns these so clients can self-throttle.Production layers: Rate limiting should happen at multiple levels:- CDN/Load Balancer (Cloudflare, AWS WAF): First line of defense, blocks obvious abuse before requests hit your servers.
- Reverse Proxy (Nginx
limit_req): Second layer, protects the application servers. - Application (express-rate-limit): Fine-grained, per-endpoint limits with business logic awareness.
trust proxy setting: If Express is behind a reverse proxy (Nginx, load balancer), req.ip returns the proxy’s IP, not the client’s. All clients appear as the same IP and share a rate limit. Fix: app.set('trust proxy', 1) to use the X-Forwarded-For header.What interviewers are really testing: Whether you think about API protection as a production concern, understand the in-memory vs distributed storage problem for multi-instance deployments, and know about the trust proxy gotcha.Red flag answer: Not knowing what rate limiting is, or implementing it but not considering multi-instance deployments where in-memory storage fails.Follow-up:- Your API is behind a load balancer with 4 Express instances. Each has its own in-memory rate limiter. What happens and how do you fix it?
- How would you implement different rate limits for free-tier vs paid-tier API users?
- What is the
trust proxysetting and why does forgetting it completely break IP-based rate limiting behind a reverse proxy?
14. What are the security best practices for a production Express application?
14. What are the security best practices for a production Express application?
X-Content-Type-Options: nosniff, X-Frame-Options, Strict-Transport-Security, and Content-Security-Policy. A single line that blocks entire classes of attacks (clickjacking, MIME-type sniffing, XSS via injected scripts).2. Input validation and sanitization: Validate every input with Joi/Zod. Use express-mongo-sanitize to prevent NoSQL injection ({ "$gt": "" } in query params). Use xss-clean or DOMPurify for user-generated content.3. Rate limiting: As discussed, use express-rate-limit with Redis backing. Apply strict limits on auth endpoints.4. Body size limits: express.json({ limit: '10kb' }). Prevents memory exhaustion from oversized payloads.5. CORS configuration: Whitelist specific origins in production. Never use cors() with no arguments in production.6. HTTPS only: Redirect HTTP to HTTPS. Set Strict-Transport-Security header (via Helmet). Use secure flag on cookies.7. Environment variables: Never hardcode secrets, API keys, or database credentials. Use .env files locally (with dotenv) and secret managers (AWS Secrets Manager, HashiCorp Vault) in production. Add .env to .gitignore.8. Dependency auditing: Run npm audit regularly. Use tools like Snyk or Dependabot for automated vulnerability scanning. Outdated dependencies are the number one attack vector for Node.js apps.9. Disable X-Powered-By header: Express sends X-Powered-By: Express by default, advertising your stack. Disable it: app.disable('x-powered-by') (or Helmet does this automatically).10. SQL/NoSQL injection prevention: Use parameterized queries (never string concatenation). With Mongoose, use express-mongo-sanitize to strip $ operators from user input.What interviewers are really testing: Whether security is an afterthought or baked into your development process. Senior candidates rattle off at least 5-6 of these without prompting. Staff-level candidates discuss threat modeling and which attacks each measure prevents.Red flag answer: Only mentioning HTTPS and passwords. Not knowing about Helmet, CORS configuration, or body size limits. Saying “security is the DevOps team’s job.”Follow-up:- What specific attacks does Helmet prevent? Can you name the headers it sets and what each one does?
- How would you prevent NoSQL injection in a Mongoose/MongoDB Express app? Show me an example of the attack and the defense.
- Walk me through how you would handle a security audit finding that your Express API is vulnerable to CSRF attacks.
15. Compare Express.js to other Node.js frameworks. When would you not choose Express?
15. Compare Express.js to other Node.js frameworks. When would you not choose Express?
- Fastify is 2-3x faster than Express in benchmarks (~30K req/s vs ~15K req/s for JSON responses). It uses a radix-tree router and schema-based serialization.
- Fastify has built-in schema validation (via JSON Schema) and automatic serialization. Express requires third-party middleware.
- Fastify has a plugin system with encapsulation (plugins cannot accidentally leak into other scopes). Express middleware is global by default.
- Choose Fastify when: Raw performance matters (high-throughput APIs, microservices), you want built-in validation, or you are starting a new TypeScript project (Fastify has excellent TS support).
- Choose Express when: You need the massive middleware ecosystem, your team already knows Express, or you are maintaining an existing Express codebase.
- Koa was created by the same team as Express as a “do-over.” It uses
async/awaitnatively (no callback-style middleware). - Koa has no built-in routing or body parsing. It is even more minimal than Express.
- Koa’s middleware uses an “onion” model (downstream then upstream), which makes response manipulation elegant.
- Choose Koa when: You want a cleaner async/await middleware model and are comfortable assembling your own stack.
- NestJS is a framework built ON TOP of Express (or Fastify). It adds TypeScript, dependency injection, decorators, modules, and an opinionated architecture inspired by Angular.
- NestJS is a full framework; Express is a micro-framework. NestJS tells you how to structure your app; Express does not.
- Choose NestJS when: You are building a large enterprise application with a team, want enforced architecture patterns, and use TypeScript.
- Hapi has built-in authentication, validation, caching, and input/output validation. Much more batteries-included than Express.
- Hapi has a configuration-centric approach rather than middleware composition.
- Smaller ecosystem and community than Express.
- Performance-critical microservices: Choose Fastify.
- Large enterprise TypeScript projects: Choose NestJS.
- Serverless functions: Express adds overhead. Consider lightweight alternatives or framework-agnostic handlers.
- Real-time only apps: If your app is primarily WebSocket-based, frameworks like Socket.io standalone or uWebSockets.js are more appropriate.
- If you were starting a new greenfield API project today in TypeScript, would you choose Express? Why or why not?
- You have an Express API handling 50,000 requests/second and need to reduce latency. Would migrating to Fastify help? What would you benchmark first?
- How would you migrate an Express app to Fastify incrementally without rewriting everything at once?
16. How do you test an Express application? Walk me through your testing strategy.
16. How do you test an Express application? Walk me through your testing strategy.
supertest to make HTTP requests against your Express app without starting a real server:- Separate
app.jsfromserver.js: Export the Express app fromapp.jswithout callinglisten().server.jsimports the app and starts listening. This letssupertestimport the app without starting a real server. - Test database: Use a separate database (e.g.,
myapp_test) that gets seeded before tests and cleaned after. Or use an in-memory MongoDB (mongodb-memory-server) for speed. - Auth in tests: Create a helper that generates valid JWT tokens for test users to avoid repeating auth setup in every test.
supertest, the app/server separation pattern, and a multi-layer testing strategy (not just “I write unit tests”).Red flag answer: “I test by running the server and using Postman” or not knowing about supertest.Follow-up:- How do you handle database state between tests? How do you ensure tests do not interfere with each other?
- How do you test error handling middleware? How do you verify that a 500 error does not leak internal details?
- What is your approach to testing authenticated routes? Do you hit a real auth service or mock it?
17. What is the difference between `app.use()` and `app.get()`/`app.post()`?
17. What is the difference between `app.use()` and `app.get()`/`app.post()`?
app.use(path, handler):- Matches all HTTP methods (GET, POST, PUT, DELETE, etc.).
- Matches the path as a prefix.
app.use('/api', handler)matches/api,/api/users,/api/users/123, etc. - Used for middleware that should run regardless of the HTTP method (logging, auth, body parsing).
- Path defaults to
'/'(matches everything) if omitted.
app.get(path, handler) / app.post(path, handler):- Matches only the specific HTTP method.
- Matches the path exactly (no prefix matching).
app.get('/api')matches onlyGET /api, notGET /api/users. - Used for route handlers that respond to specific endpoints.
app.use() is crucial to understand. It is how you scope middleware to groups of routes. app.use('/admin', adminAuth) protects all /admin/* routes without listing each one.What interviewers are really testing: Whether you understand the fundamental distinction between middleware registration (app.use) and route registration (app.get/post/put/delete). The prefix-matching behavior of app.use is a frequent source of bugs when developers expect exact matching.Red flag answer: Saying they are “basically the same thing” or not knowing about prefix matching.Follow-up:- If you register
app.use('/users', handler)and a request comes in forGET /users/123/posts, does the handler run? What isreq.pathinside that handler? - Why does
app.use()match all methods? When is this useful vs when is it dangerous?
18. How do you handle file uploads in Express?
18. How do you handle file uploads in Express?
express.json() and express.urlencoded() only parse text-based bodies. File uploads use multipart/form-data encoding, which requires dedicated middleware.Multer is the standard library:- Never store files on the Express server’s disk in production. Containers are ephemeral, horizontal scaling means files are on one instance but not others. Upload to S3, GCS, or Azure Blob Storage.
- Stream directly to cloud storage: Use
multer-s3to stream uploads directly to S3 without buffering the entire file in memory. - Validate file types seriously: Do not trust
file.mimetypealone (it comes from the client). For security-critical apps, check magic bytes (file signatures) using a library likefile-type. - Virus scanning: For user-uploaded files, pipe through a virus scanner (ClamAV) before storing.
express.json() for file uploads, or storing files on local disk in production.Follow-up:- Why should you not store uploaded files on the Express server’s local disk in production? What breaks?
- How would you implement progress tracking for large file uploads?
- How do you prevent users from uploading malicious files disguised with a
.jpgextension?
19. What happens when you call `res.json()` or `res.send()` twice in the same handler?
19. What happens when you call `res.json()` or `res.send()` twice in the same handler?
Error: Cannot set headers after they are sent to the client (often abbreviated as “ERR_HTTP_HEADERS_SENT”). This crashes the route handler.Why it happens: When you call res.json() or res.send(), Express:- Sets the response status code and headers.
- Writes the response body to the underlying TCP socket.
- Ends the response stream.
res.headersSent checks in your error middleware:return pattern to prevent it. This is a bug that every Express developer hits at some point.Red flag answer: Never having seen this error, or not knowing that the fix is to return after sending a response.Follow-up:- How would you write a linting rule or middleware to catch “double response” bugs during development?
- What does
res.headersSenttell you, and where would you check it? - In a complex handler with multiple async operations, how do you ensure only one response path executes?