How to Track Twilio SMS Delivery Status with NestJS: Complete Webhook Guide
Build production-ready SMS delivery tracking with Twilio webhooks and NestJS. Complete guide with status callbacks, signature validation, and database integration.
Meta Description: Build production-ready SMS delivery tracking with Twilio webhooks and NestJS. Complete guide with status callbacks, signature validation, and database integration.
This guide provides a complete walkthrough for building a production-ready system using NestJS and Node.js to send SMS messages via Twilio and reliably track their delivery status using Twilio's status callback webhooks. You'll learn how to implement SMS delivery tracking, handle webhook callbacks, validate Twilio signatures, and store message status updates in a database. You'll cover everything from initial project setup to deployment and verification, ensuring you have a robust SMS delivery monitoring solution.
You'll focus on creating a system that not only sends messages but also listens for status updates (like queued, sent, delivered, failed, undelivered) from Twilio, enabling features like real-time delivery confirmation, logging for auditing, and triggering subsequent actions based on message status.
What You'll Build: SMS Delivery Tracking System
Project Components:
A NestJS application with the following capabilities:
An API endpoint to trigger sending SMS messages via Twilio.
Integration with the Twilio Node.js helper library.
A dedicated webhook endpoint to receive message status updates from Twilio.
Secure handling of Twilio credentials and webhook requests.
(Optional but recommended) Storing message details and status updates in a database (using TypeORM and PostgreSQL).
Robust logging and error handling.
Problem Solved:
Standard SMS sending often operates on a "fire-and-forget" basis. This guide addresses the need for reliable delivery confirmation. Knowing if and when a message reaches the recipient is crucial for many applications, including notifications, alerts, two-factor authentication, and customer communication workflows. Tracking delivery status provides visibility and enables automated responses to delivery failures.
Technologies Used:
Node.js: The underlying JavaScript runtime environment. This guide uses Node.js v20 LTS (maintenance until April 2026) or v22 LTS (active LTS through October 2025, recommended for new projects).
NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications. This guide targets NestJS v11.x (latest as of January 2025). Its modular architecture and built-in features (dependency injection, decorators, etc.) make it ideal for structured applications.
Twilio: A cloud communications platform providing APIs for SMS, voice, video, and more. You'll use their Programmable Messaging API and Node.js helper library (v5.x latest version, providing TypeScript support, improved security, and 31% smaller bundle size).
TypeScript: Superset of JavaScript adding static typing, enhancing code quality and maintainability.
dotenv: For managing environment variables.
class-validator & class-transformer: For request data validation.
(Optional) TypeORM v0.3.27 & PostgreSQL: For database persistence with full PostgreSQL support.
(Development) ngrok: To expose the local development server to the internet for Twilio webhooks.
System Architecture:
sequenceDiagram
participant User as User/Client App
participant API as NestJS API (SMS Sending)
participant NestJS as NestJS Application
participant TwilioAPI as Twilio Messaging API
participant TwilioWebhook as Twilio Status Callback
participant NestJSCallback as NestJS API (Callback Handler)
participant DB as Database (Optional)
User->>API: POST /sms/send (to, body)
API->>NestJS: Trigger send SMS service
NestJS->>TwilioAPI: client.messages.create({ to, from, body, statusCallback: '/twilio/status' })
TwilioAPI-->>NestJS: message SID
NestJS-->>API: Acknowledge send request (with SID)
API-->>User: Success/Failure (with SID)
TwilioAPI->>TwilioWebhook: SMS Status Update (queued, sent, delivered, etc.)
TwilioWebhook->>NestJSCallback: POST /twilio/status (MessageSid, MessageStatus, etc.)
NestJSCallback->>NestJS: Validate Twilio Signature (Middleware)
alt Signature Valid
NestJSCallback->>DB: Log/Update Message Status (using MessageSid)
DB-->>NestJSCallback: DB Write Success
NestJSCallback-->>TwilioWebhook: HTTP 200 OK (or 204 No Content)
else Signature Invalid
NestJSCallback-->>TwilioWebhook: HTTP 403 Forbidden
end
Prerequisites:
Node.js v20 LTS (maintenance mode) or v22 LTS (active LTS, recommended) and npm/yarn installed. Note: Node.js v18 LTS support ends April 2025.
A Twilio account (Free Tier is sufficient to start). Find your Account SID and Auth Token in the Twilio Console.
A Twilio phone number with SMS capabilities.
Basic understanding of TypeScript, REST APIs, and NestJS concepts.
(Optional) PostgreSQL database running locally or accessible.
(Development) ngrok installed to expose your local server.
Final Outcome:
By the end of this guide, you'll have a fully functional NestJS application capable of sending SMS messages and reliably tracking their delivery status via Twilio webhooks, including optional database persistence and essential security measures.
How to Set Up Your NestJS Project for Twilio Integration
Initialize your NestJS project and set up the basic structure and dependencies for SMS delivery tracking.
Step 1: Create a new NestJS project
Open your terminal and run the NestJS CLI command:
Bash
# Install NestJS CLI if you haven't already (NestJS v11.x)
npm install -g @nestjs/cli
# Create the project (choose npm or yarn)
nest new nestjs-twilio-sms-delivery-status-callbacks
cd nestjs-twilio-sms-delivery-status-callbacks
Step 2: Install necessary dependencies
Install the Twilio helper library (v5.x), configuration management, validation pipes, and optionally TypeORM v0.3.27 for database interaction.
Step 4: Load Environment Variables using ConfigModule
Modify src/app.module.ts to load and validate environment variables globally.
TypeScript
// src/app.module.tsimport { Module } from'@nestjs/common';
import { ConfigModule } from'@nestjs/config';
import { AppController } from'./app.controller';
import { AppService } from'./app.service';
// Import other modules (like Twilio, SMS, TypeOrm) here later@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Makes ConfigModule available globallyenvFilePath: '.env', // Specify the env file path
}),
// Add other modules here
],
controllers: [AppController],
providers: [AppService],
})
exportclassAppModule {}
Step 5: Enable Validation Pipe Globally
Modify src/main.ts to automatically validate incoming request payloads using class-validator.
TypeScript
// src/main.tsimport { NestFactory } from'@nestjs/core';
import { AppModule } from'./app.module';
import { ValidationPipe } from'@nestjs/common';
import { ConfigService } from'@nestjs/config';
asyncfunctionbootstrap() {
const app = awaitNestFactory.create(AppModule);
const configService = app.get(ConfigService);
const port = configService.get<number>('APP_PORT', 3000); // Default to 3000// Enable global validation pipe
app.useGlobalPipes(newValidationPipe({
whitelist: true, // Strip properties not defined in DTOtransform: true, // Automatically transform payloads to DTO instancesforbidNonWhitelisted: true, // Throw error if extra properties are present
}));
await app.listen(port);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
Project Structure Explanation:
src/: Contains your application's source code.
src/main.ts: The application entry point, bootstrapping the NestJS app.
src/app.module.ts: The root module, organizing the application structure.
src/app.controller.ts / src/app.service.ts: Default controller and service (can be removed or repurposed).
.env: Stores sensitive configuration and credentials (ignored by Git).
nest-cli.json: NestJS CLI configuration.
tsconfig.json: TypeScript compiler options.
At this point, you have a basic NestJS project configured to load environment variables and validate incoming requests.
How to Implement SMS Sending with Twilio Service
Build the services and controllers for sending SMS messages and handling delivery status callbacks.
Step 1: Create a Twilio Module and Service
This encapsulates Twilio client initialization and interaction logic.
Bash
nest g module twilio
nest g service twilio/twilio --no-spec # No spec file for simplicity here
Step 2: Configure the Twilio Service
Inject ConfigService to access credentials and initialize the Twilio client.
TypeScript
// src/twilio/twilio.service.tsimport { Injectable, Logger, OnModuleInit } from'@nestjs/common';
import { ConfigService } from'@nestjs/config';
import twilio, { Twilio } from'twilio';
@Injectable()
exportclassTwilioServiceimplementsOnModuleInit {
privatereadonly logger = newLogger(TwilioService.name);
privateclient: Twilio;
privatetwilioPhoneNumber: string;
privateappBaseUrl: string;
constructor(privatereadonlyconfigService: ConfigService) {}
onModuleInit() {
const accountSid = this.configService.get<string>('TWILIO_ACCOUNT_SID');
const authToken = this.configService.get<string>('TWILIO_AUTH_TOKEN');
this.twilioPhoneNumber = this.configService.get<string>(
'TWILIO_PHONE_NUMBER',
);
this.appBaseUrl = this.configService.get<string>('APP_BASE_URL');
if (!accountSid || !authToken || !this.twilioPhoneNumber || !this.appBaseUrl) {
this.logger.error('Twilio credentials or Base URL not configured in .env');
thrownewError('Twilio credentials or Base URL are missing.');
}
this.client = twilio(accountSid, authToken);
this.logger.log('Twilio client initialized');
}
getClient(): Twilio {
returnthis.client;
}
getTwilioPhoneNumber(): string {
returnthis.twilioPhoneNumber;
}
// Method to construct the callback URLgetStatusCallbackUrl(): string {
// Ensure the base URL doesn't end with a slash and the path starts with oneconst baseUrl = this.appBaseUrl.endsWith('/') ? this.appBaseUrl.slice(0, -1) : this.appBaseUrl;
const path = '/twilio/status'; // Our designated callback endpoint pathreturn`${baseUrl}${path}`;
}
// Method to send SMSasyncsendSms(to: string, body: string): Promise<string> {
try {
const message = awaitthis.client.messages.create({
body: body,
from: this.twilioPhoneNumber,
to: to,
// Provide the URL for status updatesstatusCallback: this.getStatusCallbackUrl(),
// Optionally specify which events trigger the callback// statusCallbackEvent: ['queued', 'sent', 'failed', 'delivered', 'undelivered'],// Defaults to 'completed' status (delivered/undelivered/failed)
});
this.logger.log(`SMS sent to ${to}. SID: ${message.sid}`);
return message.sid; // Return the Message SID
} catch (error) {
this.logger.error(`Failed to send SMS to ${to}: ${error.message}`, error.stack);
// Re-throw or handle specific Twilio errorsthrownewError(`Twilio API Error: ${error.message}`);
}
}
// Note: Request validation is handled by TwilioRequestValidatorMiddleware (Section 7)
}
Step 3: Register the Twilio Module
Make the TwilioService available for injection. Add TwilioModule to the imports array in src/app.module.ts.
TypeScript
// src/twilio/twilio.module.tsimport { Module } from'@nestjs/common';
import { ConfigModule } from'@nestjs/config';
import { TwilioService } from'./twilio.service';
@Module({
imports: [ConfigModule], // Import ConfigModule if not global or needed hereproviders: [TwilioService],
exports: [TwilioService], // Export service for other modules to use
})
exportclassTwilioModule {}
This controller houses the endpoint that Twilio calls with status updates.
Bash
nest g controller twilio/twilio --flat --no-spec # Add controller to existing twilio module
Modify the generated src/twilio/twilio.controller.ts:
TypeScript
// src/twilio/twilio.controller.tsimport { Controller, Post, Req, Res, Body, Headers, HttpCode, HttpStatus, Logger } from'@nestjs/common';
import { Request, Response } from'express'; // Import express types// Import DB service later if needed@Controller('twilio') // Base path for Twilio related endpointsexportclassTwilioController {
privatereadonly logger = newLogger(TwilioController.name);
// Constructor for injecting services (like DB service later)constructor(// Inject services here, e.g., private readonly messageLogService: MessageLogService) {}
@Post('status') // Matches the statusCallback URL path@HttpCode(HttpStatus.NO_CONTENT) // Twilio expects 200 OK or 204 No Content on successhandleStatusCallback(@Body() body: any, // Body will contain status info (MessageSid, MessageStatus, etc.)@Headers('X-Twilio-Signature') twilioSignature: string, // Signature for validation (uses HMAC-SHA1 with AuthToken)@Req() request: Request, // Access underlying express request (needed for middleware/raw body later)@Res() response: Response// Access underlying express response) {
this.logger.log(`Received Twilio Status Callback for SID: ${body.MessageSid}, Status: ${body.MessageStatus}`);
this.logger.debug(`Callback Body: ${JSON.stringify(body)}`);
this.logger.debug(`Twilio Signature: ${twilioSignature}`);
// --- CRITICAL SECURITY WARNING ---// Validate the incoming request using the Twilio signature header (X-Twilio-Signature).// This signature is generated using HMAC-SHA1 with your AuthToken as the key.// Twilio strongly recommends using the official SDK's validateRequest method.// This ensures the request genuinely comes from Twilio and prevents unauthorized webhook calls.// Implement this using middleware in Section 7.// Until then, this endpoint is technically insecure if exposed publicly.// --- END WARNING ---// --- IMPORTANT: CALLBACK ORDERING ---// Twilio does NOT guarantee status callbacks arrive in the order they were sent.// Your callback handler must handle out-of-order events (e.g., receiving "delivered" before "sent").// Use timestamps and proper state management to handle this correctly.// --- END ORDERING NOTE ---const messageSid = body.MessageSid;
const messageStatus = body.MessageStatus;
const errorCode = body.ErrorCode; // Present ONLY if status is 'failed' or 'undelivered'const errorMessage = body.ErrorMessage; // Present ONLY if status is 'failed' or 'undelivered'// Process the status update (e.g., log, update database)// Add database interaction in Section 6.this.logger.log(`Processing status '${messageStatus}' for message ${messageSid}. ErrorCode: ${errorCode || 'N/A'}`);
// Example: Update database (implement MessageLogService later)// try {// await this.messageLogService.updateStatus(messageSid, messageStatus, errorCode, errorMessage);// this.logger.log(`Successfully updated status for ${messageSid} in DB.`);// } catch (error) {// this.logger.error(`Failed to update DB for ${messageSid}: ${error.message}`, error.stack);// // Decide how to handle DB errors. Maybe retry? Log critical error?// // For now, still respond 204 to Twilio to acknowledge receipt.// }// Respond to Twilio – IMPORTANT: Respond quickly (within 15 seconds)// NestJS handles sending the response automatically with @HttpCode(204)// If you need more control, use the @Res() decorator and response.status(204).send();
}
}
Update src/twilio/twilio.module.ts to include the controller:
Receiving Callbacks (POST /twilio/status):
This endpoint is called by Twilio. To test locally:
Start ngrok:ngrok http 3000 (replace 3000 if your app uses a different port).
Copy the ngrok URL: Note the https:// URL provided (e.g., https://abcd-1234.ngrok-free.app).
Update .env: Set APP_BASE_URL to your ngrok URL.
Restart your NestJS app: To pick up the new APP_BASE_URL.
Send an SMS: Use the /sms/send endpoint again.
Monitor Logs: Check your NestJS application logs. You should see entries from TwilioController logging the incoming callback data (MessageSid, MessageStatus, etc.) as Twilio updates the status.
Monitor ngrok: The ngrok web interface (http://localhost:4040) shows incoming requests to your /twilio/status endpoint.
Expected Callback Body (Example for delivered status):
JSON
{"SmsSid":"SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","SmsStatus":"delivered","MessageStatus":"delivered","To":"+15559876543","MessageSid":"SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","AccountSid":"ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","From":"+15551234567","ApiVersion":"2010-04-01"// … other fields possible}
Expected Callback Body (Example for failed status):
JSON
{"SmsSid":"SMzzzzzzzzzzzzzzzzzzzzzzzzzzzzz","SmsStatus":"failed","MessageStatus":"failed","To":"+15551112222","MessageSid":"SMzzzzzzzzzzzzzzzzzzzzzzzzzzzzz","AccountSid":"ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","ErrorCode":"30006",// Example: Landline or unreachable carrier"ErrorMessage":"The destination number is unable to receive this message. Potential reasons could include trying to reach a landline telephone or the destination carrier blocking the message.","From":"+15551234567","ApiVersion":"2010-04-01"// … other fields possible}
How to Configure Twilio Webhooks for Status Updates
You've already integrated the twilio library. This section summarizes the key configuration points for SMS delivery tracking.
Credentials:TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN are sourced from .env via ConfigService and used to initialize the Twilio client in TwilioService. Obtain these from your Twilio Console Dashboard.
Phone Number:TWILIO_PHONE_NUMBER is sourced from .env and used as the from number when sending SMS. Ensure this number is SMS-capable and matches the one in your Twilio account.
Status Callback URL: The statusCallback URL is dynamically constructed in TwilioService using APP_BASE_URL from .env and the fixed path /twilio/status. This URL is passed in the client.messages.create call.
Why per-message? Setting statusCallback during the API call provides flexibility, allowing different message types or workflows to potentially use different callback handlers if needed, though you use a single one here.
Dashboard Configuration: While Twilio allows setting a general messaging webhook URL on a phone number or Messaging Service in the console (for incoming messages), the statusCallback parameter in the API call overrides any console settings for outgoing message status updates. Rely solely on the API parameter here.
Status Callback Events: By default, Twilio sends callbacks for "completed" status (delivered, undelivered, or failed). You can specify statusCallbackEvent parameter with an array like ['queued', 'sent', 'failed', 'delivered', 'undelivered'] to receive callbacks for intermediate states. Note: Status callbacks may arrive out of order due to network conditions and processing delays.
Polling Requirement: If a message status hasn't been updated to delivered or undelivered within 12 hours, make a polling request to the Programmable Messaging API to retrieve the status based on the Message SID. It's possible the status callback wasn't received due to network issues.
Environment Variables:
TWILIO_ACCOUNT_SID: Your main account identifier. Found on the Twilio Console dashboard. Format: ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
TWILIO_AUTH_TOKEN: Your secret API key used for both authentication and webhook signature validation (HMAC-SHA1). Found on the Twilio Console dashboard. Keep this secret. Format: Alphanumeric string.
TWILIO_PHONE_NUMBER: The E.164 formatted Twilio number you're sending from. Format: +15551234567.
APP_BASE_URL: The public base URL where your application is accessible by Twilio. During development, this is your ngrok HTTPS URL. In production, it's your deployed application's domain. Format: https://your-domain.com or https://sub.ngrok-free.app.
How to Handle Errors and Implement Retry Logic
Robust applications need proper error handling and logging for SMS delivery monitoring.
Error Handling Strategy:
Validation Errors: Handled globally by ValidationPipe in main.ts. Invalid requests return 400 Bad Request automatically.
Twilio API Errors: Caught within TwilioService (sendSms method). Logged with details. Currently re-thrown as generic Error, which NestJS maps to 500 Internal Server Error. Consider mapping specific Twilio error codes (e.g., authentication failure → 401/403, invalid number → 400) using custom exception filters for more precise client feedback.
Callback Processing Errors: Errors during database updates or other logic in handleStatusCallback should be logged. Crucially, still respond 2xx to Twilio to acknowledge receipt, preventing Twilio from retrying the callback unnecessarily for your internal processing errors. Handle internal failures separately (e.g., dead-letter queue, alerts).
Security Errors: Signature validation failures (implemented in Section 7) should return 403 Forbidden.
ErrorCode Field: The ErrorCode field only appears in callback events for failed or undelivered messages. Ensure your callback handler checks for this field but doesn't require it, as it will be absent for successful deliveries.
Logging:
Use NestJS's built-in Logger.
Log key events: Application start, Twilio client init, incoming requests, SMS sending attempts (success/failure), received callbacks, database operations (success/failure), and any caught errors.
Include contextual information like MessageSid in logs related to callbacks.
Levels: Use log for general info, warn for potential issues, error for failures, debug for verbose development info. Control log levels based on environment (e.g., debug in dev, log or info in prod). Configure NestJS logger levels during app bootstrap.
Retry Mechanisms:
Twilio Callbacks: Twilio automatically retries sending status callbacks if your endpoint doesn't respond with 2xx within 15 seconds or returns a 5xx error. Retries happen with exponential backoff. This is why validating quickly and responding 2xx (even if internal processing fails later) is important.
Sending SMS: If the initial client.messages.create call fails due to network issues or temporary Twilio problems, you might implement retries within TwilioService. Libraries like nestjs-retry or simple loop/delay logic can be used. Implement with caution (e.g., limit retries, use exponential backoff) to avoid excessive calls or costs. For this guide, keep it simple and re-throw the error.
Callback Ordering: Because Twilio doesn't guarantee callback ordering, implement your status update logic to handle receiving callbacks out of sequence. Use timestamp comparison and idempotent database updates to ensure data consistency.
Testing Error Scenarios:
Invalid Input: Send requests to /sms/send with missing/invalid to or body fields to test ValidationPipe.
Twilio Auth Error: Temporarily use incorrect TWILIO_AUTH_TOKEN in .env and try sending SMS. Expect a 500 error from /sms/send and corresponding logs in TwilioService.
Invalid Recipient: Send SMS to a known invalid number (like +15005550001 for invalid number error, or +15005550004 for SMS queue full, see Twilio Test Credentials) to trigger failed status callbacks.
Callback Handler Error: Introduce a deliberate error (e.g., throw new Error('Test DB Error');) inside handleStatusCallbackbefore the response is sent (once DB logic is added). Observe the logs. Twilio should retry the callback (visible in ngrok/server logs). Ensure you still log the incoming callback data.
Signature Validation Error: (Once implemented in Section 7) Send a POST request to /twilio/statuswithout a valid X-Twilio-Signature header. Expect a 403 Forbidden response.
How to Store SMS Status in PostgreSQL Database
Storing message status provides persistence and enables analysis or UI updates. Use TypeORM v0.3.27 with PostgreSQL.
Step 1: Install DB Dependencies (if not done)
Bash
npm install @nestjs/typeorm typeorm@^0.3.27 pg
Important Note: TypeORM doesn't follow semantic versioning. Upgrading from 0.3.26 to 0.3.27 may include breaking changes. Always test upgrades in a non-production environment first.
Step 2: Configure TypeORM (Initial Setup)
Update src/app.module.ts to configure the database connection using ConfigService. At this stage, don't add the specific entity yet.
TypeScript
// src/app.module.tsimport { Module } from'@nestjs/common';
import { ConfigModule, ConfigService } from'@nestjs/config';
import { TypeOrmModule } from'@nestjs/typeorm';
import { AppController } from'./app.controller';
import { AppService } from'./app.service';
import { TwilioModule } from'./twilio/twilio.module';
import { SmsModule } from'./sms/sms.module';
// Do NOT import MessageLog or MessageLogModule yet@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env',
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'postgres',
host: configService.get<string>('DB_HOST'),
port: configService.get<number>('DB_PORT'),
username: configService.get<string>('DB_USERNAME'),
password: configService.get<string>('DB_PASSWORD'),
database: configService.get<string>('DB_DATABASE'),
// entities: [__dirname + '/../**/*.entity{.ts,.js}'], // Alternative: Use path matching for auto-discoveryentities: [], // Add MessageLog entity explicitly latersynchronize: true, // DEV ONLY: Automatically creates schema. Disable in prod and use migrations.logging: configService.get<string>('NODE_ENV') === 'development', // Log SQL in dev
}),
inject: [ConfigService],
}),
TwilioModule,
SmsModule,
// MessageLogModule will be added later
],
controllers: [AppController],
providers: [AppService],
})
exportclassAppModule {}
Step 3: Create MessageLog Entity and Module
Define the structure of your database table.
Bash
nest g module message-log
nest g service message-log/message-log --no-spec
Create the entity file:
TypeScript
// src/message-log/message-log.entity.tsimport { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from'typeorm';
exportenumMessageTransportStatus {
ACCEPTED = 'accepted', // Initial status upon API acceptanceQUEUED = 'queued',
SENDING = 'sending',
SENT = 'sent',
FAILED = 'failed',
DELIVERED = 'delivered',
UNDELIVERED = 'undelivered',
RECEIVING = 'receiving', // For incoming messages, if trackedRECEIVED = 'received', // For incoming messages, if trackedREAD = 'read', // If read receipts are implemented (e.g., WhatsApp)UNKNOWN = 'unknown',
}
@Entity('message_logs') // Table nameexportclassMessageLog {
@PrimaryGeneratedColumn('uuid')
id: string;
@Index() // Index for faster lookups@Column({ type: 'varchar', length: 34, unique: true }) // Twilio SIDs are typically 34 chars (SM…)messageSid: string;
@Column({ type: 'varchar', length: 20 })
to: string;
@Column({ type: 'varchar', length: 20 })
from: string;
@Column({ type: 'text', nullable: true })
body?: string; // Body might not be relevant for all logs, or could be large@Column({
type: 'enum',
enum: MessageTransportStatus,
default: MessageTransportStatus.ACCEPTED,
})
status: MessageTransportStatus;
@Column({ type: 'varchar', length: 5, nullable: true }) // E.g., 30006errorCode?: string;
@Column({ type: 'text', nullable: true })
errorMessage?: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
Frequently asked questions
Track Twilio SMS delivery status using status callback webhooks. Set up a webhook endpoint in your NestJS application that receives real-time updates from Twilio on the message status (e.g., sent, delivered, failed). This allows for immediate feedback and automated actions based on the delivery outcome.
A Twilio status callback webhook is a URL you provide to Twilio where Twilio sends real-time updates about the status of your messages. Each time a message's status changes, Twilio makes an HTTP POST request to your specified URL with details like the message SID and status.
Reliable SMS delivery tracking is essential for various applications like two-factor authentication, notifications, and customer communication workflows. It ensures you know whether messages reach recipients, allowing you to take appropriate action if a message fails, like resending via another channel.
Use a status callback for Twilio whenever you need to confirm message delivery or respond to delivery failures. This is crucial for time-sensitive information, essential notifications, and any situation where confirming successful receipt is vital.
Yes, ngrok is recommended for local Twilio webhook development. Ngrok provides a public URL that tunnels requests to your locally running application, allowing Twilio to reach your webhook endpoint even during development before deployment.
The "fire-and-forget" SMS approach refers to sending messages without actively tracking their delivery status. This guide improves upon this method by implementing a system to monitor delivery and respond accordingly, ensuring reliability.
Set up a NestJS project for Twilio SMS by installing the necessary packages such as twilio, dotenv, @nestjs/config, class-validator, and class-transformer. Initialize a Twilio client using your Account SID and Auth Token from the Twilio Console.
This guide uses Node.js, NestJS, the Twilio Node.js helper library, TypeScript, and dotenv. Optionally, it incorporates TypeORM and PostgreSQL for persistent storage of message logs, and ngrok for local development.
Twilio webhook requests need to be validated using the X-Twilio-Signature header. This header contains a cryptographic signature that ensures the request originates from Twilio. This security measure will be implemented later in the guide. Until then, the endpoint is not secure if exposed publicly.
TypeORM and PostgreSQL are optionally used for persistent storage of message logs and their delivery statuses. This facilitates analysis and allows building dashboards or automated actions based on historical data.
Handle Twilio SMS delivery failures by logging detailed error information received in status callbacks. Implement retry mechanisms with caution to avoid excessive calls to the Twilio API and potential additional costs.
Prerequisites include Node.js v16 or later, a Twilio account (free tier suffices), a Twilio phone number with SMS capabilities, basic TypeScript and REST API knowledge, and optionally PostgreSQL and ngrok.
NestJS is the underlying Node.js framework used to build the application due to its structured approach, dependency injection, and built-in features like decorators, creating a maintainable and scalable architecture.
A Twilio status callback provides crucial information such as MessageSid, MessageStatus (e.g. 'sent', 'delivered', 'failed'), ErrorCode (if applicable), and ErrorMessage, which gives you detailed insight into message delivery outcomes.
Your Twilio Account SID and Auth Token can be found in the Twilio Console Dashboard. These credentials are essential for initializing the Twilio client and making API calls.
Meta Description: Build production-ready SMS delivery tracking with Twilio webhooks and NestJS. Complete guide with status callbacks, signature validation, and database integration.
This guide provides a complete walkthrough for building a production-ready system using NestJS and Node.js to send SMS messages via Twilio and reliably track their delivery status using Twilio's status callback webhooks. You'll learn how to implement SMS delivery tracking, handle webhook callbacks, validate Twilio signatures, and store message status updates in a database. You'll cover everything from initial project setup to deployment and verification, ensuring you have a robust SMS delivery monitoring solution.
You'll focus on creating a system that not only sends messages but also listens for status updates (like
queued,sent,delivered,failed,undelivered) from Twilio, enabling features like real-time delivery confirmation, logging for auditing, and triggering subsequent actions based on message status.What You'll Build: SMS Delivery Tracking System
Project Components:
A NestJS application with the following capabilities:
Problem Solved:
Standard SMS sending often operates on a "fire-and-forget" basis. This guide addresses the need for reliable delivery confirmation. Knowing if and when a message reaches the recipient is crucial for many applications, including notifications, alerts, two-factor authentication, and customer communication workflows. Tracking delivery status provides visibility and enables automated responses to delivery failures.
Technologies Used:
System Architecture:
Prerequisites:
Final Outcome:
By the end of this guide, you'll have a fully functional NestJS application capable of sending SMS messages and reliably tracking their delivery status via Twilio webhooks, including optional database persistence and essential security measures.
How to Set Up Your NestJS Project for Twilio Integration
Initialize your NestJS project and set up the basic structure and dependencies for SMS delivery tracking.
Step 1: Create a new NestJS project
Open your terminal and run the NestJS CLI command:
# Install NestJS CLI if you haven't already (NestJS v11.x) npm install -g @nestjs/cli # Create the project (choose npm or yarn) nest new nestjs-twilio-sms-delivery-status-callbacks cd nestjs-twilio-sms-delivery-status-callbacksStep 2: Install necessary dependencies
Install the Twilio helper library (v5.x), configuration management, validation pipes, and optionally TypeORM v0.3.27 for database interaction.
# Core dependencies npm install twilio@^5.0.0 dotenv @nestjs/config class-validator class-transformer # Optional database dependencies (using PostgreSQL with TypeORM 0.3.27) npm install @nestjs/typeorm typeorm@^0.3.27 pgStep 3: Configure Environment Variables
Create a
.envfile in the project root. Never commit this file to version control. Add a.gitignoreentry for.env.Create a
.env.examplefile to track necessary variables:Step 4: Load Environment Variables using ConfigModule
Modify
src/app.module.tsto load and validate environment variables globally.// 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 other modules (like Twilio, SMS, TypeOrm) here later @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Makes ConfigModule available globally envFilePath: '.env', // Specify the env file path }), // Add other modules here ], controllers: [AppController], providers: [AppService], }) export class AppModule {}Step 5: Enable Validation Pipe Globally
Modify
src/main.tsto automatically validate incoming request payloads usingclass-validator.// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); const port = configService.get<number>('APP_PORT', 3000); // Default to 3000 // Enable global validation pipe app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip properties not defined in DTO transform: true, // Automatically transform payloads to DTO instances forbidNonWhitelisted: true, // Throw error if extra properties are present })); await app.listen(port); console.log(`Application is running on: ${await app.getUrl()}`); } bootstrap();Project Structure Explanation:
src/: Contains your application's source code.src/main.ts: The application entry point, bootstrapping the NestJS app.src/app.module.ts: The root module, organizing the application structure.src/app.controller.ts/src/app.service.ts: Default controller and service (can be removed or repurposed)..env: Stores sensitive configuration and credentials (ignored by Git).nest-cli.json: NestJS CLI configuration.tsconfig.json: TypeScript compiler options.At this point, you have a basic NestJS project configured to load environment variables and validate incoming requests.
How to Implement SMS Sending with Twilio Service
Build the services and controllers for sending SMS messages and handling delivery status callbacks.
Step 1: Create a Twilio Module and Service
This encapsulates Twilio client initialization and interaction logic.
nest g module twilio nest g service twilio/twilio --no-spec # No spec file for simplicity hereStep 2: Configure the Twilio Service
Inject
ConfigServiceto access credentials and initialize the Twilio client.// src/twilio/twilio.service.ts import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import twilio, { Twilio } from 'twilio'; @Injectable() export class TwilioService implements OnModuleInit { private readonly logger = new Logger(TwilioService.name); private client: Twilio; private twilioPhoneNumber: string; private appBaseUrl: string; constructor(private readonly configService: ConfigService) {} onModuleInit() { const accountSid = this.configService.get<string>('TWILIO_ACCOUNT_SID'); const authToken = this.configService.get<string>('TWILIO_AUTH_TOKEN'); this.twilioPhoneNumber = this.configService.get<string>( 'TWILIO_PHONE_NUMBER', ); this.appBaseUrl = this.configService.get<string>('APP_BASE_URL'); if (!accountSid || !authToken || !this.twilioPhoneNumber || !this.appBaseUrl) { this.logger.error('Twilio credentials or Base URL not configured in .env'); throw new Error('Twilio credentials or Base URL are missing.'); } this.client = twilio(accountSid, authToken); this.logger.log('Twilio client initialized'); } getClient(): Twilio { return this.client; } getTwilioPhoneNumber(): string { return this.twilioPhoneNumber; } // Method to construct the callback URL getStatusCallbackUrl(): string { // Ensure the base URL doesn't end with a slash and the path starts with one const baseUrl = this.appBaseUrl.endsWith('/') ? this.appBaseUrl.slice(0, -1) : this.appBaseUrl; const path = '/twilio/status'; // Our designated callback endpoint path return `${baseUrl}${path}`; } // Method to send SMS async sendSms(to: string, body: string): Promise<string> { try { const message = await this.client.messages.create({ body: body, from: this.twilioPhoneNumber, to: to, // Provide the URL for status updates statusCallback: this.getStatusCallbackUrl(), // Optionally specify which events trigger the callback // statusCallbackEvent: ['queued', 'sent', 'failed', 'delivered', 'undelivered'], // Defaults to 'completed' status (delivered/undelivered/failed) }); this.logger.log(`SMS sent to ${to}. SID: ${message.sid}`); return message.sid; // Return the Message SID } catch (error) { this.logger.error(`Failed to send SMS to ${to}: ${error.message}`, error.stack); // Re-throw or handle specific Twilio errors throw new Error(`Twilio API Error: ${error.message}`); } } // Note: Request validation is handled by TwilioRequestValidatorMiddleware (Section 7) }Step 3: Register the Twilio Module
Make the
TwilioServiceavailable for injection. AddTwilioModuleto theimportsarray insrc/app.module.ts.// src/twilio/twilio.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TwilioService } from './twilio.service'; @Module({ imports: [ConfigModule], // Import ConfigModule if not global or needed here providers: [TwilioService], exports: [TwilioService], // Export service for other modules to use }) export class TwilioModule {}// 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 { TwilioModule } from './twilio/twilio.module'; // Import TwilioModule // Import other modules (SMS, TypeOrm) here later @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', }), TwilioModule, // Add TwilioModule here // Add other modules (SMS, TypeOrm) later ], controllers: [AppController], providers: [AppService], }) export class AppModule {}You now have a dedicated service to handle Twilio interactions.
How to Create API Endpoints for SMS and Webhook Callbacks
Create endpoints to trigger sending SMS and to receive the status callbacks from Twilio.
Step 1: Create an SMS Module and Controller
This module handles the API endpoint for sending messages.
Step 2: Define Data Transfer Object (DTO) for Sending SMS
Create a DTO to define the expected request body structure and apply validation rules.
// src/sms/dto/send-sms.dto.ts import { IsNotEmpty, IsString, IsPhoneNumber } from 'class-validator'; export class SendSmsDto { @IsNotEmpty() @IsPhoneNumber() // Validates E.164 format (e.g., +15551234567) readonly to: string; @IsNotEmpty() @IsString() readonly body: string; }Step 3: Implement the SMS Sending Endpoint
Inject
TwilioServiceintoSmsControllerand create a POST endpoint.// src/sms/sms.controller.ts import { Controller, Post, Body, HttpCode, HttpStatus, Logger } from '@nestjs/common'; import { TwilioService } from '../twilio/twilio.service'; import { SendSmsDto } from './dto/send-sms.dto'; @Controller('sms') export class SmsController { private readonly logger = new Logger(SmsController.name); constructor(private readonly twilioService: TwilioService) {} @Post('send') @HttpCode(HttpStatus.ACCEPTED) // Use 202 Accepted as sending is asynchronous async sendSms(@Body() sendSmsDto: SendSmsDto): Promise<{ message: string; sid: string }> { this.logger.log(`Received request to send SMS to ${sendSmsDto.to}`); try { const messageSid = await this.twilioService.sendSms(sendSmsDto.to, sendSmsDto.body); return { message: 'SMS send request accepted.', sid: messageSid, }; } catch (error) { this.logger.error(`Error in sendSms endpoint: ${error.message}`, error.stack); // Consider throwing specific HTTP exceptions based on error type throw error; // Re-throw for NestJS default exception handling } } }Step 4: Register the SMS Module
Add
SmsModuletosrc/app.module.ts.// src/sms/sms.module.ts import { Module } from '@nestjs/common'; import { TwilioModule } from '../twilio/twilio.module'; // Import TwilioModule import { SmsController } from './sms.controller'; @Module({ imports: [TwilioModule], // Make TwilioService available controllers: [SmsController], }) export class SmsModule {}// 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 { TwilioModule } from './twilio/twilio.module'; import { SmsModule } from './sms/sms.module'; // Import SmsModule // Import other modules (TypeOrm) here later @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', }), TwilioModule, SmsModule, // Add SmsModule here // Add other modules (TypeOrm) later ], controllers: [AppController], providers: [AppService], }) export class AppModule {}Step 5: Create the Twilio Callback Controller
This controller houses the endpoint that Twilio calls with status updates.
nest g controller twilio/twilio --flat --no-spec # Add controller to existing twilio moduleModify the generated
src/twilio/twilio.controller.ts:// src/twilio/twilio.controller.ts import { Controller, Post, Req, Res, Body, Headers, HttpCode, HttpStatus, Logger } from '@nestjs/common'; import { Request, Response } from 'express'; // Import express types // Import DB service later if needed @Controller('twilio') // Base path for Twilio related endpoints export class TwilioController { private readonly logger = new Logger(TwilioController.name); // Constructor for injecting services (like DB service later) constructor( // Inject services here, e.g., private readonly messageLogService: MessageLogService ) {} @Post('status') // Matches the statusCallback URL path @HttpCode(HttpStatus.NO_CONTENT) // Twilio expects 200 OK or 204 No Content on success handleStatusCallback( @Body() body: any, // Body will contain status info (MessageSid, MessageStatus, etc.) @Headers('X-Twilio-Signature') twilioSignature: string, // Signature for validation (uses HMAC-SHA1 with AuthToken) @Req() request: Request, // Access underlying express request (needed for middleware/raw body later) @Res() response: Response // Access underlying express response ) { this.logger.log(`Received Twilio Status Callback for SID: ${body.MessageSid}, Status: ${body.MessageStatus}`); this.logger.debug(`Callback Body: ${JSON.stringify(body)}`); this.logger.debug(`Twilio Signature: ${twilioSignature}`); // --- CRITICAL SECURITY WARNING --- // Validate the incoming request using the Twilio signature header (X-Twilio-Signature). // This signature is generated using HMAC-SHA1 with your AuthToken as the key. // Twilio strongly recommends using the official SDK's validateRequest method. // This ensures the request genuinely comes from Twilio and prevents unauthorized webhook calls. // Implement this using middleware in Section 7. // Until then, this endpoint is technically insecure if exposed publicly. // --- END WARNING --- // --- IMPORTANT: CALLBACK ORDERING --- // Twilio does NOT guarantee status callbacks arrive in the order they were sent. // Your callback handler must handle out-of-order events (e.g., receiving "delivered" before "sent"). // Use timestamps and proper state management to handle this correctly. // --- END ORDERING NOTE --- const messageSid = body.MessageSid; const messageStatus = body.MessageStatus; const errorCode = body.ErrorCode; // Present ONLY if status is 'failed' or 'undelivered' const errorMessage = body.ErrorMessage; // Present ONLY if status is 'failed' or 'undelivered' // Process the status update (e.g., log, update database) // Add database interaction in Section 6. this.logger.log(`Processing status '${messageStatus}' for message ${messageSid}. ErrorCode: ${errorCode || 'N/A'}`); // Example: Update database (implement MessageLogService later) // try { // await this.messageLogService.updateStatus(messageSid, messageStatus, errorCode, errorMessage); // this.logger.log(`Successfully updated status for ${messageSid} in DB.`); // } catch (error) { // this.logger.error(`Failed to update DB for ${messageSid}: ${error.message}`, error.stack); // // Decide how to handle DB errors. Maybe retry? Log critical error? // // For now, still respond 204 to Twilio to acknowledge receipt. // } // Respond to Twilio – IMPORTANT: Respond quickly (within 15 seconds) // NestJS handles sending the response automatically with @HttpCode(204) // If you need more control, use the @Res() decorator and response.status(204).send(); } }Update
src/twilio/twilio.module.tsto include the controller:// src/twilio/twilio.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TwilioService } from './twilio.service'; import { TwilioController } from './twilio.controller'; // Import the controller @Module({ imports: [ConfigModule], providers: [TwilioService], exports: [TwilioService], controllers: [TwilioController], // Add the controller here }) export class TwilioModule {}Testing API Endpoints:
Sending SMS (
POST /sms/send):Use
curlor Postman:curl -X POST http://localhost:3000/sms/send \ -H 'Content-Type: application/json' \ -d '{ "to": "+15559876543", "body": "Hello from NestJS Twilio Guide!" }'(Replace
+15559876543with a real number verified on your Twilio trial account)Expected Response (Status: 202 Accepted):
{ "message": "SMS send request accepted.", "sid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }Receiving Callbacks (
POST /twilio/status): This endpoint is called by Twilio. To test locally:ngrok http 3000(replace 3000 if your app uses a different port).https://URL provided (e.g.,https://abcd-1234.ngrok-free.app)..env: SetAPP_BASE_URLto your ngrok URL.APP_BASE_URL./sms/sendendpoint again.TwilioControllerlogging the incoming callback data (MessageSid,MessageStatus, etc.) as Twilio updates the status.http://localhost:4040) shows incoming requests to your/twilio/statusendpoint.Expected Callback Body (Example for
deliveredstatus):{ "SmsSid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "SmsStatus": "delivered", "MessageStatus": "delivered", "To": "+15559876543", "MessageSid": "SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "AccountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "From": "+15551234567", "ApiVersion": "2010-04-01" // … other fields possible }Expected Callback Body (Example for
failedstatus):{ "SmsSid": "SMzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "SmsStatus": "failed", "MessageStatus": "failed", "To": "+15551112222", "MessageSid": "SMzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", "AccountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "ErrorCode": "30006", // Example: Landline or unreachable carrier "ErrorMessage": "The destination number is unable to receive this message. Potential reasons could include trying to reach a landline telephone or the destination carrier blocking the message.", "From": "+15551234567", "ApiVersion": "2010-04-01" // … other fields possible }How to Configure Twilio Webhooks for Status Updates
You've already integrated the
twiliolibrary. This section summarizes the key configuration points for SMS delivery tracking.TWILIO_ACCOUNT_SIDandTWILIO_AUTH_TOKENare sourced from.envviaConfigServiceand used to initialize the Twilio client inTwilioService. Obtain these from your Twilio Console Dashboard.TWILIO_PHONE_NUMBERis sourced from.envand used as thefromnumber when sending SMS. Ensure this number is SMS-capable and matches the one in your Twilio account.statusCallbackURL is dynamically constructed inTwilioServiceusingAPP_BASE_URLfrom.envand the fixed path/twilio/status. This URL is passed in theclient.messages.createcall.statusCallbackduring the API call provides flexibility, allowing different message types or workflows to potentially use different callback handlers if needed, though you use a single one here.statusCallbackparameter in the API call overrides any console settings for outgoing message status updates. Rely solely on the API parameter here.statusCallbackEventparameter with an array like['queued', 'sent', 'failed', 'delivered', 'undelivered']to receive callbacks for intermediate states. Note: Status callbacks may arrive out of order due to network conditions and processing delays.TWILIO_ACCOUNT_SID: Your main account identifier. Found on the Twilio Console dashboard. Format:ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.TWILIO_AUTH_TOKEN: Your secret API key used for both authentication and webhook signature validation (HMAC-SHA1). Found on the Twilio Console dashboard. Keep this secret. Format: Alphanumeric string.TWILIO_PHONE_NUMBER: The E.164 formatted Twilio number you're sending from. Format:+15551234567.APP_BASE_URL: The public base URL where your application is accessible by Twilio. During development, this is your ngrok HTTPS URL. In production, it's your deployed application's domain. Format:https://your-domain.comorhttps://sub.ngrok-free.app.How to Handle Errors and Implement Retry Logic
Robust applications need proper error handling and logging for SMS delivery monitoring.
Error Handling Strategy:
ValidationPipeinmain.ts. Invalid requests return 400 Bad Request automatically.TwilioService(sendSmsmethod). Logged with details. Currently re-thrown as genericError, which NestJS maps to 500 Internal Server Error. Consider mapping specific Twilio error codes (e.g., authentication failure → 401/403, invalid number → 400) using custom exception filters for more precise client feedback.handleStatusCallbackshould be logged. Crucially, still respond 2xx to Twilio to acknowledge receipt, preventing Twilio from retrying the callback unnecessarily for your internal processing errors. Handle internal failures separately (e.g., dead-letter queue, alerts).ErrorCodefield only appears in callback events for failed or undelivered messages. Ensure your callback handler checks for this field but doesn't require it, as it will be absent for successful deliveries.Logging:
Logger.MessageSidin logs related to callbacks.logfor general info,warnfor potential issues,errorfor failures,debugfor verbose development info. Control log levels based on environment (e.g.,debugin dev,logorinfoin prod). Configure NestJS logger levels during app bootstrap.Retry Mechanisms:
client.messages.createcall fails due to network issues or temporary Twilio problems, you might implement retries withinTwilioService. Libraries likenestjs-retryor simple loop/delay logic can be used. Implement with caution (e.g., limit retries, use exponential backoff) to avoid excessive calls or costs. For this guide, keep it simple and re-throw the error.Testing Error Scenarios:
/sms/sendwith missing/invalidtoorbodyfields to testValidationPipe.TWILIO_AUTH_TOKENin.envand try sending SMS. Expect a 500 error from/sms/sendand corresponding logs inTwilioService.+15005550001for invalid number error, or+15005550004for SMS queue full, see Twilio Test Credentials) to triggerfailedstatus callbacks.throw new Error('Test DB Error');) insidehandleStatusCallbackbefore the response is sent (once DB logic is added). Observe the logs. Twilio should retry the callback (visible in ngrok/server logs). Ensure you still log the incoming callback data./twilio/statuswithout a validX-Twilio-Signatureheader. Expect a 403 Forbidden response.How to Store SMS Status in PostgreSQL Database
Storing message status provides persistence and enables analysis or UI updates. Use TypeORM v0.3.27 with PostgreSQL.
Step 1: Install DB Dependencies (if not done)
Important Note: TypeORM doesn't follow semantic versioning. Upgrading from 0.3.26 to 0.3.27 may include breaking changes. Always test upgrades in a non-production environment first.
Step 2: Configure TypeORM (Initial Setup)
Update
src/app.module.tsto configure the database connection usingConfigService. At this stage, don't add the specific entity yet.// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TwilioModule } from './twilio/twilio.module'; import { SmsModule } from './sms/sms.module'; // Do NOT import MessageLog or MessageLogModule yet @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', }), TypeOrmModule.forRootAsync({ imports: [ConfigModule], useFactory: (configService: ConfigService) => ({ type: 'postgres', host: configService.get<string>('DB_HOST'), port: configService.get<number>('DB_PORT'), username: configService.get<string>('DB_USERNAME'), password: configService.get<string>('DB_PASSWORD'), database: configService.get<string>('DB_DATABASE'), // entities: [__dirname + '/../**/*.entity{.ts,.js}'], // Alternative: Use path matching for auto-discovery entities: [], // Add MessageLog entity explicitly later synchronize: true, // DEV ONLY: Automatically creates schema. Disable in prod and use migrations. logging: configService.get<string>('NODE_ENV') === 'development', // Log SQL in dev }), inject: [ConfigService], }), TwilioModule, SmsModule, // MessageLogModule will be added later ], controllers: [AppController], providers: [AppService], }) export class AppModule {}Step 3: Create MessageLog Entity and Module
Define the structure of your database table.
Create the entity file:
// src/message-log/message-log.entity.ts import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm'; export enum MessageTransportStatus { ACCEPTED = 'accepted', // Initial status upon API acceptance QUEUED = 'queued', SENDING = 'sending', SENT = 'sent', FAILED = 'failed', DELIVERED = 'delivered', UNDELIVERED = 'undelivered', RECEIVING = 'receiving', // For incoming messages, if tracked RECEIVED = 'received', // For incoming messages, if tracked READ = 'read', // If read receipts are implemented (e.g., WhatsApp) UNKNOWN = 'unknown', } @Entity('message_logs') // Table name export class MessageLog { @PrimaryGeneratedColumn('uuid') id: string; @Index() // Index for faster lookups @Column({ type: 'varchar', length: 34, unique: true }) // Twilio SIDs are typically 34 chars (SM…) messageSid: string; @Column({ type: 'varchar', length: 20 }) to: string; @Column({ type: 'varchar', length: 20 }) from: string; @Column({ type: 'text', nullable: true }) body?: string; // Body might not be relevant for all logs, or could be large @Column({ type: 'enum', enum: MessageTransportStatus, default: MessageTransportStatus.ACCEPTED, }) status: MessageTransportStatus; @Column({ type: 'varchar', length: 5, nullable: true }) // E.g., 30006 errorCode?: string; @Column({ type: 'text', nullable: true }) errorMessage?: string; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; }