NestJS Integration
Complete NestJS integration with dependency injection, configuration management, and modular architecture.
This example uses @sentdm/sentdm with NestJS patterns including modules, providers, services, and controllers.
Project Structure
Recommended directory structure for a NestJS project with Sent integration:
src/
├── app.module.ts
├── main.ts
├── sent/
│ ├── sent.module.ts # Dynamic module with provider
│ └── sent.types.ts # TypeScript interfaces
├── messages/
│ ├── messages.module.ts # Feature module
│ ├── messages.controller.ts # REST endpoints
│ ├── messages.service.ts # Business logic
│ └── dto/
│ ├── send-message.dto.ts
│ └── welcome-message.dto.ts
├── webhooks/
│ ├── webhooks.controller.ts # Webhook handlers
│ └── webhooks.module.ts
└── common/
└── filters/
└── sent-exception.filter.tsModule Setup
Create a dedicated Sent module with configurable options:
// sent/sent.module.ts
import { Module, DynamicModule, Provider } from '@nestjs/common';
import SentDm from '@sentdm/sentdm';
export interface SentModuleOptions {
apiKey: string;
isGlobal?: boolean;
}
export const SENT_CLIENT = Symbol('SENT_CLIENT');
@Module({})
export class SentModule {
static forRoot(options: SentModuleOptions): DynamicModule {
const sentProvider: Provider = {
provide: SENT_CLIENT,
useFactory: () => {
return new SentDm({
apiKey: options.apiKey,
});
},
};
return {
module: SentModule,
providers: [sentProvider],
exports: [sentProvider],
global: options.isGlobal ?? false,
};
}
static forRootAsync(asyncOptions: {
useFactory: (...args: any[]) => Promise<SentModuleOptions> | SentModuleOptions;
inject?: any[];
isGlobal?: boolean;
}): DynamicModule {
const sentProvider: Provider = {
provide: SENT_CLIENT,
useFactory: async (...args: any[]) => {
const options = await asyncOptions.useFactory(...args);
return new SentDm({
apiKey: options.apiKey,
});
},
inject: asyncOptions.inject || [],
};
return {
module: SentModule,
providers: [sentProvider],
exports: [sentProvider],
global: asyncOptions.isGlobal ?? false,
};
}
}Configuration with ConfigModule
Integrate with @nestjs/config for environment-based configuration:
// app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { SentModule } from './sent/sent.module';
import { MessagesModule } from './messages/messages.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env'],
}),
SentModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
apiKey: configService.getOrThrow<string>('SENT_DM_API_KEY'),
}),
inject: [ConfigService],
isGlobal: true,
}),
MessagesModule,
],
})
export class AppModule {}# .env
SENT_DM_API_KEY=your_api_key_here
SENT_DM_WEBHOOK_SECRET=your_webhook_secretService Layer
Create a service that encapsulates Sent SDK operations:
// messages/messages.service.ts
import { Injectable, Inject, Logger } from '@nestjs/common';
import SentDm from '@sentdm/sentdm';
import { SENT_CLIENT } from '../sent/sent.module';
export interface SendMessageDto {
phoneNumber: string;
templateId: string;
templateName: string;
parameters?: Record<string, string>;
channels?: string[];
}
export interface MessageResponse {
messageId: string;
status: string;
}
@Injectable()
export class MessagesService {
private readonly logger = new Logger(MessagesService.name);
constructor(
@Inject(SENT_CLIENT) private readonly sentClient: SentDm,
) {}
async sendMessage(dto: SendMessageDto): Promise<MessageResponse> {
try {
const response = await this.sentClient.messages.send({
to: [dto.phoneNumber],
template: {
id: dto.templateId,
name: dto.templateName,
parameters: dto.parameters || {},
},
channels: dto.channels,
});
const message = response.data.messages[0];
this.logger.log(`Message sent: ${message.id} to ${dto.phoneNumber}`);
return {
messageId: message.id,
status: message.status,
};
} catch (error) {
this.logger.error(
`Failed to send message to ${dto.phoneNumber}`,
error instanceof Error ? error.stack : undefined,
);
throw error;
}
}
async sendWelcomeMessage(phoneNumber: string, name: string): Promise<MessageResponse> {
return this.sendMessage({
phoneNumber,
templateId: 'welcome-template-id',
templateName: 'welcome',
parameters: { name },
channels: ['whatsapp'],
});
}
async sendOrderConfirmation(
phoneNumber: string,
orderNumber: string,
total: string,
): Promise<MessageResponse> {
return this.sendMessage({
phoneNumber,
templateId: 'order-confirmation-id',
templateName: 'order_confirmation',
parameters: { order_number: orderNumber, total },
channels: ['sms', 'whatsapp'],
});
}
}DTOs with Validation
Use class-validator for input validation:
// messages/dto/send-message.dto.ts
import { IsString, IsOptional, IsObject, IsArray, ArrayMinSize } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class SendMessageDto {
@ApiProperty({ description: 'Phone number in E.164 format', example: '+1234567890' })
@IsString()
phoneNumber: string;
@ApiProperty({ description: 'Template ID', example: '7ba7b820-9dad-11d1-80b4-00c04fd430c8' })
@IsString()
templateId: string;
@ApiProperty({ description: 'Template name', example: 'welcome' })
@IsString()
templateName: string;
@ApiPropertyOptional({ description: 'Template parameters', example: { name: 'John' } })
@IsOptional()
@IsObject()
parameters?: Record<string, string>;
@ApiPropertyOptional({ description: 'Channels to use', example: ['whatsapp', 'sms'] })
@IsOptional()
@IsArray()
@ArrayMinSize(1)
@IsString({ each: true })
channels?: string[];
}// messages/dto/welcome-message.dto.ts
import { IsString, IsOptional } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class WelcomeMessageDto {
@ApiProperty({ description: 'Phone number in E.164 format', example: '+1234567890' })
@IsString()
phoneNumber: string;
@ApiPropertyOptional({ description: 'Customer name', example: 'John Doe' })
@IsOptional()
@IsString()
name?: string;
}Controller
REST API controller with proper error handling:
// messages/messages.controller.ts
import {
Controller,
Post,
Body,
HttpCode,
HttpStatus,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
import { MessagesService, MessageResponse } from './messages.service';
import { SendMessageDto } from './dto/send-message.dto';
import { WelcomeMessageDto } from './dto/welcome-message.dto';
@ApiTags('Messages')
@Controller('api/messages')
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
export class MessagesController {
constructor(private readonly messagesService: MessagesService) {}
@Post('send')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Send a message using a template' })
@ApiResponse({ status: 200, description: 'Message sent successfully' })
@ApiResponse({ status: 400, description: 'Invalid request' })
@ApiResponse({ status: 401, description: 'Authentication failed' })
async sendMessage(@Body() dto: SendMessageDto): Promise<MessageResponse> {
return this.messagesService.sendMessage(dto);
}
@Post('welcome')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Send a welcome message' })
@ApiResponse({ status: 200, description: 'Welcome message sent successfully' })
async sendWelcome(@Body() dto: WelcomeMessageDto): Promise<MessageResponse> {
return this.messagesService.sendWelcomeMessage(
dto.phoneNumber,
dto.name || 'Valued Customer',
);
}
}// messages/messages.module.ts
import { Module } from '@nestjs/common';
import { MessagesController } from './messages.controller';
import { MessagesService } from './messages.service';
@Module({
controllers: [MessagesController],
providers: [MessagesService],
exports: [MessagesService],
})
export class MessagesModule {}Exception Filter
Global exception handling for Sent SDK errors:
// common/filters/sent-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Response } from 'express';
import SentDm from '@sentdm/sentdm';
@Catch(SentDm.APIError)
export class SentExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(SentExceptionFilter.name);
catch(exception: SentDm.APIError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
this.logger.error(
`Sent API Error: ${exception.name}`,
JSON.stringify({
status: exception.status,
message: exception.message,
headers: exception.headers,
}),
);
const statusCode = this.mapErrorStatus(exception);
response.status(statusCode).json({
error: exception.name,
message: exception.message,
statusCode,
});
}
private mapErrorStatus(error: SentDm.APIError): number {
switch (error.status) {
case 400:
return HttpStatus.BAD_REQUEST;
case 401:
return HttpStatus.UNAUTHORIZED;
case 403:
return HttpStatus.FORBIDDEN;
case 404:
return HttpStatus.NOT_FOUND;
case 422:
return HttpStatus.UNPROCESSABLE_ENTITY;
case 429:
return HttpStatus.TOO_MANY_REQUESTS;
default:
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
}Apply the filter globally or at the controller level:
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SentExceptionFilter } from './common/filters/sent-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new SentExceptionFilter());
await app.listen(3000);
}
bootstrap();Webhook Handler
Handle incoming webhooks with signature verification:
// webhooks/webhooks.controller.ts
import {
Controller,
Post,
Headers,
Body,
UnauthorizedException,
BadRequestException,
Inject,
RawBody,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import SentDm from '@sentdm/sentdm';
import { SENT_CLIENT } from '../sent/sent.module';
import { Logger } from '@nestjs/common';
interface WebhookEvent {
type: string;
data: {
id: string;
status?: string;
error?: {
message: string;
};
};
}
@ApiTags('Webhooks')
@Controller('webhooks')
export class WebhooksController {
private readonly logger = new Logger(WebhooksController.name);
constructor(
@Inject(SENT_CLIENT) private readonly sentClient: SentDm,
private readonly configService: ConfigService,
) {}
@Post('sent')
@ApiOperation({ summary: 'Handle Sent webhooks' })
async handleWebhook(
@Headers('x-webhook-signature') signature: string,
@RawBody() rawBody: Buffer,
): Promise<{ received: boolean }> {
if (!signature) {
throw new UnauthorizedException('Missing webhook signature');
}
const webhookSecret = this.configService.get<string>('SENT_DM_WEBHOOK_SECRET');
if (!webhookSecret) {
throw new BadRequestException('Webhook secret not configured');
}
// Verify signature (implementation depends on SDK capabilities)
// For now, we parse and handle the event
let event: WebhookEvent;
try {
event = JSON.parse(rawBody.toString()) as WebhookEvent;
} catch {
throw new BadRequestException('Invalid JSON payload');
}
await this.handleEvent(event);
return { received: true };
}
private async handleEvent(event: WebhookEvent): Promise<void> {
this.logger.log(`Processing webhook event: ${event.type}`);
switch (event.type) {
case 'message.status.updated':
this.logger.log(`Message ${event.data.id} status: ${event.data.status}`);
// Update database, notify user, etc.
break;
case 'message.delivered':
this.logger.log(`Message ${event.data.id} delivered`);
break;
case 'message.failed':
this.logger.error(
`Message ${event.data.id} failed: ${event.data.error?.message}`,
);
// Handle failure - retry, notify, etc.
break;
default:
this.logger.warn(`Unhandled event type: ${event.type}`);
}
}
}Enable raw body parsing for webhook signature verification:
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SentExceptionFilter } from './common/filters/sent-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
rawBody: true,
});
app.useGlobalFilters(new SentExceptionFilter());
await app.listen(3000);
}
bootstrap();Testing
Unit testing with Jest and mocking:
// messages/messages.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { MessagesService } from './messages.service';
import { SENT_CLIENT } from '../sent/sent.module';
import SentDm from '@sentdm/sentdm';
const mockSentClient = {
messages: {
send: jest.fn(),
},
};
describe('MessagesService', () => {
let service: MessagesService;
let sentClient: jest.Mocked<SentDm>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
MessagesService,
{
provide: SENT_CLIENT,
useValue: mockSentClient,
},
],
}).compile();
service = module.get<MessagesService>(MessagesService);
sentClient = module.get(SENT_CLIENT);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('sendMessage', () => {
it('should send a message successfully', async () => {
const mockResponse = {
data: {
messages: [
{
id: 'msg_123',
status: 'pending',
},
],
},
};
mockSentClient.messages.send.mockResolvedValue(mockResponse);
const result = await service.sendMessage({
phoneNumber: '+1234567890',
templateId: 'welcome-template',
templateName: 'welcome',
parameters: { name: 'John' },
});
expect(result).toEqual({
messageId: 'msg_123',
status: 'pending',
});
expect(mockSentClient.messages.send).toHaveBeenCalledWith({
to: ['+1234567890'],
template: {
id: 'welcome-template',
name: 'welcome',
parameters: { name: 'John' },
},
channels: undefined,
});
});
it('should throw error when API call fails', async () => {
const apiError = new SentDm.APIError(
400,
'BadRequestError',
'Invalid phone number',
{},
);
mockSentClient.messages.send.mockRejectedValue(apiError);
await expect(
service.sendMessage({
phoneNumber: 'invalid',
templateId: 'welcome-template',
templateName: 'welcome',
}),
).rejects.toThrow(SentDm.APIError);
});
});
describe('sendWelcomeMessage', () => {
it('should send welcome message with correct parameters', async () => {
const mockResponse = {
data: {
messages: [
{
id: 'msg_456',
status: 'queued',
},
],
},
};
mockSentClient.messages.send.mockResolvedValue(mockResponse);
const result = await service.sendWelcomeMessage('+1234567890', 'Jane');
expect(result.messageId).toBe('msg_456');
expect(mockSentClient.messages.send).toHaveBeenCalledWith(
expect.objectContaining({
to: ['+1234567890'],
template: expect.objectContaining({
name: 'welcome',
parameters: { name: 'Jane' },
}),
channels: ['whatsapp'],
}),
);
});
});
});Dependency Graph
The recommended module structure for NestJS applications:
src/
├── app.module.ts
├── main.ts
├── sent/
│ ├── sent.module.ts # Dynamic module with provider
│ └── sent.types.ts # TypeScript interfaces
├── messages/
│ ├── messages.module.ts # Feature module
│ ├── messages.controller.ts # REST endpoints
│ ├── messages.service.ts # Business logic
│ └── dto/
│ ├── send-message.dto.ts
│ └── welcome-message.dto.ts
├── webhooks/
│ ├── webhooks.controller.ts # Webhook handlers
│ └── webhooks.module.ts
└── common/
└── filters/
└── sent-exception.filter.tsEnvironment Variables
# .env
SENT_DM_API_KEY=your_api_key_here
SENT_DM_WEBHOOK_SECRET=your_webhook_secretNext Steps
- Learn about best practices for production deployments
- Set up webhooks for delivery status updates
- Explore the TypeScript SDK reference for advanced features