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
pnpm add @temporal-contract/worker-nestjs @swan-io/boxedQuick Start
1. Create the Activities Provider
Create a provider that uses NestJS services to implement activities:
// 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:
// 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:
// 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:
@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:
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:
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:
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:
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:
@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:
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
// ✅ 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.ts2. Use Global Modules for Shared Services
@Global()
@Module({
providers: [Logger, ConfigService],
exports: [Logger, ConfigService],
})
export class CoreModule {}3. Separate Business Logic from Activities
// ✅ 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:
// ✅ 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:
workflowsPath: require.resolve('./workflows') // Relative to this fileSee Also
- Worker Usage - Standard worker implementation
- Defining Contracts - Creating contracts
- Result Pattern - Error handling with Result/Future
- API Reference - Complete NestJS worker API