Skip to content

NestJS Worker Usage

Learn how to integrate temporal-contract workers with NestJS for full dependency injection support.

Overview

The @temporal-contract/worker-nestjs package provides seamless integration between temporal-contract and NestJS, enabling you to use NestJS's powerful dependency injection system in your Temporal activities.

Installation

bash
pnpm add @temporal-contract/worker-nestjs @swan-io/boxed

Quick Start

1. Create the Activities Provider

Create a provider that uses NestJS services to implement activities:

typescript
// activities.provider.ts
import { Injectable } from "@nestjs/common";
import { declareActivitiesHandler, ActivityError } from "@temporal-contract/worker/activity";
import { Future, Result } from "@swan-io/boxed";
import { myContract } from "./contract";
import { PaymentService } from "./services/payment.service";
import { NotificationService } from "./services/notification.service";

@Injectable()
export class ActivitiesProvider {
  constructor(
    private readonly paymentService: PaymentService,
    private readonly notificationService: NotificationService,
  ) {}

  createActivities() {
    return declareActivitiesHandler({
      contract: myContract,
      activities: {
        // Global activities
        log: ({ level, message }) => {
          console.log(`[${level}] ${message}`);
          return Future.value(Result.Ok(undefined));
        },

        // Workflow-specific activities with DI
        processOrder: {
          processPayment: ({ customerId, amount }) => {
            return Future.fromPromise(this.paymentService.charge(customerId, amount))
              .mapError(
                (error) =>
                  new ActivityError(
                    "PAYMENT_FAILED",
                    error instanceof Error ? error.message : "Payment failed",
                    error,
                  ),
              )
              .mapOk((transaction) => ({ transactionId: transaction.id }));
          },

          sendNotification: ({ customerId, message }) => {
            return Future.fromPromise(this.notificationService.send(customerId, message))
              .mapError(
                (error) =>
                  new ActivityError(
                    "NOTIFICATION_FAILED",
                    error instanceof Error ? error.message : "Notification failed",
                    error,
                  ),
              )
              .mapOk(() => undefined);
          },
        },
      },
    });
  }
}

2. Configure the Temporal Module

Use TemporalModule.forRootAsync to configure the worker with your activities:

typescript
// app.module.ts
import { Module } from "@nestjs/common";
import { TemporalModule } from "@temporal-contract/worker-nestjs";
import { NativeConnection } from "@temporalio/worker";
import { myContract } from "./contract";
import { ActivitiesProvider } from "./activities.provider";
import { PaymentService } from "./services/payment.service";
import { NotificationService } from "./services/notification.service";

@Module({
  imports: [
    TemporalModule.forRootAsync({
      imports: [], // Import other modules if needed
      inject: [ActivitiesProvider],
      useFactory: async (activitiesProvider: ActivitiesProvider) => ({
        contract: myContract,
        connection: await NativeConnection.connect({
          address: "localhost:7233",
        }),
        workflowsPath: require.resolve("./workflows"),
        activities: activitiesProvider.createActivities(),
      }),
    }),
  ],
  providers: [ActivitiesProvider, PaymentService, NotificationService],
})
export class AppModule {}

3. Start the Application

The worker starts automatically when the NestJS application initializes:

typescript
// main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.createApplicationContext(AppModule);

  // Worker starts automatically during module initialization

  // Graceful shutdown
  const shutdown = async () => {
    console.log("Shutting down...");
    await app.close(); // Worker shuts down automatically
    process.exit(0);
  };

  process.on("SIGTERM", shutdown);
  process.on("SIGINT", shutdown);

  console.log("Worker started successfully");
}

bootstrap();

Dependency Injection

Using Services in Activities

Access any NestJS service in your activities:

typescript
@Injectable()
export class ActivitiesProvider {
  constructor(
    private readonly paymentService: PaymentService,
    private readonly inventoryService: InventoryService,
    private readonly emailService: EmailService,
    private readonly logger: Logger,
  ) {}

  createActivities() {
    return declareActivitiesHandler({
      contract: myContract,
      activities: {
        processOrder: {
          checkInventory: ({ productId, quantity }) => {
            this.logger.log(`Checking inventory for ${productId}`);
            return Future.fromPromise(this.inventoryService.reserve(productId, quantity))
              .mapError((error) => new ActivityError("INVENTORY_UNAVAILABLE", error.message, error))
              .mapOk((reservation) => ({ reservationId: reservation.id }));
          },
        },
      },
    });
  }
}

Using Configuration

Access configuration values:

typescript
import { ConfigService } from "@nestjs/config";

@Injectable()
export class ActivitiesProvider {
  constructor(
    private readonly configService: ConfigService,
    private readonly paymentService: PaymentService,
  ) {}

  createActivities() {
    const paymentGatewayUrl = this.configService.get("PAYMENT_GATEWAY_URL");

    return declareActivitiesHandler({
      contract: myContract,
      activities: {
        processOrder: {
          processPayment: ({ amount }) => {
            return Future.fromPromise(this.paymentService.charge(amount, paymentGatewayUrl))
              .mapError((err) => new ActivityError("PAYMENT_FAILED", err.message, err))
              .mapOk((tx) => ({ transactionId: tx.id }));
          },
        },
      },
    });
  }
}

Module Configuration Options

Synchronous Configuration

Use forRoot for simple, synchronous configuration:

typescript
TemporalModule.forRoot({
  contract: myContract,
  connection: await NativeConnection.connect({ address: "localhost:7233" }),
  workflowsPath: require.resolve("./workflows"),
  activities: activitiesProvider.createActivities(),
});

Asynchronous Configuration

Use forRootAsync for configuration that requires async setup or DI:

typescript
TemporalModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService, ActivitiesProvider],
  useFactory: async (config: ConfigService, activitiesProvider: ActivitiesProvider) => ({
    contract: myContract,
    connection: await NativeConnection.connect({
      address: config.get("TEMPORAL_ADDRESS"),
    }),
    namespace: config.get("TEMPORAL_NAMESPACE"),
    workflowsPath: require.resolve("./workflows"),
    activities: activitiesProvider.createActivities(),
  }),
});

Worker Lifecycle

The TemporalService manages the worker lifecycle automatically:

  • onModuleInit: Worker is created and started
  • onModuleDestroy: Worker is gracefully shut down

You can access the worker instance if needed:

typescript
import { Injectable } from "@nestjs/common";
import { TemporalService } from "@temporal-contract/worker-nestjs";

@Injectable()
export class MyService {
  constructor(private readonly temporalService: TemporalService) {}

  async getWorkerStatus() {
    const worker = this.temporalService.getWorker();
    // Access worker if needed
  }
}

Multiple Workers

Run multiple workers in the same NestJS application:

typescript
@Module({
  imports: [
    // Order processing worker
    TemporalModule.forRootAsync({
      name: "orders",
      inject: [OrderActivitiesProvider],
      useFactory: async (provider: OrderActivitiesProvider) => ({
        contract: orderContract,
        connection: await NativeConnection.connect({ address: "localhost:7233" }),
        workflowsPath: require.resolve("./order-workflows"),
        activities: provider.createActivities(),
      }),
    }),

    // Payment processing worker
    TemporalModule.forRootAsync({
      name: "payments",
      inject: [PaymentActivitiesProvider],
      useFactory: async (provider: PaymentActivitiesProvider) => ({
        contract: paymentContract,
        connection: await NativeConnection.connect({ address: "localhost:7233" }),
        workflowsPath: require.resolve("./payment-workflows"),
        activities: provider.createActivities(),
      }),
    }),
  ],
  providers: [OrderActivitiesProvider, PaymentActivitiesProvider],
})
export class AppModule {}

Testing

Test your activities with NestJS testing utilities:

typescript
import { Test } from "@nestjs/testing";
import { ActivitiesProvider } from "./activities.provider";
import { PaymentService } from "./services/payment.service";

describe("ActivitiesProvider", () => {
  let provider: ActivitiesProvider;
  let paymentService: PaymentService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        ActivitiesProvider,
        {
          provide: PaymentService,
          useValue: {
            charge: jest.fn(),
          },
        },
      ],
    }).compile();

    provider = module.get<ActivitiesProvider>(ActivitiesProvider);
    paymentService = module.get<PaymentService>(PaymentService);
  });

  it("should process payment", async () => {
    jest.spyOn(paymentService, "charge").mockResolvedValue({
      id: "tx-123",
    });

    const activities = provider.createActivities();
    const result = await activities.activities.processOrder.processPayment({
      customerId: "CUST-123",
      amount: 100,
    });

    const value = await result;
    expect(value.isOk()).toBe(true);
    expect(value.get()).toEqual({ transactionId: "tx-123" });
  });
});

Best Practices

1. Organize by Domain

typescript
// ✅ Good - organized by domain
src/
├── orders/
│   ├── orders.activities.provider.ts
│   ├── orders.contract.ts
│   ├── orders.workflows.ts
│   └── orders.module.ts
├── payments/
│   ├── payments.activities.provider.ts
│   ├── payments.contract.ts
│   ├── payments.workflows.ts
│   └── payments.module.ts

2. Use Global Modules for Shared Services

typescript
@Global()
@Module({
  providers: [Logger, ConfigService],
  exports: [Logger, ConfigService],
})
export class CoreModule {}

3. Separate Business Logic from Activities

typescript
// ✅ Good - business logic in services
@Injectable()
export class PaymentService {
  async processPayment(customerId: string, amount: number) {
    // Business logic here
  }
}

@Injectable()
export class ActivitiesProvider {
  constructor(private readonly paymentService: PaymentService) {}

  createActivities() {
    return declareActivitiesHandler({
      contract: myContract,
      activities: {
        processOrder: {
          processPayment: ({ customerId, amount }) => {
            return Future.fromPromise(this.paymentService.processPayment(customerId, amount))
              .mapError((err) => new ActivityError("PAYMENT_FAILED", err.message, err))
              .mapOk((tx) => ({ transactionId: tx.id }));
          },
        },
      },
    });
  }
}

Common Issues

Worker Not Starting

Ensure the connection is properly awaited:

typescript
// ✅ Correct
useFactory: async () => ({
  connection: await NativeConnection.connect({ address: "localhost:7233" }),
  // ...
});

// ❌ Wrong
useFactory: () => ({
  connection: NativeConnection.connect({ address: "localhost:7233" }),
  // ...
});

Activities Not Found

Verify the workflowsPath is correct:

typescript
workflowsPath: require.resolve("./workflows"); // Relative to this file

See Also

Released under the MIT License.