open atlas
↑ Back to track
Backend Architecture BE · 02 · 01

Middleware and DI: the two patterns that shape every backend

Two patterns sit underneath every framework: middleware intercepts each request in order, and dependency injection wires services once at startup. One controls what runs around your code; the other controls what your code can reach and how you test it.

BE Junior ◷ 11 min
Level
FoundationsJuniorMiddleSenior
Already know this unit? Take a 1-minute quick check →

Two engineers describe the same backend. One says “every request flows through auth, logging, and validation before the handler.” The other says “the handler gets a database client, a cache, and a mailer it never created.” They are describing the same code from two angles: the request flowing through layers, and the dependencies flowing into objects. Those two angles are middleware and dependency injection — and almost every framework decision you will make is one or the other.

Two orthogonal axes

Before you can fix a backend architecture problem, you need to know which axis it lives on — otherwise you reach for the wrong tool entirely. Middleware and dependency injection answer different questions, and confusing them is a common source of muddled architecture:

  • Middleware is about the request axis — what runs around a handler on every request, in what order. It is dynamic, per-request, and ordered: auth, logging, body parsing, rate limiting. The previous unit showed why that order is a security boundary.
  • Dependency injection is about the wiring axis — what objects a piece of code is given instead of constructing itself. It is mostly static, decided at startup, and about coupling and testability: a handler receives a UserRepository rather than calling new UserRepository() inside itself.

One is a pipeline; the other is a graph. A request travels the pipeline; the graph is assembled once before any request arrives.

Why middleware exists: cross-cutting concerns

Auth, logging, tracing, compression, and error handling are needed by almost every endpoint but belong to none of them. Without middleware, each handler re-implements them — inconsistently. Middleware factors these cross-cutting concerns into ordered layers that wrap the handler, so they are written once and applied uniformly.

Why DI exists: inversion of control

When a class calls new EmailService() internally, it is welded to that exact implementation: you cannot swap it for a fake in a test, point it at a different config, or reuse the class elsewhere. Inversion of control flips this — the class declares what it needs (an emailer) and something external decides which concrete one it gets. That external place is the composition root, the single spot near startup that knows the real implementations and wires the object graph.

MiddlewareDependency injection
AxisThe request (what runs around code)The wiring (what code is given)
WhenPer request, dynamicallyOnce at startup, statically
ShapeOrdered pipelineObject graph
SolvesCross-cutting concernsCoupling, testability
Gets wrongOrdering → security/perf bugsHidden new → untestable code

They meet in the framework

Real frameworks fuse the two. NestJS controllers are resolved from a DI container and sit behind a middleware/guard/interceptor pipeline. Express keeps DI implicit (you wire by hand or req-attach), but the middleware model is explicit. Recognizing which axis a problem lives on is the senior reflex: “this endpoint is slow on every request” is usually middleware; “this class is impossible to test” is usually DI.

The two axes also have very different cost profiles, which is why they are worth separating. Middleware cost is per request and recurring: each layer in an Express chain typically adds single-digit microseconds of overhead, but a bloated 12–15-layer chain (auth, logging, body parse, compression, CORS, helmet, rate limit, etc.) can add roughly 0.3–0.5ms at p50 and 1–3ms at p99 once GC and a slow async layer like an audit log are in play — multiplied by every request, that is real throughput. DI cost is mostly paid once at startup: a NestJS app with a few hundred providers spends on the order of tens to low-hundreds of milliseconds building its graph at boot, then serves requests with singletons at near-zero per-request wiring cost. So a middleware mistake taxes every request forever; a DI mistake usually taxes only the boot (with one sharp exception — request-scoped providers, covered in lesson 04, that drag wiring cost back into the per-request hot path).

Quiz

A service class calls `new StripeClient()` directly in its constructor, and the team finds it impossible to unit-test without hitting the real Stripe API. Which axis is the problem on?

Quiz

Which best describes the difference in *when* middleware and DI act?

Order the steps

Order these events from process start to a served request:

  1. 1 Composition root wires the object graph (repositories, clients, services)
  2. 2 Framework registers the middleware pipeline in order
  3. 3 Server starts listening for connections
  4. 4 A request arrives and flows through the middleware pipeline
  5. 5 The handler runs using the dependencies it was injected
  6. 6 The response is produced and returned
Recall before you leave
  1. 01
    What is the difference between the request axis and the wiring axis, and why keep them separate?
  2. 02
    Why does middleware exist, and what problem do cross-cutting concerns create without it?
  3. 03
    What is inversion of control and what role does the composition root play?
Recap

Underneath every backend framework sit two orthogonal patterns. Middleware is the request axis: ordered layers that wrap the handler and factor cross-cutting concerns — auth, logging, body parsing, rate limiting — so they are written once and applied uniformly, with their order acting as a security and performance boundary. Dependency injection is the wiring axis: instead of a class constructing its own collaborators with new, it declares what it needs and a composition root assembles the object graph once at startup, which is what makes code swappable and testable. One is a per-request pipeline; the other is a startup-time graph. Frameworks fuse them — a NestJS controller is DI-resolved and sits behind a guard/interceptor pipeline — but the senior reflex is to ask which axis a problem lives on. The next lessons go deep on each: first the anatomy of writing correct middleware, then the mechanics of inversion of control and DI scopes. Now when you see a flaky endpoint or an untestable class, your first question is: request axis or wiring axis?

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done
Connected lessons
appears again in188

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.