This guide provides a complete walkthrough for building a robust two-way SMS messaging system using NestJS, Node.js, and the Vonage Messages API. We'll cover everything from initial project setup to deployment and monitoring, enabling you to send outbound SMS messages and reliably receive inbound messages via webhooks.
This implementation solves the common need for applications to interact with users via SMS for notifications, alerts, verification, or conversational experiences. By leveraging NestJS, we gain a structured, scalable, and maintainable backend architecture. Vonage provides the communication infrastructure for reliable SMS delivery and reception.
Technology Stack:
- Node.js: JavaScript runtime environment.
- NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications.
- Vonage Messages API: Enables sending and receiving SMS (and other channels) programmatically.
@vonage/server-sdk
: The official Vonage Node.js SDK.@nestjs/config
: For managing environment variables.ngrok
: To expose local development server for webhook testing.- (Optional) Prisma: For database interactions (e.g., logging messages).
- (Optional) Docker: For containerizing the application.
System Architecture:
+-----------------+ +---------------------+ +-----------------+ +-----------------+
| User's Phone | <--->| Vonage Platform | <--->| Your NestJS App| ---> | (Optional) DB |
| (Sends/Receives)| | (SMS Gateway, API) | | (API, Webhooks) | | (Message Logs) |
+-----------------+ +---------------------+ +-----------------+ +-----------------+
| ^ | ^
| (Outbound SMS) | | (Inbound SMS Webhook) |
+------------------------+ +------------------------+
Prerequisites:
- Node.js (LTS version recommended) and npm/yarn installed.
- A Vonage API account (Sign up here).
- Your Vonage Application ID and Private Key file (generated via Vonage Dashboard).
- A Vonage virtual phone number capable of sending/receiving SMS, linked to your application.
ngrok
installed and authenticated (Download here).- Basic understanding of TypeScript and NestJS concepts.
- (Optional) Docker installed if you plan to containerize.
Final Outcome:
By the end of this guide, you will have a NestJS application capable of:
- Sending SMS messages via a simple API endpoint.
- Receiving inbound SMS messages via a webhook endpoint.
- Securely managing Vonage credentials.
- Basic logging and error handling for SMS operations.
- (Optional) Storing message history in a database.
- Ready for deployment with considerations for security and testing.
1. Setting up the NestJS Project
We'll start by creating a new NestJS project and setting up the basic structure and dependencies.
1. Install NestJS CLI (if you haven't already):
npm install -g @nestjs/cli
2. Create a new NestJS Project:
nest new vonage-sms-app
cd vonage-sms-app
Choose your preferred package manager (npm or yarn) when prompted.
3. Install Necessary Dependencies:
We need the Vonage SDK and NestJS config module.
# Using npm
npm install @vonage/server-sdk @nestjs/config class-validator class-transformer
# Using yarn
yarn add @vonage/server-sdk @nestjs/config class-validator class-transformer
@vonage/server-sdk
: The official SDK for interacting with Vonage APIs.@nestjs/config
: Handles environment variables gracefully.class-validator
&class-transformer
: Used for validating incoming request data (like webhook payloads or API requests).
4. Configure Environment Variables:
Managing sensitive credentials like API keys is crucial. We'll use a .env
file for local development.
-
Create a
.env
file in the project root:touch .env
-
Add the following variables to your
.env
file. Obtain these values from your Vonage Dashboard (Applications -> Your Application):# .env # Vonage Credentials VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID VONAGE_PRIVATE_KEY_PATH=./private.key # Path relative to project root VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # The number sending/receiving SMS # Application Port PORT=3000
VONAGE_APPLICATION_ID
: Found on your Vonage Application page.VONAGE_PRIVATE_KEY_PATH
: The path to theprivate.key
file you downloaded when creating the Vonage Application. Copy theprivate.key
file into your project's root directory. Ensure this path is correct. Never commit your private key to version control.VONAGE_NUMBER
: The Vonage virtual number linked to your application. Use E.164 format (e.g.,14155550100
).PORT
: The port your NestJS application will listen on.
-
Important Security Note: Add
.env
andprivate.key
to your.gitignore
file immediately to prevent accidentally committing secrets:# .gitignore # dependencies /node_modules /dist # env .env private.key # Add this line! # logs npm-debug.log* yarn-debug.log* yarn-error.log*
5. Integrate ConfigModule:
Load the environment variables into your NestJS application using ConfigModule
.
-
Modify
src/app.module.ts
:// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; // Import ConfigModule import { AppController } from './app.controller'; import { AppService } from './app.service'; import { VonageModule } from './vonage/vonage.module'; // We will create this next // Import PrismaModule if using Prisma and it's set to Global // import { PrismaModule } from './prisma/prisma.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Make ConfigModule globally available envFilePath: '.env', // Specify the env file path }), VonageModule, // Import our upcoming Vonage module // PrismaModule, // Include if using Prisma ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' })
: This line initializes the configuration module, makes it available application-wide, and tells it to load variables from the.env
file.
Project Structure Rationale:
Using NestJS CLI provides a standard, modular structure (src
, test
, configuration files). We will create dedicated modules (VonageModule
) for specific functionalities (like interacting with Vonage) to keep the codebase organized and maintainable, following NestJS best practices. Environment variables are managed centrally via @nestjs/config
for security and flexibility across different environments (development, staging, production).
2. Implementing Core Functionality (Vonage Service)
We'll create a dedicated module and service to encapsulate all interactions with the Vonage SDK.
1. Generate the Vonage Module and Service:
Use the NestJS CLI to generate the necessary files:
nest generate module vonage
nest generate service vonage --no-spec # We'll add tests later
This creates a src/vonage
directory with vonage.module.ts
and vonage.service.ts
.
2. Implement the VonageService:
This service will initialize the Vonage SDK and provide methods for sending SMS. Note: If using Prisma (Section 6), ensure PrismaService
is injected here.
-
Edit
src/vonage/vonage.service.ts
:// src/vonage/vonage.service.ts import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Vonage } from '@vonage/server-sdk'; import { MessageSendRequest } from '@vonage/messages'; import * as fs from 'fs'; // Import Node.js fs module import { PrismaService } from '../prisma/prisma.service'; // Import if using Prisma @Injectable() export class VonageService implements OnModuleInit { private readonly logger = new Logger(VonageService.name); private vonageClient: Vonage; private vonageNumber: string; constructor( private configService: ConfigService, // Inject PrismaService if you are using the optional database logging (Section 6) // Make sure PrismaModule is imported in AppModule and PrismaService is exported/global private readonly prismaService?: PrismaService, // Make optional if Prisma is optional ) {} onModuleInit() { const applicationId = this.configService.get<string>('VONAGE_APPLICATION_ID'); const privateKeyPath = this.configService.get<string>('VONAGE_PRIVATE_KEY_PATH'); this.vonageNumber = this.configService.get<string>('VONAGE_NUMBER'); if (!applicationId || !privateKeyPath || !this.vonageNumber) { this.logger.error('Vonage credentials missing in environment variables.'); throw new Error('Vonage credentials missing.'); } try { // Read the private key file content const privateKey = fs.readFileSync(privateKeyPath); this.vonageClient = new Vonage({ applicationId: applicationId, privateKey: privateKey, // Pass the key content, not the path }); this.logger.log('Vonage Client Initialized Successfully.'); } catch (error) { this.logger.error(`Failed to initialize Vonage Client: ${error.message}`, error.stack); throw error; // Re-throw to prevent application startup if Vonage fails } } async sendSms(to: string, text: string): Promise<string | null> { const messageRequest: MessageSendRequest = { message_type: 'text', to: to, from: this.vonageNumber, channel: 'sms', text: text, }; let messageUuid: string | null = null; try { const response = await this.vonageClient.messages.send(messageRequest); messageUuid = response.message_uuid; this.logger.log(`SMS sent successfully to ${to}, message_uuid: ${messageUuid}`); // Optional: Log to database (See Section 6) // Ensure PrismaService is injected and available before using this.prismaService if (messageUuid && this.prismaService) { this.prismaService.smsMessage.create({ data: { messageUuid: messageUuid, direction: 'OUTBOUND', fromNumber: this.vonageNumber, toNumber: to, text: text, timestamp: new Date(), // Time sent initiated status: 'submitted', // Initial status }, }).catch(err => this.logger.error('Failed to save outbound message to DB', err?.stack || err)); } // End Optional DB Log return messageUuid; } catch (error) { this.logger.error(`Failed to send SMS to ${to}: ${error?.message || error}`, error?.response?.data || error?.stack); // Depending on requirements, you might want to throw the error // or handle it gracefully (e.g., return null, queue for retry). // For this example, we log and return null. return null; } } // We will add methods for handling inbound messages later if needed, // but the primary handling will be in a controller. }
OnModuleInit
: Ensures the Vonage client is initialized when the module loads.ConfigService
: Injected to retrieve environment variables securely.PrismaService
: Injected (conditionally, if using DB logging from Section 6). Made optional in constructor.- Error Handling: Checks for missing credentials and catches errors during client initialization and message sending.
fs.readFileSync
: Reads the content of the private key file specified byVONAGE_PRIVATE_KEY_PATH
. The SDK expects the key content, not the file path directly.sendSms
Method:- Constructs the
MessageSendRequest
object required by the SDK. - Uses
this.vonageClient.messages.send()
to dispatch the SMS. - Logs success or failure, returning the
message_uuid
on success ornull
on failure. - Includes optional Prisma logging logic (checks if
this.prismaService
exists before using).
- Constructs the
3. Update VonageModule:
Make the VonageService
available for injection elsewhere in the application.
-
Edit
src/vonage/vonage.module.ts
:// src/vonage/vonage.module.ts import { Module } from '@nestjs/common'; import { VonageService } from './vonage.service'; import { ConfigModule } from '@nestjs/config'; // Import ConfigModule // No need to import PrismaModule here if it's marked as @Global in AppModule // and PrismaService is exported from PrismaModule. @Module({ imports: [ConfigModule], // Import ConfigModule here as VonageService depends on it providers: [VonageService], exports: [VonageService], // Export VonageService so other modules can use it }) export class VonageModule {}
Now, any other module that imports VonageModule
can inject VonageService
.
3. Building the API Layer (Sending and Receiving SMS)
We need endpoints to trigger sending SMS and to receive inbound SMS webhooks from Vonage.
1. Generate a Controller:
Let's create a controller to handle SMS-related HTTP requests.
nest generate controller sms --no-spec
This creates src/sms/sms.controller.ts
.
2. Create Data Transfer Objects (DTOs):
DTOs define the expected shape of request bodies and enable validation using class-validator
.
-
Create
src/sms/dto
directory if it doesn't exist. -
Create
src/sms/dto/send-sms.dto.ts
:// src/sms/dto/send-sms.dto.ts import { IsNotEmpty, IsString, IsPhoneNumber } from 'class-validator'; export class SendSmsDto { @IsNotEmpty() @IsPhoneNumber(null) // Use null for generic phone number validation (adjust region if needed) @IsString() to: string; // E.164 format recommended (e.g., +14155550100) @IsNotEmpty() @IsString() text: string; }
-
Create
src/sms/dto/inbound-sms.dto.ts
:// src/sms/dto/inbound-sms.dto.ts import { IsString, IsNotEmpty, IsOptional, IsEnum, ValidateNested, IsDateString } from 'class-validator'; import { Type } from 'class-transformer'; // Based on Vonage Messages API webhook format for inbound SMS // Ref: https://developer.vonage.com/en/messages/concepts/inbound-sms#webhook-format enum MessageType { TEXT = 'text', IMAGE = 'image', AUDIO = 'audio', VIDEO = 'video', FILE = 'file', VCARD = 'vcard', LOCATION = 'location', TEMPLATE = 'template', CUSTOM = 'custom', UNSUPPORTED = 'unsupported', // Added for robustness } class UsageDto { @IsOptional() @IsString() currency?: string; @IsOptional() @IsString() // Price might be a string representation price?: string; } class SmsInfoDto { @IsOptional() @IsString() num_messages?: string; // Often comes as a string } export class InboundSmsDto { @IsNotEmpty() @IsString() message_uuid: string; @IsNotEmpty() @IsString() to: string; // Your Vonage number @IsNotEmpty() @IsString() from: string; // Sender's number @IsNotEmpty() @IsDateString() timestamp: string; @IsNotEmpty() @IsEnum(MessageType) message_type: MessageType; @IsOptional() @IsString() text?: string; // Only present for message_type 'text' @IsOptional() @IsString() keyword?: string; // If applicable @IsNotEmpty() @IsString() channel: 'sms'; // Hardcoded for SMS focus // Optional fields based on Vonage documentation @IsOptional() @ValidateNested() @Type(() => UsageDto) usage?: UsageDto; @IsOptional() @ValidateNested() @Type(() => SmsInfoDto) sms?: SmsInfoDto; // Add other fields if needed (e.g., for MMS: image, audio, video URLs) }
-
Create
src/sms/dto/sms-status.dto.ts
:// src/sms/dto/sms-status.dto.ts import { IsString, IsNotEmpty, IsOptional, IsEnum, IsDateString, IsUUID } from 'class-validator'; // Based on common Vonage Delivery Receipt (DLR) format via Status Webhook // Ref: https://developer.vonage.com/en/messages/concepts/delivery-receipts#dlr-format export enum MessageStatus { SUBMITTED = 'submitted', DELIVERED = 'delivered', EXPIRED = 'expired', FAILED = 'failed', REJECTED = 'rejected', ACCEPTED = 'accepted', // intermediate state BUFFERED = 'buffered', // intermediate state UNKNOWN = 'unknown', // default/fallback READ = 'read', // For channels supporting read receipts UNDELIVERABLE = 'undeliverable', // Common failure reason } export class SmsStatusDto { @IsNotEmpty() @IsUUID() message_uuid: string; @IsNotEmpty() @IsString() to: string; // Recipient number @IsNotEmpty() @IsString() from: string; // Your Vonage number @IsNotEmpty() @IsDateString() timestamp: string; // Timestamp of the status update @IsNotEmpty() @IsEnum(MessageStatus) status: MessageStatus; @IsOptional() @IsString() client_ref?: string; // If you included one in the outbound request @IsOptional() @IsString() // Often a numeric string error_code?: string; @IsOptional() @IsString() error_code_label?: string; // Human-readable error label // Add other potentially useful fields if needed // e.g., network_code, message_price, currency }
3. Implement the SMS Controller:
-
Edit
src/sms/sms.controller.ts
:// src/sms/sms.controller.ts import { Controller, Post, Body, HttpCode, HttpStatus, Logger, ValidationPipe, UsePipes } from '@nestjs/common'; import { VonageService } from '../vonage/vonage.service'; import { SendSmsDto } from './dto/send-sms.dto'; import { InboundSmsDto } from './dto/inbound-sms.dto'; import { SmsStatusDto } from './dto/sms-status.dto'; // Import the new DTO import { PrismaService } from '../prisma/prisma.service'; // Import if using Prisma @Controller('sms') export class SmsController { private readonly logger = new Logger(SmsController.name); constructor( private readonly vonageService: VonageService, // Inject PrismaService if needed for direct DB access in controller // Make sure PrismaModule is imported in AppModule and PrismaService is exported/global private readonly prismaService?: PrismaService, // Make optional if Prisma is optional ) {} /** * Endpoint to trigger sending an SMS. * POST /sms/send */ @Post('send') @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) @HttpCode(HttpStatus.OK) // Return 200 OK on success async sendSms(@Body() sendSmsDto: SendSmsDto): Promise<{ message: string; messageId?: string }> { this.logger.log(`Received request to send SMS to: ${sendSmsDto.to}`); // VonageService now handles DB logging internally if enabled and available const messageId = await this.vonageService.sendSms(sendSmsDto.to, sendSmsDto.text); if (messageId) { return { message: 'SMS sent successfully requested.', messageId: messageId }; } else { // Consider returning a more specific error status, e.g., HttpStatus.INTERNAL_SERVER_ERROR // For simplicity here, we stick to a basic response. // The VonageService already logged the detailed error. return { message: 'Failed to send SMS.' }; } } /** * Webhook endpoint for receiving inbound SMS messages from Vonage. * POST /sms/inbound */ @Post('inbound') @HttpCode(HttpStatus.OK) // Vonage expects a 200 OK to confirm receipt @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) // Validate incoming payload handleInboundSms(@Body() inboundSmsDto: InboundSmsDto): void { // Important: Respond immediately with 200 OK before processing. // Vonage has short timeouts and will retry if it doesn't get a quick 200. this.logger.log(`Received inbound SMS from ${inboundSmsDto.from} with message ID ${inboundSmsDto.message_uuid}`); this.logger.debug('Inbound SMS Payload:', JSON.stringify(inboundSmsDto, null, 2)); // --- Process the inbound message asynchronously --- // Avoid blocking the response to Vonage. // Example: Save the message to a database (using PrismaService) if (this.prismaService) { this.prismaService.smsMessage.create({ data: { messageUuid: inboundSmsDto.message_uuid, direction: 'INBOUND', fromNumber: inboundSmsDto.from, toNumber: inboundSmsDto.to, text: inboundSmsDto.text, // Handle cases where text might be missing timestamp: new Date(inboundSmsDto.timestamp), status: 'received', // Initial status for inbound vonageStatus: 'received', // Can align vonageStatus too }, }).catch(err => this.logger.error('Failed to save inbound message to DB', err?.stack || err)); } // Add other async processing: trigger workflows, queue jobs, etc. // No explicit return needed as NestJS handles the 200 OK due to @HttpCode } /** * Webhook endpoint for receiving SMS status updates (DLRs) from Vonage. * POST /sms/status */ @Post('status') @HttpCode(HttpStatus.OK) // Vonage expects 200 OK @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) // Validate DLR payload handleSmsStatus(@Body() statusPayload: SmsStatusDto): void { // Respond quickly this.logger.log(`Received DLR status '${statusPayload.status}' for message ID ${statusPayload.message_uuid}`); this.logger.debug('DLR Payload:', JSON.stringify(statusPayload, null, 2)); // Process the status update asynchronously // Example: Update message status in the database (using PrismaService) if (this.prismaService) { this.prismaService.smsMessage.update({ where: { messageUuid: statusPayload.message_uuid }, data: { status: statusPayload.status, // Use validated status enum value vonageStatus: statusPayload.status, // Store raw status too errorCode: statusPayload.error_code, // If present // You might want to parse statusPayload.timestamp here too updatedAt: new Date(), }, }).catch(err => { // Log error, maybe check if it's a non-critical error like 'message not found' this.logger.error(`Failed to update DLR status for ${statusPayload.message_uuid}: ${err?.message}`, err?.stack); }); } // Add other async processing based on status (e.g., trigger alert on 'failed') // No explicit return needed } }
SendSmsDto
,InboundSmsDto
,SmsStatusDto
: Imported and used for request body validation and type safety.ValidationPipe
: Applied to all endpoints receiving data (send
,inbound
,status
) to enforce DTO rules.handleInboundSms
&handleSmsStatus
: Respond immediately with200 OK
. Database operations (if using Prisma) are performed asynchronously (.catch()
handles errors without blocking the response). Checks ifthis.prismaService
exists.PrismaService
: Injected into the controller (conditionally, made optional) if needed for direct database access in webhook handlers.
4. Register the Controller:
Add the SmsController
to a module. We can add it to the main AppModule
or create a dedicated SmsModule
. Let's add it to AppModule
for simplicity.
-
Modify
src/app.module.ts
:// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { VonageModule } from './vonage/vonage.module'; import { SmsController } from './sms/sms.controller'; // Import SmsController // Import PrismaModule if using Prisma and it's set to Global // import { PrismaModule } from './prisma/prisma.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', }), VonageModule, // PrismaModule, // Include if using Prisma (ensure it's Global or imported here) ], controllers: [ AppController, SmsController, // Add SmsController here ], providers: [AppService], // PrismaService is available globally if PrismaModule is Global and exports it }) export class AppModule {}
4. Integrating with Vonage (Dashboard Configuration)
Now that the code is ready, we need to configure Vonage to send webhooks to our application.
1. Start Your Local Application:
npm run start:dev
Your NestJS app should be running, typically on http://localhost:3000
(or the PORT
specified in .env
).
2. Expose Your Localhost using ngrok:
Vonage needs a publicly accessible URL to send webhooks. Note: ngrok, especially the free tier with changing URLs, is primarily intended for development and testing. For production deployments, you will need a stable, public URL provided by your hosting platform or a static IP address.
ngrok http 3000 # Use the same port your NestJS app is running on
ngrok will provide a Forwarding
URL (e.g., https://abcdef123456.ngrok.io
). Copy the https
version. This URL forwards public internet traffic to your local application.
3. Configure Vonage Application Webhooks:
- Go to your Vonage API Dashboard.
- Find the Application you created (or create a new one).
- Click ""Edit"" next to your application.
- Capabilities: Ensure ""Messages"" is toggled ON.
- Webhooks:
- Inbound URL: Enter your ngrok
https
URL followed by the path to your inbound webhook endpoint:YOUR_NGROK_HTTPS_URL/sms/inbound
(e.g.,https://abcdef123456.ngrok.io/sms/inbound
) - Status URL: Enter your ngrok
https
URL followed by the path to your status webhook endpoint:YOUR_NGROK_HTTPS_URL/sms/status
(e.g.,https://abcdef123456.ngrok.io/sms/status
)
- Inbound URL: Enter your ngrok
- Link Virtual Number: Ensure your Vonage virtual number (
VONAGE_NUMBER
) is linked to this application at the bottom of the page. If not, link it. - Save Changes.
4. Set Default SMS API (Crucial):
Vonage has two SMS APIs. For the @vonage/server-sdk
's messages.send
and the webhook format we're using, you must set the Messages API as the default for your account.
- Go to your Vonage API Dashboard Settings.
- Under ""API settings"", find the ""Default SMS Setting"".
- Select ""Use the Messages API"".
- Click ""Save changes"".
Environment Variables Recap:
VONAGE_APPLICATION_ID
: (String) Your application's unique ID from the Vonage dashboard. Used by the SDK to identify your app.VONAGE_PRIVATE_KEY_PATH
: (String) The relative path from your project root to theprivate.key
file. Used by the SDK for JWT authentication when sending messages via the Messages API. Obtain by downloading the key when creating/editing the Vonage application.VONAGE_NUMBER
: (String) The E.164 formatted Vonage virtual number linked to your application. Used as thefrom
number when sending SMS and is the number users will text to. Purchase/manage in the Numbers section of the dashboard.PORT
: (Number) The local port your NestJS application listens on. Must match the port used in thengrok
command.
5. Error Handling, Logging, and Retry Mechanisms
Robustness comes from anticipating failures.
Error Handling:
- VonageService: The
sendSms
method includes atry...catch
block. It logs detailed errors usingthis.logger.error
, including the error message and potentially the response data from Vonage (error?.response?.data
). Note: The exact structure of theerror.response.data
object can vary depending on the specific Vonage API error, so it's wise to inspect it during testing to understand its format for different failure scenarios. Currently, it returnsnull
on failure. For critical messages, consider implementing retries or queuing (see below). - SmsController:
- Uses
ValidationPipe
to automatically reject requests with invalid data (e.g., missingto
number, invalid format), returning a400 Bad Request
. - The
handleInboundSms
andhandleSmsStatus
endpoints must respond with200 OK
quickly. Any errors during the asynchronous processing of the message/status (like DB writes) should be logged and handled without causing the endpoint to return an error (e.g., 500). Otherwise, Vonage will retry the webhook unnecessarily.
- Uses
- Global Exception Filter (Optional): For centralized error handling, implement a NestJS Exception Filter to catch unhandled exceptions, log them, and return standardized error responses.
Logging:
- NestJS's built-in
Logger
is used. Logs provide context, timestamps, and levels. - Consider redacting sensitive data (like full message text) in production logs or using appropriate log levels (
debug
vs.log
). - For advanced logging (structured JSON, better performance), consider
Pino
vianestjs-pino
.
Retry Mechanisms (Vonage Webhooks):
- Vonage automatically retries webhooks if they don't receive a
200 OK
quickly. Our immediate200 OK
response in handlers prevents most retries. - Ensure your processing logic is idempotent (can handle the same webhook multiple times without side effects). Check if
message_uuid
already exists in the DB before creating a new record.
Retry Mechanisms (Sending SMS):
- The current
sendSms
doesn't retry. For higher reliability:- Simple Retry: Implement a loop with delays within
sendSms
. - Exponential Backoff: Use increasing delays between retries (libraries like
async-retry
can help). - Job Queues (Recommended): Use BullMQ, RabbitMQ, etc. On failure in
sendSms
, add a job to a queue. A separate worker process handles sending, retries, and dead-lettering.
- Simple Retry: Implement a loop with delays within
6. Creating a Database Schema and Data Layer (Optional)
Storing message history using Prisma.
1. Install Prisma:
npm install prisma --save-dev
npm install @prisma/client
2. Initialize Prisma:
npx prisma init --datasource-provider postgresql # Or your preferred DB
Update DATABASE_URL
in .env
.
3. Define Schema:
-
Edit
prisma/schema.prisma
:// prisma/schema.prisma generator client { provider = ""prisma-client-js"" } datasource db { provider = ""postgresql"" // Or your chosen provider url = env(""DATABASE_URL"") } model SmsMessage { id String @id @default(uuid()) messageUuid String @unique // Vonage message_uuid direction Direction // INBOUND or OUTBOUND fromNumber String toNumber String text String? // Message content (nullable if not always text) status String? // Our internal status (e.g., submitted, received, delivered, failed) vonageStatus String? // Raw status from Vonage DLR (e.g., delivered, expired) errorCode String? // Error code from Vonage DLR if failed timestamp DateTime // Time received by Vonage (inbound/status) or sent initiated (outbound) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } enum Direction { INBOUND OUTBOUND }
(Note: The original text ended here. Further steps would involve creating a PrismaService, generating the client, running migrations, and integrating the service as shown optionally in previous code snippets.)