TypeScript SDK

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.ts

Module 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_secret

Service 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.ts

Environment Variables

# .env
SENT_DM_API_KEY=your_api_key_here
SENT_DM_WEBHOOK_SECRET=your_webhook_secret

Next Steps

On this page