Skip to content

add @node-ts/bus-nestjs, a NestJS module for the bus - #352

Merged
adenhertog merged 3 commits into
masterfrom
issue-268-nestjs-module
Oct 8, 2026
Merged

adenhertog merged 3 commits into
masterfrom
issue-268-nestjs-module

Conversation

@adenhertog

@adenhertog adenhertog commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Closes #268

Summary

Adds @node-ts/bus-nestjs, a NestJS module (NestJS 11 and 12) that registers the bus, resolves class handlers and workflows from Nest's container, and runs the bus from Nest's lifecycle hooks. The design was agreed on the issue: #268 (comment).

Background

NestJS is the most common way enterprise TypeScript teams structure services. Until now, an app had to wire the bus into Nest itself, through withContainer() and its own lifecycle code (#180).

Problem

Nest apps had no supported way to:

  • register handlers and workflows as providers
  • inject the bus
  • start and stop it with the app
  • give request-scoped providers to handlers
  • provision a bus whose handlers only exist once Nest has booted

The bus' own SIGINT/SIGTERM listeners also clash with Nest's shutdown hooks: they stop the bus, but can leave the process running.

Approach

Registering the bus

  • BusModule.forRoot({ configure }) and forRootAsync({ imports, inject, useFactory }) are global.
  • The factory gets a configuration that logs through Nest's Logger and has no interrupt signals. It returns it with the transport, persistence and message types. BusModule then adds the handlers, workflows and a container backed by Nest, and builds the bus.

Registering handlers and workflows

  • Class handlers and workflows are providers of the user's own modules. They're registered with the bus by @BusHandler() / @BusWorkflow() (built on DiscoveryService.createDecorator(), and typed so only Handler / Workflow classes can be decorated) or by listing them in BusModule.forFeature().
  • forFeature only registers: it never makes a class a provider.
  • Function handlers and defineWorkflow() workflows are registered with forFeatureAsync(), whose factory is given the providers they need.
  • Messages still use no decorators.

Injecting the bus

  • The default bus is injected as BusInstance. Named buses use @InjectBus(name) / getBusToken(name).
  • The bus is built in onModuleInit, because that's the first point where every forFeatureAsync factory has run. Until then, providers get a stand-in that forwards to the bus once it's built.
  • Used before then, the stand-in throws BusNotBuilt, or rejects with it for async methods like send(). Its help says to use the bus from onApplicationBootstrap() or later.

Request scope

  • nestContainer(moduleRef) uses moduleRef.get for singletons.
  • Request-scoped and transient providers are resolved with moduleRef.resolve in one ContextId per received message, with { message, attributes } registered as REQUEST (BusRequest).
  • bus-core also resolves each class workflow once with no message, to read its configureWorkflow(). If a request-scoped workflow fails then, startup fails with WorkflowResolvedWithoutMessage.

Lifecycle

  • onApplicationBootstrap: initialize, then start if the new BusInstance.canStart (bus-core) is true. If either fails, the bus is disposed and the error rethrown.
  • onModuleDestroy: stop. onApplicationShutdown: dispose.
  • BusModule is global, so Nest runs these after the same hook of every non-global module. The bus therefore keeps handling messages during other modules' onModuleDestroy, finishes them before any beforeApplicationShutdown, and is disposed after other modules' onApplicationShutdown.
  • A test pins that order with a message in flight. The docs tell users to release what handlers use in beforeApplicationShutdown / onApplicationShutdown, or to use lifecycle: 'manual' and call bus.stop() before app.close().
  • lifecycle: 'manual' leaves initializing and starting to the app.

Startup errors

  • BusClassNotProvided, BusNotRegistered, BusAlreadyRegistered, BusFeatureNotStatic, WorkflowResolvedWithoutMessage and BusCoreVersionNotSupported (when the bus-core peer is too old to have canStart), each naming the fix.

Provisioning

  • createBusForProvisioning(AppModule) boots the app with each bus built but never initialized.
  • bus provision now also accepts an export that is a built bus, or a function returning one.
  • bus.mjs exits once its output is flushed, so a Nest app's open handles don't keep the process alive.

Tests

  • Nest 12 is ESM-only. test.env already ran Jest with --experimental-vm-modules (added for mongodb in support the outbox and inbox in bus-mongodb #345), so every existing suite already passes with it; its comment now mentions Nest too.
  • The integration tests use Nest testing modules over the in-memory queue, so they need no infra. They cover every way to register handlers and workflows, request scope, several buses, the lifecycle and shutdown order, disposing after a failed start, the startup errors, provider overrides and provisioning. There are specs for the stand-in bus, the container adapter, the lifecycle host and the logger.
  • Docs: a new guide/nestjs page with type-checked snippets (docs/tsconfig.json turns on experimentalDecorators for them), and a package README.

Departure from the agreed design

  • configure / useFactory are given the configuration to extend, rather than creating one. This means a withLogger() in the factory overrides Nest's logger, instead of being silently replaced.
  • If the factory returns a different configuration, BusModule warns that Nest's logger and shutdown handling are lost.

Follow-ups

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
adenhertog and others added 2 commits October 8, 2026 08:47
…tup errors

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…alize and dispose

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@adenhertog
adenhertog merged commit c1b949a into master Oct 8, 2026
4 checks passed
@adenhertog
adenhertog deleted the issue-268-nestjs-module branch October 8, 2026 01:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NestJS integration module

1 participant