Chapter 99 โ Node & Express Scenarios
๐ Definition
Backend scenario questions describe a production-style problem in a Node/Express API โ latency, crashes, duplicates, security holes, scaling limits โ and ask how you would approach it. Answer with clarify โ hypothesise โ verify โ fix โ prevent, and name the tools you would use.
Scenario 1 โ "One endpoint takes 5 seconds; everything else is fine."
Clarify: always slow or only for some inputs/users? since a deploy? p50 or only p99?
Verify
- Add timing logs/spans around each step (DB query, external call, serialisation), or check APM traces.
- Run the MongoDB query with
.explain("executionStats")โ look forCOLLSCAN,totalDocsExaminedโซnReturned. - Check for N+1 queries (many small queries in a loop) in the MongoDB profiler or logs.
Common fixes
- Add the right compound index (Equality โ Sort โ Range order).
- Replace per-item queries with
$in/populate/$lookup. .lean()+ projection; paginate.- Run independent calls with
Promise.all. - Cache stable results in Redis; move slow side work (emails, PDFs) to a queue.
Prevent: p95 latency alerts per route; slow-query logging in MongoDB.
Scenario 2 โ "When one heavy request runs, every other request becomes slow."
Cause: the event loop is blocked by synchronous CPU work (big JSON.parse/stringify, sorting/aggregating large arrays in JS, bcrypt.hashSync, image processing, a catastrophic regex).
Verify: event-loop delay metric (monitorEventLoopDelay), clinic doctor, CPU profile (--cpu-prof).
Fix: push aggregation into MongoDB; use async versions of crypto/zlib; move CPU work to worker_threads (pool with Piscina) or a separate service via a queue; chunk work with setImmediate; validate input length to prevent ReDoS.
Scenario 3 โ "The API works locally but returns 500 in production."
Checklist
- Read the production logs for the stack trace (structured logs + request ID).
- Environment variables missing or different (
MONGO_URI, secrets,NODE_ENV). - Network/permissions: DB IP allow-list, VPC/security groups, IAM role for S3.
- Differences in data: a null field that never existed in dev data.
- Versions: Node version, a dependency installed without lockfile (
npm civsnpm install). - Case-sensitive file paths (macOS/Windows dev vs Linux server):
import "./Utils"vsutils.js. - Proxy issues:
app.set("trust proxy", 1)needed for correct IPs/HTTPS behind a load balancer; cookies withsecureover HTTP.
Prevent: validate env vars at startup and crash early (e.g. with Zod), keep a staging environment identical to prod, Docker for parity.
const env = z.object({
MONGO_URI: z.string().url(),
JWT_SECRET: z.string().min(32),
PORT: z.coerce.number().default(3000),
}).parse(process.env); // throws at boot with a clear messageScenario 4 โ "Duplicate orders are created when users double-click or the network retries."
Fix in layers
- Frontend: disable the button while submitting.
- API idempotency key: client sends
Idempotency-Key; server stores it with a unique index and replays the stored response for repeats. - Database constraint: unique index on a natural key (e.g.
{ cartId: 1 }on orders, or{ paymentId: 1 }). - Webhooks: store processed event IDs; process each once.
await Order.collection.createIndex({ cartId: 1 }, { unique: true });
try {
const order = await Order.create({ cartId, userId, items });
} catch (e) {
if (e.code === 11000) return res.status(200).json(await Order.findOne({ cartId })); // already created
throw e;
}Scenario 5 โ "Two users bought the last item in stock at the same time; stock went negative."
Cause: read-modify-write race:
const p = await Product.findById(id); // both requests read stock = 1
if (p.stock > 0) { p.stock -= 1; await p.save(); } // both write 0โฆ or -1Fix: make the check and the update one atomic operation.
const updated = await Product.findOneAndUpdate(
{ _id: id, stock: { $gte: qty } }, // condition
{ $inc: { stock: -qty } }, // atomic decrement
{ new: true },
);
if (!updated) throw new HttpError(409, "Out of stock");For multi-document changes (stock + order + payment record), use a transaction. For flash sales, consider a Redis counter (DECR) as a gate in front of the database.
Scenario 6 โ "Users can see other users' invoices by changing the ID in the URL."
Cause: Insecure Direct Object Reference โ authentication is checked, authorisation (ownership) is not.
Fix: scope every query by the owner, return 404 for others, and add tests.
const invoice = await Invoice.findOne({ _id: req.params.id, userId: req.user.id });
if (!invoice) throw new HttpError(404, "Invoice not found");Prevent: a shared helper/repository that always applies tenant/owner filters; authorisation tests for every resource route; security review checklist.
Scenario 7 โ "The Node process crashes with JavaScript heap out of memory every few hours."
Verify: memory metrics over time (does the minimum after GC keep rising?); heap snapshots under load compared over time; look at retainers.
Common causes: unbounded in-memory cache/map; per-request listeners not removed; loading whole collections or big files into memory; global arrays.
Fix: bounded LRU or Redis; remove listeners; stream large data; paginate.
Raising --max-old-space-size only buys time. Run under PM2/Kubernetes so a crash restarts the process while you fix the leak.
Scenario 8 โ "We need to send 1 lakh (100,000) notification emails."
Answer
- Never in the request handler. Enqueue jobs (BullMQ) โ one job per batch of ~100 recipients.
- Workers with controlled concurrency and a rate limiter matching the provider's limit (SES/SendGrid).
- Retries with backoff; failed jobs to a dead-letter set; idempotency (
notificationId + userId) so retries never send twice. - Progress tracking in a
campaigndocument; unsubscribe handling; bounce/complaint webhooks. - Use the provider's bulk API where available.
Scenario 9 โ "A third-party API (payment/SMS) is slow or sometimes down and it takes our API down with it."
Fix
- Timeouts on every outbound call (
AbortSignal.timeout(3000)) โ never wait forever. - Retries with exponential backoff + jitter, only for idempotent or keyed requests.
- Circuit breaker (e.g.
opossum): after N failures, fail fast for a cool-down period instead of piling up requests. - Fallbacks: queue the work and confirm later ("payment pending"), cached data, a secondary provider.
- Bulkheads: limit concurrent calls to that dependency so it cannot exhaust all sockets/memory.
import CircuitBreaker from "opossum";
const breaker = new CircuitBreaker((body) => smsApi.send(body), { timeout: 3000, errorThresholdPercentage: 50, resetTimeout: 30_000 });
breaker.fallback((body) => smsQueue.add("retry-sms", body));
await breaker.fire({ to, text });Scenario 10 โ "We deployed and for 30 seconds users got 502 errors."
Causes: the old process was killed while handling requests; new instances received traffic before they were ready (DB not connected).
Fix: graceful shutdown on SIGTERM (stop accepting, finish in-flight, close DB), readiness probes (/health/ready only returns 200 after Mongo/Redis connect), rolling deploys with at least one healthy instance, pm2 reload instead of restart, keep-alive timeout on Node greater than the load balancer's idle timeout.
Scenario 11 โ "Login is being brute-forced; we see thousands of attempts from many IPs."
Fix: rate limit by IP and by account (email), with Redis storage; progressive delays or temporary lockout per account; CAPTCHA after N failures; MFA; alerting on spikes; generic error messages; monitor for credential stuffing (breached-password checks).
Scenario 12 โ "We need to add API versioning because a mobile app cannot update immediately."
Answer: URL versioning (/api/v1, /api/v2) is the simplest and most visible; keep v1 running while clients migrate; share services, version only controllers/DTOs; document with OpenAPI; add a Deprecation/Sunset header and track v1 usage before removing it. Prefer additive, backward-compatible changes (new optional fields) so most changes do not need a new version.
Scenario 13 โ "Our monolithic Express app has 200 routes and is hard to work on. How would you restructure it?"
Answer outline
- Move to a feature-based (modular) structure:
modules/orders/{routes, controller, service, repository, schema, tests}. - Layers: routes (HTTP) โ controllers (req/res mapping) โ services (business rules) โ repositories (DB).
- Central error handling, validation middleware, config module, logger.
- Dependency injection (constructor params or NestJS) to make services testable.
- Clear module boundaries first; only split into microservices where there is a real scaling or team-ownership reason.
Scenario 14 โ "File uploads of 500 MB fail or crash the server."
Causes: express.json/Multer memory storage buffering the whole file; proxy body limits (Nginx client_max_body_size); request timeouts.
Fix: let the browser upload directly to S3 using a pre-signed URL (multipart upload for big files); the API only issues the URL and records the key. If it must pass through the server, stream it (busboy โ S3 Upload) with size limits.
๐ฏ What Interviewers Are Scoring
- You think in terms of measurement (logs, metrics, traces,
explain). - You know the classic backend bugs: blocking the event loop, race conditions, IDOR, N+1, memory leaks, missing timeouts.
- Your fixes include prevention: constraints, tests, alerts, idempotency.