API Strategy
This library implements the strategy pattern and factory pattern so you can switch between different API transport types (HTTP, in-process calls, events) through provider configuration or explicit runtime mutation. You code against interfaces, while the concrete strategy can change per environment.
Getting started
Strategies included
NestHttpCallStrategyβ uses NestHttpService.axiosReffor real HTTP calls. It merges CLS-propagated headers (viaYalcGlobalClsService), applies an optional whitelist, mapsHttpOptionsto Axios config, and supports query parameters viaURLSearchParams.NestLocalCallStrategyβ uses Fastifyinjectto perform in-process HTTP-like calls against your app. Useful for local/dev or βmonoβ deployments where both caller and callee live in the same Nest runtime. Can optionally skip JSON parsing withshouldSkipJsonParse.NestLocalEventStrategyβ emits events throughEventEmitter2(sync or async).RabbitMqEventStrategyβ publishes events to a RabbitMQ exchange. It is a broker transport only; compose it withNestLocalEventStrategywhen same-runtime handlers must run too.- Strategy wrappers β
CompositeEventStrategy,ConditionalEventStrategy,ConditionalCallStrategy,FallbackCallStrategy, andShadowCallStrategylet you fan out, disable, filter, fallback, or shadow strategies without changing the concrete transports. - Abstracts/interfaces β
HttpAbstractStrategyaddsget/posthelpers;IHttpCallStrategy,HttpOptions,IHttpCallStrategyResponse,IHttpCallStrategyOptionsdefine the HTTP contract;IApiCallStrategy/IEventStrategydefine the core contracts for calls and events. - Strategy selector providers β
StrategySelectorProvider,ApiCallStrategySelectorProvider, andEventStrategySelectorProviderexpose one stable provider token while selecting one concrete strategy from a registered map. - Context services β
ContextCallServiceFactoryandContextEventServiceFactorybuild injectable services withgetStrategy/setStrategyfor explicit runtime mutation by application code.
You can also implement your own strategies by extending HttpAbstractStrategy or providing custom IApiCallStrategy/IEventStrategy implementations (e.g., gRPC, Kafka, SNS).
Providers (Nest wiring)
Use the factory helpers to register strategies as providers in your modules:
import {
ApiCallStrategySelectorProvider,
CompositeEventStrategy,
ConditionalEventStrategy,
EventStrategySelectorProvider,
NestHttpCallStrategyProvider,
NestLocalCallStrategyProvider,
NestLocalEventStrategyProvider,
RabbitMqEventStrategy,
// Accept the same options, including headersWhitelist/internalRequestHeader/internalRequestToken
} from '@nestjs-yalc/api-strategy';
import { HttpModule } from '@nestjs/axios';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { Module } from '@nestjs/common';
@Module({
imports: [
HttpModule, // required for NestHttpCallStrategy
EventEmitterModule.forRoot(), // required for NestLocalEventStrategy
],
providers: [
NestHttpCallStrategyProvider('HTTP_STRATEGY', {
baseUrl: 'https://api.example.com',
internalRequestToken: process.env.INTERNAL_REQUEST_TOKEN,
internalRequestHeader: 'x-internal-request-token',
}),
NestLocalCallStrategyProvider('LOCAL_STRATEGY', {
baseUrl: '/', // path prefix inside the same app
internalRequestToken: process.env.INTERNAL_REQUEST_TOKEN,
internalRequestHeader: 'x-internal-request-token',
}),
NestLocalEventStrategyProvider('EVENT_STRATEGY'),
{
provide: 'RABBITMQ_EVENT_STRATEGY',
useFactory: (localStrategy) =>
new CompositeEventStrategy([
localStrategy,
new ConditionalEventStrategy(
new RabbitMqEventStrategy({
url: process.env.RABBITMQ_URL ?? 'amqp://127.0.0.1:5672',
exchange: 'app.events',
}),
{
enabled: () => process.env.RABBITMQ_PUBLISH_ENABLED !== 'false',
disabledResult: false,
},
),
]),
inject: ['EVENT_STRATEGY'],
}),
ApiCallStrategySelectorProvider({
provide: 'API_STRATEGY',
defaultStrategy: 'local',
strategies: {
local: 'LOCAL_STRATEGY',
http: 'HTTP_STRATEGY',
},
selector: {
useFactory: () => process.env.API_STRATEGY,
},
}),
EventStrategySelectorProvider({
provide: 'SELECTED_EVENT_STRATEGY',
defaultStrategy: 'local',
strategies: {
local: 'EVENT_STRATEGY',
rabbitmq: 'RABBITMQ_EVENT_STRATEGY',
},
selector: {
useFactory: () => process.env.EVENT_STRATEGY,
},
}),
],
exports: [
'HTTP_STRATEGY',
'LOCAL_STRATEGY',
'EVENT_STRATEGY',
'RABBITMQ_EVENT_STRATEGY',
'API_STRATEGY',
'SELECTED_EVENT_STRATEGY',
],
})
export class ApiStrategyModule {}
Provider options:
NestHttpCallStrategyProvider({ baseUrl?, headersWhitelist?, internalRequestHeader?, internalRequestToken?, NestHttpStrategy? })- Injects
HttpServiceandYalcGlobalClsService. - Respects CLS headers (filtered by
headersWhitelistif provided). - Map query params via
options.parameters.
- Injects
NestLocalCallStrategyProvider({ baseUrl?, headersWhitelist?, internalRequestHeader?, internalRequestToken?, NestLocalStrategy? })- Injects
HttpAdapterHost,YalcGlobalClsService,AppConfigService. - Uses Fastify
inject; respects CLS headers andheadersWhitelist. - Adds
internalRequestTokenheader when provided (or fromAppConfigService.values.internalRequestToken). shouldSkipJsonParsecan bypassresult.json()when the body isnβt JSON.
- Injects
NestLocalEventStrategyProvider({ NestLocalStrategy? })- Injects
EventEmitter2, supportsemitandemitAsync.
- Injects
RabbitMqEventStrategyProvider({ RabbitMqStrategy?, options })- Publishes
emit/emitAsynccalls to RabbitMQ. - It does not emit through
EventEmitter2by itself. UseCompositeEventStrategyto combine local runtime handlers and broker publishing. options.urlandoptions.exchangeare required.- Optional
exchangeType,durable,persistent,contentType,publishOptions, andserializecustomize broker behavior.
- Publishes
CompositeEventStrategy(strategies, { errorMode? })- Emits the same event through every nested strategy.
- Use it for local + RabbitMQ, local + Kafka, or multi-broker fan-out.
errorMode: 'throw'is the default;ignoreskips failed branches.
ConditionalEventStrategy(strategy, { enabled?, shouldEmit?, disabledResult? })- Wraps any event strategy with a feature flag or per-event predicate.
- Use it to disable a broker publish while leaving the rest of a composite strategy active.
ConditionalCallStrategy(strategy, { enabled?, disabledResponse?, disabledError? })- Wraps any call strategy with a feature flag.
FallbackCallStrategy(strategies, { shouldFallback? })- Tries call strategies in order until one succeeds.
- Use it for explicit migration/failover cases, not as a default replacement for a clear selected transport.
ShadowCallStrategy(primary, shadows, { awaitShadows?, shadowErrorMode? })- Returns the primary call response and also invokes one or more shadow transports.
- Use it to compare an HTTP implementation with a future gRPC implementation while keeping HTTP as the user-facing result.
Strategy selector providers
Use selector providers when the caller should depend on one stable token while the concrete transport is chosen from app configuration. The selector does not construct strategies itself; it selects between strategy provider tokens that Nest has already resolved.
import {
ApiCallStrategySelectorProvider,
NestHttpCallStrategyProvider,
NestLocalCallStrategyProvider,
} from '@nestjs-yalc/api-strategy';
export const USER_API_STRATEGY = 'USER_API_STRATEGY';
export const USER_LOCAL_API_STRATEGY = 'USER_LOCAL_API_STRATEGY';
export const USER_HTTP_API_STRATEGY = 'USER_HTTP_API_STRATEGY';
providers: [
NestLocalCallStrategyProvider(USER_LOCAL_API_STRATEGY, {
baseUrl: '/users',
}),
NestHttpCallStrategyProvider(USER_HTTP_API_STRATEGY, {
baseUrl: process.env.USERS_HTTP_BASE_URL,
}),
ApiCallStrategySelectorProvider({
provide: USER_API_STRATEGY,
defaultStrategy: 'local',
strategies: {
local: USER_LOCAL_API_STRATEGY,
http: USER_HTTP_API_STRATEGY,
},
selector: {
useFactory: () => process.env.USERS_API_STRATEGY,
},
}),
];
The same pattern works for event strategies:
import {
CompositeEventStrategy,
ConditionalEventStrategy,
EventStrategySelectorProvider,
NestLocalEventStrategyProvider,
RabbitMqEventStrategy,
} from '@nestjs-yalc/api-strategy';
export const USER_EVENT_STRATEGY = 'USER_EVENT_STRATEGY';
export const USER_LOCAL_EVENT_STRATEGY = 'USER_LOCAL_EVENT_STRATEGY';
export const USER_RABBITMQ_EVENT_STRATEGY = 'USER_RABBITMQ_EVENT_STRATEGY';
providers: [
NestLocalEventStrategyProvider(USER_LOCAL_EVENT_STRATEGY),
{
provide: USER_RABBITMQ_EVENT_STRATEGY,
useFactory: (localStrategy) =>
new CompositeEventStrategy([
localStrategy,
new ConditionalEventStrategy(
new RabbitMqEventStrategy({
url: process.env.RABBITMQ_URL,
exchange: 'users.events',
}),
{
enabled: () => process.env.USER_RABBITMQ_PUBLISH_ENABLED !== 'false',
disabledResult: false,
},
),
]),
inject: [USER_LOCAL_EVENT_STRATEGY],
}),
EventStrategySelectorProvider({
provide: USER_EVENT_STRATEGY,
defaultStrategy: 'local',
strategies: {
local: USER_LOCAL_EVENT_STRATEGY,
rabbitmq: USER_RABBITMQ_EVENT_STRATEGY,
},
selector: {
useFactory: () => process.env.USERS_EVENT_STRATEGY,
},
}),
];
Selector options:
provide: final token injected by application services.defaultStrategy: key used when the selector returnsundefined,null, or an empty string.strategies: map of strategy keys to concrete provider tokens.selector.inject/selector.useFactory: optional Nest-style factory for reading configuration fromConfigService, environment variables, feature flags, or any other provider.unknownStrategyBehavior: defaults tothrow; set tofallbackto use the default strategy when configuration names an unknown key.
Prefer selector providers for environment-level transport changes. Use context services only when you need to mutate the strategy instance after the provider has been resolved.
Strategy composition
Composition is intentionally separate from selection:
- selectors choose one named strategy for the current environment
- wrappers modify or combine strategies
- concrete strategies stay focused on one transport
For events, this means a broker strategy should not also own local runtime dispatch. Compose both branches instead:
new CompositeEventStrategy([
localEventStrategy,
new ConditionalEventStrategy(rabbitMqEventStrategy, {
enabled: () => process.env.RABBITMQ_PUBLISH_ENABLED !== 'false',
disabledResult: false,
}),
]);
For call strategies, prefer one selected transport in normal request paths. Reach for wrappers only when the use case is explicit:
ConditionalCallStrategyfor feature-flagged outbound callsFallbackCallStrategyfor migration/failover flows where retrying a second transport is acceptableShadowCallStrategyfor dual-running a future transport without changing the response returned to callers
Real-world module client pattern
In application code, keep api-strategy behind typed module clients. Put the
client in the reusable domain module/package when one exists, then let each app
wire the concrete strategies and selector. Controllers should expose use cases,
not transport details, and workflow services should depend on client methods
rather than raw URLs.
Recommended layering:
controller -> workflow service -> module API client -> selected API strategy
The advanced task app is the reference example:
TasksApiClientis exported byexamples/task/moduleand wraps calls to/tasksand/projects.TaskWorkflowsControllerexposes real workflows under/task-workflows.ApiCallStrategySelectorProviderkeeps the client token stable while selectinglocalby default orhttpwhenTASKS_API_STRATEGY=http.- The e2e suite executes the same workflows through real HTTP and Fastify local injection.
The skeleton app shows the same shape in a smaller module:
UsersApiClientis exported byexamples/skeleton/moduleand wraps/usersand/phones.UsersClientControllerexposes the low-level client example under/users-client.- The Express runtime uses the HTTP strategy by default, while the e2e suite also starts a Fastify app to cover the local strategy.
Use this shape for new apps: keep the typed client close to the domain contract, then expose application workflows above it instead of thin forwarding endpoints that only relay transport calls.
Options reference (HTTP)
HttpOptions<TData, TParams>:
headers: request headers (merged with CLS headers and filtered).method: HTTP verb (defaults set by helper).signal:AbortSignal.data: request body.parameters: query parameters (converted toURLSearchParams).
IHttpCallStrategyOptions:
headersWhitelist: array of header names to propagate from CLS context.shouldSkipJsonParse(body: string): forNestLocalCallStrategy, decide whether to skip JSON parsing and return raw body.
IHttpCallStrategyResponse<T>:
data,status,statusText,headers,request?.
HttpAbstractStrategy:
call(path, options?): implemented by concrete strategies.get(path, options?),post(path, options?): convenience wrappers that setmethod.
Context services (runtime mutation)
import { ContextCallServiceFactory, ContextEventServiceFactory } from '@nestjs-yalc/api-strategy';
const CallService = ContextCallServiceFactory(defaultHttpStrategy);
const EventService = ContextEventServiceFactory(defaultEventStrategy);
Inject these services to getStrategy() or setStrategy(newStrategy) when a
consumer must mutate the strategy instance after construction. For normal
app-configuration switches, prefer the selector providers above.
Usage example
import { Inject, Injectable } from '@nestjs/common';
import { IHttpCallStrategy, IEventStrategy } from '@nestjs-yalc/api-strategy';
@Injectable()
export class UserService {
constructor(
@Inject('API_STRATEGY') private readonly http: IHttpCallStrategy,
@Inject('SELECTED_EVENT_STRATEGY') private readonly events: IEventStrategy,
) {}
async createUser(userId: string) {
await this.http.post('/users', { data: { id: userId } });
await this.events.emitAsync('user.created', { userId });
}
}
Use cases
- Start with local-call/local-event for fast dev/test in a monolith, then switch to HTTP or other transports without refactoring callers.
- Route per-environment using selector providers, while keeping caller tokens stable.
- Keep event transport open for future brokers by selecting or composing
IEventStrategyimplementations such as localEventEmitter2, RabbitMQ, Kafka, SNS, or other transports. - Use context services only for explicit runtime mutation by application code.
- Prototype service-to-service communication before introducing full API gateways/brokers.