Crux Read real Express and NestJS snippets — a middleware ordering bug, a singleton holding request state, a request-scope bubbling cost, and a dependency cycle — and pick the highest-leverage fix.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min
Middleware and DI bugs are diagnosed in wiring code and registration order, not in business logic. Read each snippet, predict what breaks under load, and choose the fix a senior engineer reaches for first.
Goal
Practise the loop every middleware/DI incident runs: read the pipeline order or the wiring, predict the failure (a leaked response, a cross-user data race, a per-request graph rebuild, an unresolvable cycle), and pick the structural fix over the band-aid.
Snippet 1 — the middleware order
const app = express();app.use(expensiveBodyParser()); // parses + buffers the full bodyapp.use(rateLimit({ max: 100 })); // rejects the 101st requestapp.use(authenticate); // verifies the bearer tokenapp.use('/api', apiRouter);
Quiz
Completed
A flood of unauthenticated, oversized requests drives CPU to 100%. What is wrong with this ordering, and what is the fix?
Heads-up Express runs middleware sequentially in registration order, each calling next(). Order is exactly the lever here: work placed before a rejection point is paid by requests that get rejected anyway.
Heads-up Registering it last would let every request reach the handler before being limited. Cheap rejections belong early — the cost of this ordering is parsing bodies for requests auth/limit will drop.
Heads-up Nothing in the snippet shows authenticate misbehaving. The visible defect is the pipeline order: the heaviest step runs before the two cheap rejection points.
Snippet 2 — the singleton field
@Injectable() // default scope: singletonexport class AuditService { private currentUser?: User; // set per request, read later setUser(u: User) { this.currentUser = u; } record(action: string) { log(`${this.currentUser?.id} did ${action}`); }}
Quiz
Completed
Under concurrent load the audit log attributes actions to the wrong user. Why, and what is the cleanest fix?
Heads-up The corruption happens before logging: the shared currentUser field is overwritten by a concurrent request. The fix is to stop storing per-request state on a singleton, not to change the logger.
Heads-up readonly would forbid setUser entirely and not solve sharing — the instance is still one per process. The field itself is the problem; pass the user as an argument instead.
Heads-up Methods do not have scopes; only providers do. The singleton's shared field is the race. Either pass the value as an argument or make the provider request-scoped.
Snippet 3 — the request-scoped logger
@Injectable({ scope: Scope.REQUEST }) // one instance per requestexport class ContextLogger { /* holds the request id */ }@Injectable() // intended as a singletonexport class PaymentService { constructor(private log: ContextLogger) {} // injects request-scoped dep}// AppController injects PaymentService, OrderService injects PaymentService...
Quiz
Completed
After this change, a load test shows higher allocation and latency across many unrelated services. Why?
Heads-up Request scope does not touch query caching. The cost is structural: it propagates up the injection chain and forces per-request re-instantiation of every consumer.
Heads-up Request-scoped instances are discarded after the request. The cost here is the per-request re-creation of the dependent subgraph, not a leak.
Heads-up It cannot stay isolated: any singleton injecting a request-scoped provider must itself become request-scoped, and so must its consumers. The lifetime bubbles upward through the graph.
The container throws 'cannot resolve dependencies'. What is the root cause, and the structurally correct fix?
Heads-up forwardRef defers one side's resolution to break the deadlock at runtime, but it preserves the cyclic design and can resolve nondeterministically across environments. It is a smell, not the structural fix.
Heads-up Scope is about lifetime, not ordering. A cycle has no valid construction order regardless of scope; changing the lifetime does not give the graph a topological order.
Heads-up Sometimes two classes that cycle should be one, but the disciplined fix is usually to extract the shared dependency into a third class both use, keeping responsibilities separate while making the graph acyclic.
Recap
Every middleware/DI incident is read in wiring, not logic. Middleware order is a cost and security lever — put cheap rejections (rate limit, auth) before expensive work like body parsing. A singleton must be stateless with respect to the request; storing per-request fields on it races across concurrent users, so pass the value as an argument. Request scope bubbles up the injection chain and rebuilds a subgraph per request, so reach for it only when truly needed. And a dependency cycle has no topological order — the structural fix is to extract a shared third class, not to silence the resolver with forwardRef.