Postboy Help

Migrating Middleware from 3.4

Postboy 3.5 replaces the 3.4 single-hook middleware with a staged abstract class, moves chain management to infrastructure messages, and turns throw-to-block into a structured cancellation. This guide maps every 3.4 construct to its 3.5 equivalent. All changes are source-breaking: middleware written for 3.4 does not compile against 3.5.

What changed

3.4

3.5

interface PostboyMiddleware { handle(message): void }

abstract class with canHandle, before, after, dispose

One handle hook for every operation

Stage-aware hooks: MiddlewareStage.Publish/Callback/Execute

postboy.addMiddleware(mw)

postboy.exec(new AddMiddleware(mw))

postboy.removeMiddleware(mw)

postboy.exec(new RemoveMiddleware(mw)) — also calls mw.dispose()

Throwing from handle to stop processing

Return { type: MiddlewareDecisionType.Interrupt } from before; the bus throws a CancelError

No visibility into outcomes

after(context, result?) receives the executor's return value on the Execute stage

No filter — every middleware saw every message

canHandle(context) scopes by stage and message

postboy.addMiddleware and postboy.removeMiddleware were removed in 3.5 together with lock, unlock, addNamespace, eliminateNamespace, and unregister — infrastructure messages are the only way to mutate the bus.

Rewrite the middleware

A 3.4 implementation with a single hook:

// 3.4 class LoggingMiddleware implements PostboyMiddleware { handle(message: PostboyMessage): void { console.log('message', message.id); } } postboy.addMiddleware(new LoggingMiddleware());

Split the hook by responsibility: gating goes to before, observation to after, filtering to canHandle:

// 3.5 import { PostboyMiddleware, PipelineContext, MiddlewareDecision, MiddlewareDecisionType, MiddlewareStage, AddMiddleware, } from '@artstesh/postboy'; class LoggingMiddleware extends PostboyMiddleware { canHandle(context: PipelineContext): boolean { return context.stage === MiddlewareStage.Execute; } before(context: PipelineContext): MiddlewareDecision { console.log('attempt', context.message.id); return { type: MiddlewareDecisionType.Continue }; } after(context: PipelineContext, result?: unknown): void { console.log('done', context.message.id, result); } } postboy.exec(new AddMiddleware(new LoggingMiddleware()));

Replace throw-to-block

Throwing from handle was the only way to stop an operation in 3.4, and callers saw an ordinary exception:

// 3.4 handle(message: PostboyMessage): void { if (this.readonly) throw new Error('Readonly mode'); }

In 3.5 a block is a decision, and the bus converts it into a structured error:

// 3.5 before(_context: PipelineContext): MiddlewareDecision { return this.readonly ? { type: MiddlewareDecisionType.Interrupt } : { type: MiddlewareDecisionType.Continue }; }

Call sites that expect blocking now catch a CancelError instead of parsing message text:

import { CancelError } from '@artstesh/postboy'; try { postboy.exec(new DeleteUserExecutor(userId)); } catch (e) { if (e instanceof CancelError || (e as Error).name === 'PostboyCancelError') { return; // blocked by policy — see e.details.middleware, .stage, .messageId } throw e; }

Migration checklist

  • Replace every postboy.addMiddleware(...)/removeMiddleware(...) call with exec(new AddMiddleware(...))/exec(new RemoveMiddleware(...)).

  • Split each handle implementation: policy checks into before, logging and metrics into after.

  • Add a canHandle filter — stage first, then message.id. Middleware now also sees infrastructure messages, which are executors on the Execute stage.

  • Give guards an explicit name via super('ReadonlyGuard'): the CancelError reason is generated from it.

  • Implement dispose() for anything the middleware allocates; RemoveMiddleware and postboy.dispose() call it.

  • Assume after can be skipped: any middleware may interrupt the operation first.

  • Update tests: assert on CancelError.details instead of thrown message strings, and remove middleware between tests to avoid leakage.

Next steps

07 September 2026