Middleware Recipes
Copy-paste middleware patterns for the most common cross-cutting concerns: logging, metrics, scoping, and resource management. Each recipe relies only on the PostboyMiddleware contract (canHandle/before/after/dispose) and the three stages. Attach any of them with postboy.exec(new AddMiddleware(mw)).
Logging and tracing messages
Record attempts in before and completions in after. Attempts survive cancellations; completions do not, because after is skipped when any middleware interrupts.
Log stable fields only: stage and message.id (the static type ID, safe for correlation). Do not log payloads by default — they may contain secrets or PII.
Measuring duration and collecting metrics
Capture a start timestamp in before, record the duration in after. Keying by message.id is safe here because exec is synchronous — operations of one type never interleave. Clear the map in dispose as well, because cancelled operations never reach after.
Label metrics with the static ID only. It is bounded by the number of message types in the codebase; per-instance values such as correlation ids are not. For the Callback stage, remember that after fires on every emitted result value — decide whether you measure "request accepted" or "request completed" before adding latency there.
Targeting a subset of messages
Scope middleware with canHandle: stage first, then message identity. Both hooks are then guaranteed to run only for the matched operations.
Put all scoping in canHandle, never inside before or after — a middleware that filters internally still pays hook overhead everywhere and skews attempt metrics.
Safely managing resources
Everything a middleware allocates must be releasable in dispose. It runs on RemoveMiddleware and on postboy.dispose(). Make it idempotent — it must not throw when there is nothing left to clean.
Keep per-operation state out of the design when possible. When you cannot avoid it — like the timing map above — treat "no after " as a normal outcome and clear leftovers in dispose.
Troubleshooting
Symptom | Cause | Fix |
|---|---|---|
Middleware never runs | Added to a different | Attach on the same instance at startup; temporarily return |
Middleware runs for everything, logs are noisy |
| Move all scoping into |
| An earlier middleware interrupted the operation | Expected on cancellation; record attempts in |
Operations cancelled unexpectedly | A guard's | Inspect |
Cannot tell which middleware cancelled | Middleware relies on the default class name or the check uses message text | Check |
Middleware fires for unknown messages | Infrastructure messages ( | Exclude them in |
Memory grows over time | Per-operation state is cleared only in | Use bounded state and clear it in |
Tests affect each other | Middleware attached globally and never removed | Remove it in teardown: |
Anti-patterns
God middleware that logs, validates, guards, and transforms in one class — split by concern.
Business logic in
before: routing, orchestration, and domain rules belong in handlers.Throwing from
beforeto block an operation — returnInterruptso callers get a structuredCancelError.Cleanup that depends on
after— cancellations skip it.Mutating
context.message— middleware observes; it does not rewrite intent.Undocumented ordering dependencies — the chain runs in registration order and the first
Interruptwins.Heavy work in
before— serialization, payload logging, I/O — it sits on every operation's hot path.