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.
BESenior◷ 14 min
Level
FoundationsJuniorMiddleSenior
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.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.