@rayondigital/nest-dapr
v0.11.4
Published
Develop NestJs microservices using Dapr pubsub, actors, workflows and other bindings
Readme
NestJS + Dapr
Develop NestJs microservices using Dapr pubsub, actors and bindings.
Description
Dapr Module for Nest built on top of the latest Dapr JS SDK. Additional monkey patches are applied to the SDK to support NestJS dependency injection, decorators, observability and lifecycle hooks.
Supported features
- [x] Actors
- [x] PubSub
- [x] Bindings
- [x] Workflows
- [x] Distributed Lock
- [x] State
- [x] Service Invocation
Installation
npm i @rayondigital/nest-daprPeer dependencies
This library depends on:
nestjs-clseventemitter2rxjs@opentelemetry/api
nest-dapr declares its NestJS-ecosystem dependencies as peer dependencies rather than bundling its own copies, so your app shares a single instance of them — this matters in particular for nestjs-cls, whose context propagation depends on there being exactly one shared instance across your app.
Depending on your package manager, peer dependencies aren't always installed automatically. nestjs-cls, eventemitter2 and @opentelemetry/api are required from the moment you import DaprModule (not optional), so if you hit Cannot find module 'nestjs-cls' or similar, install them explicitly:
npm i nestjs-cls eventemitter2 @opentelemetry/apiA standard Nest project (scaffolded via nest new) already provides the remaining peer dependencies — @nestjs/common, @nestjs/core, @nestjs/platform-express and rxjs.
Requirements
Install Dapr as per getting started guide. Ensure Dapr is running with
dapr --versionOutput:
CLI version: 1.18.0
Runtime version: 1.18.2Quick start
The following scaffolds a Nest project with the nest-dapr package and demonstrates using Nest with Dapr using actors and RabbitMQ pubsub bindings.
Install Nest CLI
npm install -g @nestjs/cliScaffold Nest project
nest new nest-dapr
cd nest-dapr/Install nest-dapr package
npm i --save @rayondigital/nest-daprImport DaprModule in AppModule class
@Module({
imports: [DaprModule.register()],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}Import DaprClient from @dapr/dapr package and add dependency to AppController class
import { DaprClient } from '@dapr/dapr';
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
private readonly daprClient: DaprClient,
) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}Actors
Note: Actors communicate via HTTP and require the Dapr sidecar to be running with HTTP enabled. See DaprServer section for more information. The port for HTTP is set in the
DAPR_HTTP_PORTenvironment variable. Actors may communicate over gRPC in the future, but this is not yet supported.
Create actors and connect them to your NestJS application using the @DaprActor decorator.
This decorator takes in the interface of the actor, and marks the Actor as transient inside the NestJS
dependency injection container.
Ensure your actor classes are added to the providers array of your NestJS module.
// You must expose your actors interface as an abstract class because Typescript interfaces are not available at runtime (erasure).
// Having the interface as an abstract class allows us to call the actor by only knowing the interface type.
export abstract class CounterActorInterface {
abstract increment(): Promise<number>;
abstract getCounter(): Promise<number>;
}
@DaprActor({
interfaceType: CounterActorInterface,
})
export class CounterActor
extends StatefulActor
implements CounterActorInterface
{
// You can inject other NestJS services into your actor.
// Only Singleton services are supported at this time.
@Inject(CacheService)
private readonly cacheService: CacheService;
counter: number;
async onActivate(): Promise<void> {
this.counter = await this.getStateValue('counter', 0);
return super.onActivate();
}
async increment(): Promise<number> {
this.counter++;
// Use a NestJS service as an example.
// Share in memory state between actors on this node.
// You probably will never want to do this, but we're just demonstrating a singleton service.
await this.cacheService.increment('total');
await this.setStateValue('counter', this.counter);
await this.saveState();
return this.counter;
}
async getCounter(): Promise<number> {
return this.counter;
}
}Actor State
StatefulActor gives you four ways to persist state, in increasing order of correctness. <T> generic parameters are erased at compile time — the only thing that exists at runtime is whatever concrete class/type you actually pass or declare, so "reconstruction" below always means "what really happens when the JSON comes back off the wire," not what TypeScript's types promise you.
| Strategy | How | Reconstruction | Best for |
|---|---|---|---|
| Raw | this.getStateValue(name, default) / this.setStateValue(name, value) — no decorator | None. You get back exactly what was stored (transparently decompressed if needed) | A single primitive/plain value under one key |
| StatefulActorOf<TState> | Extend StatefulActorOf<TState> instead of StatefulActor; use this.state | None. TState is compile-time only — whatever's stored is trusted as-is, like JSON.parse(x) as TState | A whole plain-data blob per actor, no class behavior needed |
| @State() on a plain class | @State() foo: PlainClass where PlainClass has no toJSON/fromJSON | Best-effort: new PlainClass(), then the stored value's own keys are shallow-copied onto it | Simple, flat classes with a working no-arg constructor — use sparingly |
| @State() on an IState class (recommended) | @State() foo: MyState where MyState implements IState (toJSON/fromJSON) | Guaranteed — your fromJSON fully controls reconstruction, including any nested objects/arrays | Anything with nested state, custom construction, or where correctness matters |
The plain-class strategy has two sharp edges that made IState the recommended default:
- No nested reconstruction. Nested objects/arrays come back as plain data, never as instances of their own classes — only the top-level
typeyou declared gets instantiated. - Silent fallback if construction fails. If the class's constructor throws when called with no arguments, you get back a plain object, not an instance of your class —
instanceofisfalseand every method is gone. Nothing catches this at compile time, because the generic type parameter that would have caught it doesn't exist at runtime.
The recommended pattern, taken directly from this repo's own test actors:
export class CounterState implements IState {
counter: number = 0;
fromJSON(json: any) {
this.counter = json.counter;
return this;
}
toJSON(): any {
return { counter: this.counter };
}
}
@DaprActor({ interfaceType: CounterActorInterface })
export class CounterActor extends StatefulActor implements CounterActorInterface {
// A factory, not a shared object literal — every actor that falls back to
// this default gets its own instance. A static object here would be
// silently shared (and mutated) across every actor of this type.
@State({ defaultValue: () => new CounterState() })
state: CounterState;
async increment(): Promise<void> {
this.state.counter++;
await this.saveState();
}
}@State() also accepts compressed: true (gzip-envelopes the value on write, transparently unwrapped on read regardless of whether the flag is currently on), and serialize/deserialize hooks. Beyond normal (de)serialization, deserialize is the supported way to migrate data written by an older version of your actor — e.g. adapting a renamed/restructured field before fromJSON ever sees it:
@State({
deserialize: (raw) => ('amount' in raw ? { amountInDollars: raw.amount / 100 } : raw),
})
price: Price;Primitive type changes (e.g. a field that used to be a string and is now a number) don't need a deserialize hook at all — reading old data with the new declared type coerces it automatically (Number, String, Boolean, and Date are all handled).
Compression
Setting compressed: true on @State() gzips the value (after toJSON/serialize have run) and stores it as a small { "$gz": "<base64>" } envelope instead of the raw JSON — useful for cutting down the size of larger state values in the state store.
Reads don't rely on the compressed flag at all: every read checks whether the stored value actually looks like a compression envelope and unwraps it if so, decompressing before deserialize/fromJSON ever see it. That means toggling compressed on or off is always safe — data written while it was on still decompresses correctly after you turn it off, and existing uncompressed data keeps reading fine after you turn it on.
Actor Client
This module provides the DaprActorClient which is a NestJS service.
It can be injected into controllers, services, handlers and other actors.
It acts as a proxy service to the actors, and allows you to call methods on the actors - similar to the Orleans GrainFactory.
@Controller()
export class CounterController {
constructor(
private readonly actorClient: DaprActorClient,
) {}
@Get(":id")
async increment(@Param("id") id: string): Promise<string> {
const value = await this.actorClient
.getActor(CounterActorInterface, id)
.increment();
return `Counter incremented to ${value}`;
}
}Workflows
Note: Workflows communicate via GRPC and require the Dapr sidecar to be running with GRPC enabled. See DaprServer section for more information. The port for GRPC is set in the
DAPR_GRPC_PORTenvironment variable.
Workflows and Activities are annotated with the @DaprWorkflow and @DaprActivity decorators respectively.
When added to the providers array of a NestJS module, they are automatically registered with the Dapr server.
Note: Take care to ensure that your activities are stateless, and idempotent. Be very careful with the state, and services you have inside your workflows.
@DaprActivity()
export class HelloActivity implements WorkflowActivity<string, string> {
async run(context: WorkflowActivityContext, name: string): Promise<string> {
return `Hello ${name}!`;
}
}
@DaprActivity()
export class CreateEntityActivity implements WorkflowActivity<string, Entity> {
@Inject()
private readonly entityService: EntityService;
constructor(private readonly cacheService: CacheService) {}
async run(context: WorkflowActivityContext, id: string): Promise<Entity> {
const entity: Entity = { id: id, createdAt: new Date(), lastUpdatedAt: new Date(), status: 'created', data: {} };
await this.entityService.update(entity);
console.log('entity', entity);
return entity;
}
}
@DaprActivity()
export class GetEntityActivity implements WorkflowActivity<string, Entity> {
@Inject()
private readonly entityService: EntityService;
constructor(private readonly cacheService: CacheService) {}
async run(context: WorkflowActivityContext, id: string): Promise<Entity> {
const entity = await this.entityService.get(id);
console.log('entity', entity);
return entity;
}
}
@DaprWorkflow()
export class HelloWorkflow implements Workflow<string[], string> {
async *run(ctx: WorkflowContext, input: string): AsyncGenerator<unknown, string[]> {
const cities: string[] = [];
let entity = expect<Entity>(yield ctx.callActivity(CreateEntityActivity, '12345'));
ctx.setCustomStatus('Entity');
entity = expect<Entity>(yield ctx.callActivity(GetEntityActivity, '12345'));
console.log('entity', entity);
ctx.setCustomStatus('Entity');
const result1 = expect<string>(yield ctx.callActivity(HelloActivity, 'Tokyo'));
ctx.setCustomStatus('Tokyo');
const event = yield ctx.waitForExternalEvent('next');
console.log('event', event);
const result2 = expect<string>(yield ctx.callActivity(HelloActivity, 'Seattle'));
ctx.setCustomStatus('Seattle');
const result3 = expect<string>(yield ctx.callActivity(HelloActivity, 'London'));
ctx.setCustomStatus('London');
return cities;
}
}Workflow Client
@Controller()
export class WorkflowController {
constructor(
private readonly workflowClient: DaprWorkflowClient,
) {}
@Get(":id")
async start(@Param("id") uuid: string): Promise<string> {
const id = await workflowClient.scheduleNewWorkflow(HelloWorkflow, 'Hello', uuid);
// Workflow is started, and the id is returned.
// You can wait for the workflow to start and get the initial state.
const initialState = await workflowClient.waitForWorkflowStart(id, undefined, 15);
// You can raise events
// await workflowClient.raiseEvent(id, 'next', { input: 'next' });
// Optionally you can also wait for it to complete.
// const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 15);
// Use the workflowOutput helper to get typed variables
// const value = workflowOutput(HelloWorkflow, state);
return {
id: id,
state: initialState
}
}
}Distributed Lock
DaprLockClient is a thin NestJS-friendly wrapper around the Dapr JS SDK's Distributed Lock API, injectable via DI like any other Nest provider.
@Injectable()
export class OrderService {
constructor(private readonly lockClient: DaprLockClient) {}
async processOrder(orderId: string) {
// Acquires the lock, runs the callback, and always releases the lock afterwards
// (even if the callback throws). Throws DaprLockAcquisitionError if the lock
// is already held by someone else.
return this.lockClient.withLock(
orderId,
async () => {
// Critical section protected by the distributed lock
return this.doProcessOrder(orderId);
},
{ expiryInSeconds: 30, storeName: 'lockstore' },
);
}
async tryProcessOrder(orderId: string) {
// Or manage the lock manually
const lockOwner = randomUUID();
const acquired = await this.lockClient.tryLock(orderId, lockOwner, 30);
if (!acquired) {
return false;
}
try {
await this.doProcessOrder(orderId);
} finally {
await this.lockClient.unlock(orderId, lockOwner);
}
return true;
}
}The default lock store name (lockstore) can be changed via lockClient.setDefaultName('my-lock-store'), or overridden per-call via the storeName argument/option.
State Management
DaprStateClient is a thin NestJS-friendly wrapper around the Dapr JS SDK's State Management API, injectable via DI like any other Nest provider.
It has two tiers of methods:
- Raw (
getRawValue,setRawValue,getBulk,save,delete,transaction) — exactly what the SDK returns, no decompression, no hydration. - Typed (
getObject,saveObject) — compression-aware, and will calltoJSON/fromJSONon your value automatically if it looks like one.
class Order implements IState {
id: string;
total: number;
toJSON() {
return { id: this.id, total: this.total };
}
fromJSON(json: any): this {
this.id = json.id;
this.total = json.total;
return this;
}
}
@Injectable()
export class OrderService {
constructor(private readonly stateClient: DaprStateClient) {}
async saveOrder(order: Order) {
// Calls order.toJSON() automatically, and gzip-compresses it before saving
await this.stateClient.saveObject(order.id, order, { compressed: true });
}
async getOrder(id: string): Promise<Order> {
// Decompresses (if needed) and calls Order.fromJSON() to restore the type
return this.stateClient.getObject(id, Order);
}
async getOrderRaw(id: string) {
// No hydration — exactly what the state store returns
return this.stateClient.getRawValue(id);
}
}The default state store name (statestore) can be changed via stateClient.setDefaultName('my-state-store'), or overridden per-call via the storeName argument.
Note:
query()is intentionally not exposed. Only a handful of state store components support querying, and Dapr has flagged the entire state-query feature for deprecation in a future release.
Service Invocation
Service Invocation lets other Dapr-enabled services call methods on your app directly through the sidecar (as opposed to PubSub/Bindings, which are for asynchronous messaging).
Use the @DaprHandler decorator to register a handler for incoming invocations. It's a Nest DI-aware wrapper around the Dapr JS SDK's server.invoker.listen() — same discovery/registration mechanism as @DaprPubSub and @DaprBinding, so it works on any @Injectable() provider or @Controller() registered in your module.
import { Injectable } from '@nestjs/common';
import { DaprHandler } from '@rayondigital/nest-dapr';
import { DaprInvokerCallbackContent, HttpMethod } from '@dapr/dapr';
@Injectable()
export class GreeterService {
@DaprHandler({ path: 'hello-world', method: HttpMethod.GET })
helloWorld(data: DaprInvokerCallbackContent): any {
console.log('Received body: ', data.body);
console.log('Received metadata: ', data.metadata);
console.log('Received query: ', data.query);
console.log('Received headers: ', data.headers); // only available over HTTP
return { message: 'Hello from nest-dapr!' };
}
}path is the Dapr method name callers use — it corresponds to the methodName in client.invoker.invoke(appId, methodName, ...). method is the HTTP verb the handler accepts (defaults to GET); it's ignored when the Dapr server is running gRPC-only. The handler may return a value (serialized back to the caller) or void.
Call it from another service using DaprClient (already available via DI, see DaprServer & DaprClient providers):
await this.daprClient.invoker.invoke('my-app-id', 'hello-world', HttpMethod.GET);PubSub
Create pubsub & topic names used for pubsub operations and message interface
const pubSubName = 'my-pubsub';
const topicName = 'my-topic';
interface Message {
hello: string;
}
@Controller()Create endpoint to publish topic message
@Post('pubsub')
async pubsub(): Promise<boolean> {
const message: Message = { hello: 'world' };
return this.daprClient.pubsub.publish(pubSubName, topicName, message);
}Create pubsub handler which will subscribe to the topic and log the received message
@DaprPubSub(pubSubName, topicName)
pubSubHandler(message: Message): void {
console.log(`Received topic:${topicName} message:`, message);
}Create Dapr pubsub component in components folder
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: my-pubsub
namespace: default
spec:
type: pubsub.rabbitmq
version: v1
metadata:
- name: host
value: amqp://guest:guest@localhost:5674Save file as components/rabbitmq-pubsub.yaml
Create docker-compose.yml in the project root used to run RabbitMQ
version: '3.9'
services:
pubsub:
image: rabbitmq:3-management-alpine
ports:
- 5674:5672
- 15674:15672Start RabbitMQ
docker-compose upCreate script to bootstrap your Nest project using Dapr sidecar. Update package.json and add script
"scripts": {
..
"start:dapr": "dapr run --app-id nest-dapr --app-protocol http --app-port 50001 --dapr-http-port 50000 --components-path ./components npm run start"
},Start Nest app with Dapr
npm run start:daprInvoke endpoint to publish the message
curl -X POST localhost:3000/pubsubThis should publish a message to RabbitMQ which should be consumed by the handler and written to the console:
== APP == Received topic:my-topic message: { hello: 'world' }Full example
import { DaprClient } from '@dapr/dapr';
import { DaprPubSub } from '@rayondigital/nest-dapr';
import { Controller, Get, Post } from '@nestjs/common';
import { AppService } from './app.service';
const pubSubName = 'my-pubsub';
const topicName = 'my-topic';
interface Message {
hello: string;
}
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
private readonly daprClient: DaprClient,
) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Post('pubsub')
async pubsub(): Promise<boolean> {
const message: Message = { hello: 'world' };
return this.daprClient.pubsub.publish(pubSubName, topicName, message);
}
@DaprPubSub(pubSubName, topicName)
pubSubHandler(message: Message): void {
console.log(`Received topic:${topicName} message:`, message);
}
}DaprModule
DaprModule is a global Nest Module used to register DaprServer & DaprClient as providers within your project. It also registers all your handlers which listen to Dapr pubsub and input bindings so that when messages are received by Dapr, they are forwarded to the handler. Handler registration occurs during the onApplicationBootstrap lifecycle hook.
To use nest-dapr, import the DaprModule into the root AppModule and run the register() static method.
@Module({
imports: [DaprModule.register()],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}register() takes an optional DaprModuleOptions object which allows passing arguments to DaprServer instance.
export interface DaprModuleOptions {
serverHost?: string;
serverPort?: string;
daprHost?: string;
daprPort?: string;
communicationProtocol?: CommunicationProtocolEnum;
clientOptions?: DaprClientOptions;
}See Dapr JS docs for more information about these arguments.
Async configuration
You can pass your options asynchronously instead of statically. In this case, use the registerAsync() method, which provides several ways to deal with async configuration. One of which is to use a factory function:
DaprModule.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
serverHost: configService.get('DAPR_SERVER_HOST'),
serverPort: configService.get('DAPR_SERVER_PORT'),
daprHost: configService.get('DAPR_HOST'),
daprPort: configService.get('DAPR_PORT'),
communicationProtocol: CommunicationProtocolEnum.GRPC,
clientOptions: {
logger: {
level: LogLevel.Verbose,
},
},
}),
inject: [ConfigService],
})DaprServer & DaprClient providers
DaprModule registers DaprServer and DaprClient as Nest providers. These can be injected into your controllers and services like any other provider.
import { DaprClient } from '@dapr/dapr';
import { Controller, Post } from '@nestjs/common';
@Controller()
export class AppController {
constructor(readonly daprClient: DaprClient) {}
@Post()
async pubsub(): Promise<boolean> {
return this.daprClient.pubsub.publish('my-pub-sub', 'my-topic', {
hello: 'world',
});
}
}Dapr decorators
nest-dapr provides two TypeScript decorators which are used to declaratively configure subscriptions and bindings. These are used by DaprModule in conjunction with the handler method to define the handler implementations.
DaprPubSub decorator
DaprPubSub decorator is used to set-up a handler for receiving pubsub topic messages. The handler has 3 arguments (name, topicName & route). name specifies the pubsub component name as defined in the Dapr component metadata section. topicName is the name of the pubsub topic. Route is an optional argument and defines possible routing values.
Example:
@DaprPubSub('my-pubsub', 'my-topic')
pubSubHandler(message: any): void {
console.log('Received message:', message);
}RabbitMQ pubsub Component:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: my-pubsub
namespace: default
spec:
type: pubsub.rabbitmq
version: v1
metadata:
- name: host
value: amqp://guest:guest@localhost:5674Publish message:
await this.daprClient.pubsub.publish('my-pubsub', 'my-topic', { hello: 'world' });In this example the handler pubSubHandler method will receive messages from the my-topic topic through the my-pubsub component which in this case is RabbitMQ.
DaprBinding decorator
DaprBinding decorator is used to set-up a handler for receiving input binding data. The handler has one argument name which specifies the binding component name as defined in the Dapr component metadata section.
Example:
@DaprBinding('my-queue-binding')
bindingHandler(message: any): void {
coneole.log('Received message:', message);
}RabbitMQ binding component:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: my-queue-binding
namespace: default
spec:
type: bindings.rabbitmq
version: v1
metadata:
- name: queueName
value: queue1
- name: host
value: amqp://guest:guest@localhost:5674
- name: durable
value: true
- name: deleteWhenUnused
value: false
- name: ttlInSeconds
value: 60
- name: prefetchCount
value: 0
- name: exclusive
value: false
- name: maxPriority
value: 5
- name: contentType
value: "text/plain"Send message:
await this.daprClient.binding.send('my-queue-binding', 'create', { hello: 'world' });In this example the handler bindingHandler method will receive messages from the queue1 queue defined in the my-queue-binding component which in this case is RabbitMQ.
Writing handlers
DaprModule uses reflection to register all handlers found either in Controller or Provider classes. These classes must be registered in a Nest module. Providers must be decorated with the @Injectable() decorator at the class level. Once this is done and your provider is added to your module's [providers] array then nest-dapr will use Nest dependency injection container to resolve the provider instance and call your handler when the message is received.
Here's an example of a Provider containing a Dapr handler.
import { DaprPubSub } from '@rayondigital/nest-dapr';
import { Injectable, Logger } from '@nestjs/common';
@Injectable()
export class AppService {
private readonly logger = new Logger(AppService.name);
@DaprPubSub('my-pubsub', 'my-topic')
pubSubHandler(message: any): void {
this.logger.log(`Received topic message:`, message);
}
}Examples
| Example | Description | |---|-------------------------------------------------------------------------| | Basics | Demonstrates a very basic actors, pubsub & input binding using RabbitMQ |
Dapr Server
Whilst testing you will need to run a Dapr sidecar:
- HTTP Transport
dapr run --app-id nest-dapr --app-protocol http --dapr-http-port 3500 --app-port 3001 --log-level debug - gRPC Transport
dapr run --app-id nest-dapr --app-protocol grpc --dapr-grpc-port 3501 --app-port 3001 --log-level debug - Dual Mode (HTTP and gRPC)
dapr run --app-id nest-dapr --dapr-http-port 3500 --dapr-grpc-port 3501 --app-port 3001 --app-protocol grpc --app-protocol http
For testing components are defined in tests/components:
dapr run --app-id testing --app-protocol http --app-port 3001 --dapr-http-port 3500 --dapr-grpc-port 3501 --resources-path ./tests/componentsNote: On Windows the dapr sidecar may need to be run in an administrator terminal.
Troubleshooting
Dapr is a complex set of tools and services and must be set-up and deployed carefully to ensure your system operates correctly. This library is merely integration using the existing Dapr js-sdk. If things are not working out for you please review:
- Your configuration
- Your Dapr local environment
- Your port numbers and hostnames
- Dapr & SDK documentation
- The tests and examples in this project
If you find that both Dapr and the Javascript SDK is both working fine but nest-dapr is not working in some way,
please file an issue and state clearly the problem and provide a reproducible code example.
Filing an issue with something like: "It doesn't work" is likely to be ignored or removed.
Credits/Contributions :heart:
Thanks to:
- @dbc-tech/nest-dapr - We forked from this repository
- nad-au - Worked on pubsub and initial integration
- dapr-nestjs-pubsub - The original library
- @dapr/dapr - Development team
- Nest - Development team
Licence
Released under the MIT license. No warranty expressed or implied.
Testing
npm run test:in-memoryThis is the primary test suite and should be your default when running tests locally or in CI. It starts a real Dapr sidecar (via the Dapr CLI) configured against the in-memory-backed components in tests/components (pubsub.in-memory, state.in-memory), then runs the full Jest suite against it — exercising the actual Dapr sidecar and @dapr/dapr SDK integration, not a mocked or emulated one.
Prerequisites:
- The Dapr CLI installed locally (
dapr --version). - Redis running locally on
localhost:6379—tests/components/lock.yamlconfigures the lock store aslock.redis, so the distributed lock tests need a real Redis instance even though pubsub/state use in-memory components.
Other test modes, useful in specific situations:
| Command | Description |
|---|---|
| npm run test:emulator | Runs against the lightweight in-process Dapr sidecar emulator (lib/emulator/sidecar.ts) instead of a real sidecar. No Dapr CLI or Redis required, so it's the fastest option for quick local iteration — but it's a hand-rolled emulation of a subset of the Dapr HTTP API, so treat test:in-memory as the source of truth before merging. |
| npm run test | Runs the Jest suite assuming a Dapr sidecar is already running externally (DAPR_TEST_MODE defaults to real). Pair with npm run start:dapr in a separate terminal if you want to run tests repeatedly without relaunching the sidecar each time. |
Individual spec files can be targeted the usual Jest way, e.g. npm run test:in-memory -- invoke to run only tests/dapr-invoke.spec.ts.
