API Strategy and EventManager Integration Patterns
api-strategy and event-manager are not optional add-ons in the example
applications. They are the recommended way to keep transport concerns and
structured operational behavior out of domain CRUD code.
This guide describes the intended split.
Principle
Use:
- CrudGen for standard resource CRUD contracts
- ApiStrategy for service-to-service communication
- YalcEventService for logs, domain events, and HTTP-aware errors
These three tools solve different problems and should not replace one another.
When to use ApiStrategy
Use ApiStrategy when a module or app needs to call another service boundary.
Typical examples:
- typed module clients that call another HTTP surface
- local in-process calls in development
- swappable transport per environment
Recommended pattern:
- keep CRUD controllers/resolvers focused on resource behavior
- inject a typed module client for cross-boundary calls
- keep the transport implementation behind a provider token
- when multiple transports are valid, expose one stable caller token through
ApiCallStrategySelectorProvider
This allows the same application logic to move between:
- local-call transport
- HTTP transport
- event transport
without changing the caller contract.
Use selector providers for app-level configuration such as local development versus remote deployment. Register each concrete strategy under its own token, then expose one final token to consumers:
ApiCallStrategySelectorProvider({
provide: TASKS_CLIENT_API_STRATEGY,
defaultStrategy: "local",
strategies: {
local: TASKS_CLIENT_LOCAL_API_STRATEGY,
http: TASKS_CLIENT_HTTP_API_STRATEGY,
},
selector: {
useFactory: () => process.env.TASKS_API_STRATEGY,
},
});
The same selector shape applies to IEventStrategy via
EventStrategySelectorProvider. The built-in local event strategy uses
EventEmitter2 for same-runtime handlers. Broker transports such as
RabbitMqEventStrategy publish externally only. When a use case needs both,
wrap the local and broker strategies with CompositeEventStrategy; then wrap
the broker branch with ConditionalEventStrategy if publishing must be
feature-flagged.
The task-system app demonstrates this with TasksEventsClient:
workflow service -> domain events service -> task events client -> selected event strategy
The app keeps local as the default event transport and exposes RabbitMQ as an
optional composed strategy selected with TASK_EVENTS_STRATEGY=rabbitmq. In
that mode the selected strategy is:
CompositeEventStrategy(local EventEmitter2, conditional RabbitMQ publish)
This keeps the workflow code independent from the broker while still allowing
an e2e suite to exercise local handlers plus a real exchange, queue binding,
publisher, and consumer. Setting TASK_RABBITMQ_PUBLISH_ENABLED=false disables
only the broker branch; local handlers still receive the event.
When to use YalcEventService
Use YalcEventService whenever the code needs:
- structured logging
- domain event emission
- safe HTTP-aware errors
- consistent error forwarding
Recommended pattern:
- service-layer validation errors should use
errorBadRequest,errorNotFound, and related helpers - operational or domain events should go through
log,logAsync,emit, oremitAsync - avoid raw
consoleand avoid ad-hocHttpExceptionusage in framework examples
When the same behavior must be externally observable, register
@nestjs-yalc/observability and wrap selected call/event strategies with
TelemetryCallStrategy or TelemetryEventStrategy. Keep OpenTelemetry as a
monitoring plugin: it should observe EventManager and strategy activity, not
replace domain event transports such as local EventEmitter2 or RabbitMQ.
How this combines with CrudGen-first apps
In a CrudGen-first app:
- generated GraphQL/REST surfaces stay thin
- workflow services implement the business behavior
- ApiStrategy lives behind typed module clients
- YalcEventService is used inside those services for logs/events/errors
This keeps the CRUD surface generic while the operational behavior remains explicit and testable.
Task-system app examples
The task-system app intentionally keeps several non-CRUD endpoints to show these patterns:
/task-workflows/*endpoints for ApiStrategy usage throughTasksApiClientexported by the task-system module package- logging endpoints for EventManager usage
- error endpoints for HTTP-aware EventManager errors
- domain event demo endpoints
The workflow endpoints combine strategy-backed calls and domain events:
POST /task-workflows/project-with-taskcreates resources through the selected strategy and emitstask-system.tasks.created.PUT /task-workflows/tasks/:id/completeupdates through the selected strategy and emitstask-system.tasks.status-changed.
By default these events are handled through local EventEmitter2. For a
composed local-plus-broker run, start the task app RabbitMQ compose file and
run:
npm run rabbitmq:up --prefix examples/task/app
npm run test:e2e:rabbitmq --prefix examples/task/app
npm run rabbitmq:down --prefix examples/task/app
That suite sets TASK_EVENTS_STRATEGY=rabbitmq and verifies that workflow
domain events still reach local EventEmitter2 handlers, are published to
RabbitMQ, and are consumed back by the task app’s queue-backed handler. It also
verifies that TASK_RABBITMQ_PUBLISH_ENABLED=false suppresses only the broker
publish branch.
The observability e2e suite starts a lightweight OpenTelemetry Collector and verifies that the same workflows export telemetry for workflow spans, API strategy calls, event strategy emits, EventManager domain events, errors, and RabbitMQ publishing. Local development can use the Grafana LGTM compose stack for a UI, but CI only needs the Collector file/debug exporter.
Those endpoints are examples of integration behavior. They are intentionally separate from generated CRUD resources.
Recommended decision rule
If the behavior is:
- resource CRUD -> keep it in CrudGen
- cross-service communication -> use ApiStrategy, with selector providers when the transport is configurable
- log/error/event behavior -> use YalcEventService
If all three are mixed into one controller/resolver method, the layering is usually wrong.