Send MMS with Twilio and NestJS: Complete Node.js Guide 2025
Build a production-ready NestJS application to send MMS messages using Twilio API. Step-by-step guide with TypeScript, validation, error handling, and deployment best practices.
Multimedia Messaging Service (MMS) allows you to enrich your user communication by sending images, GIFs, and other media alongside text. Integrating Twilio MMS capabilities into your NestJS applications can significantly enhance user engagement and provide richer context than Short Message Service (SMS) alone.
This guide provides a step-by-step walkthrough for building a robust NestJS application capable of sending MMS messages via the Twilio Programmable Messaging Application Programming Interface (API). You'll learn how to send multimedia messages with Node.js, implement proper validation, handle errors, and deploy a production-ready MMS messaging solution.
Project Overview and Goals
What You'll Build:
You will create a NestJS backend application featuring a dedicated API endpoint (/mms) that accepts requests to send MMS messages. This endpoint will securely interact with the Twilio API to deliver messages containing text and media (specified by publicly accessible Uniform Resource Locators (URLs)) to designated recipients.
Problem Solved:
This project provides a structured, scalable, and maintainable way to integrate Twilio MMS functionality into any NestJS-based system. It abstracts the complexities of direct Twilio API interaction behind a clean NestJS service and controller layer, incorporating best practices for configuration, validation, and error handling.
Technologies Used:
Node.js: The underlying JavaScript runtime environment (v20 or later recommended for 2025; v18 reached end-of-life).
NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications using TypeScript (v10+ requires Node.js v16+ and TypeScript v4.8+).
TypeScript: Superset of JavaScript adding static types.
Twilio: A cloud communications platform providing APIs for SMS, MMS, voice, video, and more. You'll use their Node.js helper library (v5.x supports Node.js v22).
dotenv: Module for loading environment variables from a .env file.
(Optional) Prisma: A modern database toolkit for TypeScript and Node.js (if message logging is desired).
(Optional) Docker: For containerizing the application for deployment.
System Architecture:
The basic flow involves a client (like a frontend app, Postman, or another service) making an Hypertext Transfer Protocol (HTTP) POST request to your NestJS API. The NestJS application validates the request, uses the Twilio Software Development Kit (SDK) (configured with credentials from environment variables) to call the Twilio Messaging API, which then handles the delivery of the MMS message to the end user's mobile device.
Node.js: Version 20.x or later installed (Node.js v18 reached end-of-life; Twilio Command-Line Interface (CLI) 6.0.0+ requires v20 or newer as of 2025). Verify with node -v.
npm or yarn: Package manager installed. Verify with npm -v or yarn -v.
Purchase or use an existing Twilio Phone Number with MMS capabilities. Important: Native MMS support is limited to US and Canada only (as of 2025). For international destinations outside US/Canada, Twilio automatically converts MMS to SMS with a media link via the MMS Converter feature. You can buy a number via the Phone Numbers section in the Console.
Code Editor: Such as Visual Studio (VS) Code.
Terminal/Command Prompt: For running commands.
(Optional) Git: For version control.
(Optional) Postman or curl: For testing the API endpoint.
How to Set Up Your NestJS Project for Twilio MMS
Initialize your NestJS project and install the necessary dependencies.
Create a New NestJS Project:
Open your terminal and run the NestJS CLI command to scaffold a new project. Replace nestjs-twilio-mms with your preferred project name.
Bash
nest new nestjs-twilio-mms
Choose your preferred package manager (npm or yarn) when prompted.
Navigate into Project Directory:
Bash
cd nestjs-twilio-mms
Install Dependencies:
Install the official Twilio Node.js helper library and dotenv for managing environment variables. You also need NestJS configuration and validation packages.
Bash
# Using npm
npm install twilio dotenv @nestjs/config class-validator class-transformer
# Using yarn
yarn add twilio dotenv @nestjs/config class-validator class-transformer
twilio: The official SDK for interacting with the Twilio API.
dotenv: Loads environment variables from a .env file into process.env.
@nestjs/config: Provides configuration management for NestJS applications.
class-validator & class-transformer: Used for request data validation via Data Transfer Objects (DTOs).
Configure Environment Variables:
Create a file named .env in the root directory of your project. This file will store sensitive credentials and configuration details securely. Never commit this file to version control.
Add a .gitignore entry if it doesn't exist:
Text
# .gitignore
node_modules
dist
.env
Populate the .env file with your Twilio credentials and phone number:
.env
# .env
# Twilio Credentials – Find these in your Twilio Console: https://console.twilio.com/
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token_xxxxxxxxxxxxxxx
# Twilio Phone Number – Must be an MMS-enabled number from your Twilio account
# Use E.164 format (e.g., +15551234567)
TWILIO_PHONE_NUMBER=+1xxxxxxxxxx
# Application Port (Optional)
PORT=3000
Replace the placeholder values (ACxxx…, your_auth_token…, +1xxx…) with your actual credentials and phone number.
Load Configuration in AppModule:
Modify src/app.module.ts to load the environment variables using @nestjs/config.
TypeScript
// src/app.module.tsimport { Module } from'@nestjs/common';
import { ConfigModule } from'@nestjs/config'; // Import ConfigModuleimport { AppController } from'./app.controller';
import { AppService } from'./app.service';
import { MmsModule } from'./mms/mms.module'; // You will create this soon@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Makes ConfigModule available globallyenvFilePath: '.env', // Specify the env file path (useful for local dev)// In production, environment variables are typically set directly
}),
MmsModule, // Import your MMS module
],
controllers: [AppController],
providers: [AppService],
})
exportclassAppModule {}
Setting isGlobal: true makes the ConfigService available throughout the application without needing to import ConfigModule in other modules.
How to Implement MMS Sending Functionality with Twilio SDK
Encapsulate the Twilio interaction logic within a dedicated NestJS service.
Generate the MMS Module and Service:
Use the NestJS CLI to generate a new module and service for MMS handling.
Bash
nest generate module mms
nest generate service mms --no-spec # Use --no-spec to skip generating test file for now
This creates src/mms/mms.module.ts and src/mms/mms.service.ts. The MmsModule is automatically imported into AppModule if you generated it after setting up the AppModule imports.
Implement the MmsService:
Open src/mms/mms.service.ts and implement the logic to send MMS messages using the Twilio client.
TypeScript
// src/mms/mms.service.tsimport { Injectable, Logger, InternalServerErrorException, BadRequestException } from'@nestjs/common';
import { ConfigService } from'@nestjs/config';
import * asTwiliofrom'twilio'; // Use wildcard import for Twilio namespaceimport { MessageInstance } from'twilio/lib/rest/api/v2010/account/message';
@Injectable()
exportclassMmsService {
privatereadonly logger = newLogger(MmsService.name);
privatetwilioClient: Twilio.Twilio; // Type the client correctlyprivatetwilioPhoneNumber: string;
constructor(privateconfigService: ConfigService) {
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');
if (!accountSid || !authToken || !this.twilioPhoneNumber) {
this.logger.error('Twilio configuration (Account SID, Auth Token, Phone Number) is missing or invalid.');
// Throw InternalServerErrorException because this is a server configuration issuethrownewInternalServerErrorException('Twilio configuration is missing or invalid.');
}
try {
this.twilioClient = Twilio(accountSid, authToken); // Initialize Twilio clientthis.logger.log('Twilio client initialized successfully.');
} catch (error) {
const errorMessage = error instanceofError ? error.message : String(error);
this.logger.error(`Failed to initialize Twilio client: ${errorMessage}`, error instanceofError ? error.stack : undefined);
thrownewInternalServerErrorException(`Failed to initialize Twilio client: ${errorMessage}`);
}
}
/**
* Sends an MMS message using Twilio.
* @param to The recipient's phone number in E.164 format (e.g., +15551234567).
* @param body The text body of the message.
* @param mediaUrls An array of publicly accessible URLs for the media attachments (up to 10, max 5 MB total).
* @returns A Promise resolving to the Twilio MessageInstance.
*/asyncsendMms(to: string, body: string, mediaUrls: string[]): Promise<MessageInstance> {
this.logger.log(`Attempting to send MMS to ${to} with ${mediaUrls.length} media items.`);
if (mediaUrls.length === 0) {
this.logger.warn('No media URLs provided. Sending as SMS instead.');
// Note: Twilio automatically sends as SMS if mediaUrl is empty/omitted,// but the DTO currently requires mediaUrls. Modify DTO if SMS fallback is desired.
}
if (mediaUrls.length > 10) {
this.logger.error(`Cannot send more than 10 media items. Provided: ${mediaUrls.length}`);
// Use BadRequestException as this is a client-provided data issuethrownewBadRequestException('Exceeded maximum number of media attachments (10). Twilio MMS supports up to 10 media files with a combined size limit of 5 MB.');
}
try {
const message = awaitthis.twilioClient.messages.create({
to: to,
from: this.twilioPhoneNumber,
body: body,
mediaUrl: mediaUrls, // Pass the array directly
});
this.logger.log(`MMS sent successfully! Message SID: ${message.sid}`);
return message;
} catch (error) {
const errorMessage = error instanceofError ? error.message : String(error);
this.logger.error(`Failed to send MMS to ${to}: ${errorMessage}`, error instanceofError ? error.stack : undefined);
// Consider mapping specific Twilio errors (e.g., 21211 invalid "to" number)// to different NestJS exceptions (e.g., BadRequestException) if needed.// For now, rethrow as InternalServerErrorException for simplicity.thrownewInternalServerErrorException(`Failed to send MMS via Twilio: ${errorMessage}`);
}
}
}
Dependency Injection: Inject ConfigService to safely access your environment variables.
Twilio Client Initialization: The Twilio client is initialized in the constructor using credentials from ConfigService. Checks ensure variables are present, throwing InternalServerErrorException on failure. The error message is more general now.
sendMms Method: This asynchronous method takes the recipient (to), message body (body), and an array of mediaUrls.
Media Limit Check: Uses BadRequestException if more than 10 media URLs are provided.
mediaUrl Parameter: The Twilio Node.js library accepts an array of strings for the mediaUrl parameter.
Error Handling: Basic logging uses NestJS's Logger. Errors during message sending are caught, logged, and rethrown as InternalServerErrorException. Specific Twilio errors could be handled more granularly. Improved error message extraction.
Logging: Key events like initialization, sending attempts, success, and failures are logged.
Provide ConfigService in MmsModule:
Ensure ConfigService is available within the MmsModule. Since you made ConfigModule global in AppModule, no extra import is needed here.
How to Create an MMS API Endpoint in NestJS
Create an API endpoint to trigger the MmsService.
Generate the MMS Controller:
Bash
nest generate controller mms --no-spec
This creates src/mms/mms.controller.ts.
Create a Data Transfer Object (DTO) for Validation:
DTOs define the expected shape of request data and enable automatic validation using class-validator. Create a file src/mms/dto/send-mms.dto.ts.
TypeScript
// src/mms/dto/send-mms.dto.tsimport { IsString, IsNotEmpty, IsPhoneNumber, IsArray, ArrayMaxSize, ArrayNotEmpty, IsUrl, MaxLength } from'class-validator';
exportclassSendMmsDto {
@IsPhoneNumber(null, { message: 'Recipient phone number must be a valid E.164 format string (e.g., +15551234567).' }) // Validates E.164 format@IsNotEmpty({ message: 'Recipient phone number (to) cannot be empty.' })
to: string;
@IsString()
@IsNotEmpty({ message: 'Message body cannot be empty.' })
@MaxLength(1600, { message: 'Message body cannot exceed 1600 characters. Twilio supports concatenated messages up to 1600 chars (split into 153-char segments for GSM-7 or 67-char segments for UCS-2/Unicode).'}) // Twilio's max concatenated message lengthbody: string;
@IsArray()
@ArrayNotEmpty({ message: 'At least one media URL must be provided for MMS.' })
@ArrayMaxSize(10, { message: 'Cannot attach more than 10 media items. Total size must not exceed 5 MB.' })
@IsUrl({}, { each: true, message: 'Each media URL must be a valid, publicly accessible URL.' }) // Validate each item in the array is a URLmediaUrls: string[];
}
Use decorators like @IsPhoneNumber, @IsString, @IsArray, @IsUrl, @ArrayMaxSize, etc., to define validation rules.
Messages provide user-friendly error feedback.
Implement the MmsController:
Open src/mms/mms.controller.ts and define the POST endpoint.
TypeScript
// src/mms/mms.controller.tsimport { Controller, Post, Body, HttpCode, HttpStatus, Logger } from'@nestjs/common';
import { MmsService } from'./mms.service';
import { SendMmsDto } from'./dto/send-mms.dto';
@Controller('mms') // Route prefix for this controllerexportclassMmsController {
privatereadonly logger = newLogger(MmsController.name);
constructor(privatereadonlymmsService: MmsService) {}
@Post() // Handles POST requests to /mms@HttpCode(HttpStatus.ACCEPTED) // Return 202 Accepted as sending is async// ValidationPipe is applied globally in main.tsasyncsendMms(@Body() sendMmsDto: SendMmsDto): Promise<{ messageSid: string; status: string }> {
this.logger.log(`Received request to send MMS to: ${sendMmsDto.to}`);
const { to, body, mediaUrls } = sendMmsDto;
// No try-catch needed here if you let exceptions propagate// NestJS global exception filters (or default handler) will catch errors// from the service or validation pipe.const message = awaitthis.mmsService.sendMms(to, body, mediaUrls);
this.logger.log(`MMS queued successfully for SID: ${message.sid}`);
// Return the SID and initial status (usually "queued" or "sending")return { messageSid: message.sid, status: message.status };
}
}
@Controller('mms'): Sets the base route path to /mms.
@Post(): Decorator for handling HTTP POST requests.
@Body(): Extracts the request body and implicitly validates/transforms it if a global pipe is enabled.
@HttpCode(HttpStatus.ACCEPTED): Sets the default response status code to 202 Accepted.
Dependency Injection:MmsService is injected.
Response: Returns the messageSid and initial status from Twilio.
Removed @UsePipes: Enable the ValidationPipe globally.
Enable Global Validation Pipe (Recommended):
Modify src/main.ts to enable the validation pipe for all incoming requests.
TypeScript
// src/main.tsimport { NestFactory } from'@nestjs/core';
import { AppModule } from'./app.module';
import { ValidationPipe, Logger } from'@nestjs/common'; // Import Loggerimport { ConfigService } from'@nestjs/config'; // Import ConfigServiceasyncfunctionbootstrap() {
const app = awaitNestFactory.create(AppModule);
const logger = newLogger('Bootstrap'); // Create logger instance// Enable Cross-Origin Resource Sharing (CORS)
app.enableCors({
// WARNING: Allow all origins in dev. Use env variables for specific origins in production!origin: process.env.CORS_ORIGIN || '*',
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
allowedHeaders: 'Content-Type, Accept, Authorization',
});
// Example: Add CORS_ORIGIN=https://yourfrontend.com to .env for production// Apply global validation pipe
app.useGlobalPipes(
newValidationPipe({
whitelist: true, // Strip properties not in DTOtransform: true, // Transform payloads to DTO instancesforbidNonWhitelisted: true, // Throw error if extra properties are senttransformOptions: {
enableImplicitConversion: true, // Allow basic type conversions
},
}),
);
const configService = app.get(ConfigService); // Get ConfigService instanceconst port = configService.get<number>('PORT') || 3000; // Get port from .env or defaultawait app.listen(port);
logger.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
Now the ValidationPipe configured here will automatically apply to the @Body() in MmsController.
How to Integrate Twilio Credentials and Configuration
Credentials:TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN are fetched from environment variables using @nestjs/config and ConfigService.
Twilio Client Initialization:Twilio(accountSid, authToken) in MmsService constructor.
API Call:this.twilioClient.messages.create(…) makes the authenticated request.
Best Practices for Error Handling in Twilio MMS Integration
Error Handling Strategy:
Validation Errors: Handled by the global ValidationPipe, returning 400 Bad Request.
Configuration Errors: Checked in MmsService constructor, throws 500 Internal Server Error.
Twilio API Errors: Caught in MmsService, logged, and rethrown (currently as 500 Internal Server Error). Consider mapping specific Twilio error codes (e.g., 21211) to more specific NestJS exceptions (BadRequestException, NotFoundException) in the service if needed.
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql" // Match your chosen provider
url = env("DATABASE_URL")
}
model SentMms {
id String @id @default(cuid())
// twilioSid is nullable because you might log before getting a SID (e.g., pre-send failure)
// It should be unique for messages successfully accepted by Twilio.
twilioSid String? @unique @map("twilio_sid")
to String
from String
body String?
mediaUrls String[] @map("media_urls")
status String // e.g., queued, sending, sent, delivered, failed, undelivered
errorCode Int? @map("error_code")
errorMessage String? @map("error_message")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("sent_mms")
}
Note:twilioSid is now String? (nullable) but still @unique (allowing multiple nulls but only one entry per actual SID). Corrected provider strings.
Apply Migrations:npx prisma migrate dev --name init-mms-table
Generate Prisma Client:npx prisma generate
Create Prisma Service (src/prisma/prisma.service.ts): (Generate with nest g service prisma --no-spec and implement standard Prisma service setup). Ensure it's provided and exported in a PrismaModule.
Inject and Use PrismaService in MmsService:
TypeScript
// src/mms/mms.service.ts// ... other imports ...import { PrismaService } from'../prisma/prisma.service'; // Adjust path if neededimport { Prisma } from'@prisma/client'; // Import Prisma namespace for typesimport { MessageInstance } from'twilio/lib/rest/api/v2010/account/message'; // Ensure MessageInstance is imported@Injectable()
exportclassMmsService {
privatereadonly logger = newLogger(MmsService.name);
privatetwilioClient: Twilio.Twilio;
privatetwilioPhoneNumber: string;
constructor(privateconfigService: ConfigService,
privateprisma: PrismaService// Inject PrismaService) {
// ... Twilio client init (as before) ...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');
if (!accountSid || !authToken || !this.twilioPhoneNumber) {
this.logger.error('Twilio configuration (Account SID, Auth Token, Phone Number) is missing or invalid.');
thrownewInternalServerErrorException('Twilio configuration is missing or invalid.');
}
try {
this.twilioClient = Twilio(accountSid, authToken);
this.logger.log('Twilio client initialized successfully.');
} catch (error) {
const errorMessage = error instanceofError ? error.message : String(error);
this.logger.error(`Failed to initialize Twilio client: ${errorMessage}`, error instanceofError ? error.stack : undefined);
thrownewInternalServerErrorException(`Failed to initialize Twilio client: ${errorMessage}`);
}
}
asyncsendMms(to: string, body: string, mediaUrls: string[]): Promise<MessageInstance> {
this.logger.log(`Attempting to send MMS to ${to} with ${mediaUrls.length} media items.`);
if (mediaUrls.length > 10) {
this.logger.error(`Cannot send more than 10 media items. Provided: ${mediaUrls.length}`);
thrownewBadRequestException('Exceeded maximum number of media attachments (10). Twilio MMS supports up to 10 media files with a combined size limit of 5 MB.');
}
// Prepare record data before sendingletmessageRecordData: Prisma.SentMmsCreateInput = {
twilioSid: null, // Will be updated on successto: to,
from: this.twilioPhoneNumber,
body: body,
mediaUrls: mediaUrls,
status: 'pending', // Initial status before sendingerrorCode: null,
errorMessage: null,
};
try {
const message = awaitthis.twilioClient.messages.create({
to: to,
from: this.twilioPhoneNumber,
body: body,
mediaUrl: mediaUrls,
});
this.logger.log(`MMS sent successfully! Message SID: ${message.sid}`);
// Update record with SID and status from Twilio
messageRecordData.twilioSid = message.sid;
messageRecordData.status = message.status;
messageRecordData.errorCode = message.errorCode;
messageRecordData.errorMessage = message.errorMessage;
// Save successful attempt to database (fire-and-forget or await)this.saveMmsRecord(messageRecordData);
return message;
} catch (error) {
const errorMessage = error instanceofError ? error.message : String(error);
// Attempt to get Twilio error code if available (structure might vary)const errorCode = typeof error === 'object' && error !== null && 'code'in error ? Number(error.code) : null;
this.logger.error(`Failed to send MMS to ${to}: ${errorMessage}`, error instanceofError ? error.stack : undefined);
// Update the prepared record with failure details
messageRecordData.status = 'failed';
messageRecordData.errorCode = isNaN(errorCode) ? null : errorCode;
messageRecordData.errorMessage = errorMessage.substring(0, 255); // Limit error message length if needed// Save failed attempt record (twilioSid remains null)this.saveMmsRecord(messageRecordData);
// Rethrow original error for controller/global handlerthrownewInternalServerErrorException(`Failed to send MMS via Twilio: ${errorMessage}`);
}
}
// Helper method to save record and handle potential DB errorsprivateasyncsaveMmsRecord(data: Prisma.SentMmsCreateInput): Promise<void> {
try {
awaitthis.prisma.sentMms.create({ data });
this.logger.log(`MMS record saved/updated for SID: ${data.twilioSid ?? 'N/A (failed pre-send)'}`);
} catch (dbError) {
const errorMessage = dbError instanceofError ? dbError.message : String(dbError);
this.logger.error(`Failed to save MMS record to DB: ${errorMessage}`, dbError instanceofError ? dbError.stack : undefined);
// Decide how to handle DB errors – log, alert, add to retry queue?
}
}
}
Prepare the record data before the Twilio call.
On success, update the record with the SID and status, then save.
On failure, update the status/error fields in the prepared record (leaving twilioSid as null) and save the failure record.
A helper function handles the actual save and logs database (DB) errors. Added constructor back for clarity.
Security Best Practices for Twilio MMS Applications
Input Validation: Handled by class-validator and the global ValidationPipe.
Environment Variable Security: Use .env locally, platform secrets in production.
Rate Limiting: Use nestjs-throttler.
Install: npm install @nestjs/throttler
Setup (in src/app.module.ts):
TypeScript
// src/app.module.tsimport { Module } from'@nestjs/common';
import { ConfigModule } from'@nestjs/config';
import { ThrottlerModule, ThrottlerGuard } from'@nestjs/throttler';
import { APP_GUARD } from'@nestjs/core';
import { AppController } from'./app.controller';
import { AppService } from'./app.service';
import { MmsModule } from'./mms/mms.module';
// Import PrismaModule if using Prisma// import { PrismaModule } from './prisma/prisma.module';@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }),
ThrottlerModule.forRoot([{
ttl: 60000, // 60 secondslimit: 10, // Max 10 requests per IP per ttl
}]),
MmsModule,
// PrismaModule, // Include if using Prisma
],
controllers: [AppController],
providers: [
AppService,
// Apply ThrottlerGuard globally
{ provide: APP_GUARD, useClass: ThrottlerGuard },
// PrismaService should be provided within PrismaModule if used
],
})
exportclassAppModule {}
Authentication/Authorization: Protect the /mms endpoint using NestJS auth strategies (JSON Web Token (JWT), API Keys via Guards) if needed.
Hypertext Transfer Protocol Secure (HTTPS): Ensure deployment uses HTTPS (usually handled by load balancer/reverse proxy).
Public Media URLs: Emphasize that mediaUrlsmust be publicly accessible for Twilio.
MMS Messaging Special Cases and Limitations
Multiple Media Files: Handled via string[] for mediaUrls (max 10 files, 5 MB total size combined).
No Media Files: Current DTO requires media (@ArrayNotEmpty). To allow SMS via this endpoint, remove @ArrayNotEmpty from SendMmsDto and handle empty mediaUrls array in the service (Twilio handles it automatically if mediaUrl is empty/omitted).
Media URL Accessibility: URLs must be public and accessible by Twilio's servers. Twilio fetch failures result in Error 12300 (indicates media URL is inaccessible, incorrect, or returns an error when Twilio attempts to retrieve it).
Character Limits: Twilio supports messages up to 1600 characters (concatenated SMS/MMS). Messages are split into segments: 153 characters per segment for Global System for Mobile Communications 7-bit (GSM-7) encoding (standard alphanumeric), or 67 characters per segment for Universal Coded Character Set 2-byte (UCS-2) encoding (Unicode/emoji). For best deliverability, Twilio recommends keeping messages under 320 characters.
File Types/Sizes: MMS supports various media types (Joint Photographic Experts Group (JPEG), Portable Network Graphics (PNG), Graphics Interchange Format (GIF), etc.). Total size for all media attachments must not exceed 5 MB. Refer to Twilio MMS accepted content types. Handle potential Twilio errors for invalid/oversized media.
International MMS:Native MMS is supported only in US and Canada (as of 2025). For destinations outside US/Canada, Twilio automatically converts MMS to SMS with a link to the media via the MMS Converter feature, allowing recipients to access media through a web link. Check Twilio docs for specific regional capabilities and sender type restrictions (long code vs short code).
Idempotency: Consider adding a client-provided idempotency key if duplicate sends are a concern.
Performance Optimization for Twilio MMS Integration
Twilio Client Instantiation: Singleton instance in MmsService is efficient.
Asynchronous Operations:async/await used for input/output (I/O).
Database Connection Pooling: Handled by Prisma.
Payload Size: Keep request/response small.
Load Testing: Use tools like k6, Artillery to test under load.
Node.js Performance: Use recent Long-Term Support (LTS) Node.js version. Profile if needed.
Monitoring and Observability for MMS Applications
Health Checks: Implement /health endpoint using @nestjs/terminus.
Performance Metrics: Monitor request latency, rate, error rate using Application Performance Monitoring (APM) tools (Datadog, New Relic) or Prometheus/Grafana (nestjs-prometheus).
Error Tracking: Integrate with Sentry, Bugsnag, etc.
Logging Aggregation: Ship structured logs to a central platform (Datadog Logs, Splunk, Elasticsearch Logstash Kibana (ELK), Loki).
Invalid Credentials (Twilio Error 20003): Check TWILIO_ACCOUNT_SID/TWILIO_AUTH_TOKEN in environment.
Invalid "To" Number (Twilio Error 21211): Ensure E.164 format. The "To" phone number supplied was not valid or was incorrectly formatted. DTO validator helps prevent this.
Invalid "From" Number (Twilio Error 21606): Check TWILIO_PHONE_NUMBER in environment; verify number in Twilio Console (belongs to account SID, has MMS capability for US/Canada destinations).
Media URL Inaccessible (Twilio Error 12300): Verify URLs are publicly accessible and return valid media. This error indicates Twilio could not retrieve the media from the provided URL (unreachable, authentication required, or returns an error).
Max Media Exceeded (Twilio Error 21623): DTO validation (@ArrayMaxSize(10)) should prevent exceeding 10 media attachments.
Message Body Too Long (Twilio Error 21617): The concatenated message body exceeds the 1600 character limit. DTO validation (@MaxLength(1600)) should prevent this.
MMS Not Supported (Various Codes): Carrier/region limitation. Native MMS only works in US/Canada; other regions receive SMS with media links via MMS Converter. Log error for troubleshooting.
Rate Limits Exceeded (429 Too Many Requests): Adjust nestjs-throttler config or investigate client behavior.
Environment Variables Not Loaded: Check ConfigModule setup, .env file location/presence (for local dev), platform variable configuration (for prod).
Missing Dependencies: Run npm install / yarn install.
Node.js Version Issues: Ensure Node.js v20+ is installed. Node.js v18 reached EOL and is not supported by Twilio CLI 6.0.0+.
How to Deploy Your NestJS MMS Application
Build for Production:npm run build (generates dist folder).
Running in Production:NODE_ENV=production node dist/main.js. Use pm2 for process management: pm2 start dist/main.js --name nestjs-mms-api -i max.
Environment Configuration: Use platform's environment variable/secret management (Amazon Web Services (AWS) Secrets Manager, Kubernetes (K8s) Secrets, Heroku Config Vars, etc.). Do not deploy .env files.
Dockerization (Example Dockerfile):
Dockerfile
# Dockerfile
# ---- Base Node ----
FROM node:20-alpine AS base
WORKDIR /usr/src/app
COPY package*.json ./
# ---- Dependencies ----
FROM base AS dependencies
# Install production dependencies only
RUN npm install --omit=dev
# If using Prisma, copy schema and generate client
# COPY prisma ./prisma
# RUN npx prisma generate
# ---- Build ----
FROM base AS build
# Install ALL dependencies (including dev for build process)
RUN npm install
# Copy all source files
COPY . .
# Build the application
RUN npm run build
# If using Prisma, copy schema again for runtime
# COPY prisma ./dist/prisma
# ---- Production ----
FROM node:20-alpine AS production
WORKDIR /usr/src/app
# Copy production node_modules from dependencies stage
COPY --from=dependencies /usr/src/app/node_modules ./node_modules
# Copy built application from build stage
COPY --from=build /usr/src/app/dist ./dist
# Copy package.json for runtime identification (optional but good practice)
COPY package*.json ./
# If using Prisma, copy the generated client from dependencies stage
# COPY --from=dependencies /usr/src/app/node_modules/.prisma ./node_modules/.prisma
# COPY --from=build /usr/src/app/dist/prisma ./dist/prisma
# Expose the application port
EXPOSE 3000
# Set NODE_ENV to production
ENV NODE_ENV=production
# Command to run the application
# Ensure your PORT env variable is set correctly in your deployment environment
CMD ["node", "dist/main.js"]
Updated Dockerfile to use Node.js v20 (from v18) for 2025 compatibility. Improved multi-stage build for better caching and smaller final image. Added Prisma steps (commented out).
Frequently Asked Questions About Twilio MMS and NestJS
How do I send MMS messages with Twilio and NestJS?
To send MMS messages with Twilio and NestJS, you create a NestJS service that initializes the Twilio client with your Account SID and Auth Token, then call client.messages.create() with the recipient's phone number, your Twilio number, message body, and an array of media URLs. The media URLs must be publicly accessible so Twilio can retrieve and deliver them to the recipient.
What Node.js version is required for Twilio MMS integration in 2025?
For Twilio MMS integration in 2025, you need Node.js v20 or later. Node.js v18 reached end-of-life and is no longer supported by Twilio CLI 6.0.0+. NestJS v10+ requires Node.js v16+ minimum, but v20 is recommended for production deployments to ensure compatibility with the latest Twilio SDK (v5.x supports Node.js v22).
What is the maximum file size for Twilio MMS attachments?
Twilio MMS supports up to 10 media files with a combined total size of 5 MB per message. Messages exceeding 5 MB will be rejected. For individual file types like JPEG, PNG, and GIF, Twilio automatically resizes images as necessary based on carrier specifications. Other messaging channels have different limits: WhatsApp supports up to 16 MB, while Rich Communication Services (RCS) supports 16 MB but falls back to MMS (5 MB limit) if RCS is unavailable.
Does Twilio MMS work internationally outside the US and Canada?
Native MMS is only supported in the US and Canada as of 2025. For international destinations outside US/Canada, Twilio automatically converts MMS to SMS using the MMS Converter feature, which includes a link to the media content that recipients can access via web browser. This ensures global delivery while maintaining the ability to share multimedia content with international recipients.
How do I handle Twilio Error 12300 for media URL accessibility?
Twilio Error 12300 indicates that Twilio could not retrieve media from the provided URL. To fix this error: (1) Verify the URL is publicly accessible without authentication, (2) Ensure the URL returns a valid media file (JPEG, PNG, GIF, etc.), (3) Check that your server allows requests from Twilio's IP addresses, (4) Test the URL in a browser to confirm it loads correctly. Media URLs must be publicly accessible for Twilio to fetch and deliver them.
What character limits apply to MMS messages with Twilio?
Twilio supports messages up to 1600 characters for concatenated SMS/MMS. Messages are automatically split into segments: 153 characters per segment for GSM-7 encoding (standard alphanumeric characters), or 67 characters per segment for UCS-2 encoding (Unicode/emoji). For optimal deliverability, Twilio recommends keeping messages under 320 characters. Use the @MaxLength(1600) validator in your DTO to prevent exceeding this limit.
How do I validate phone numbers for Twilio MMS in NestJS?
Use the @IsPhoneNumber() decorator from class-validator in your DTO to validate phone numbers in E.164 format (e.g., +15551234567). This prevents Twilio Error 21211 (invalid "To" number). Combine it with @IsNotEmpty() to ensure the field is required. The E.164 format includes a plus sign, country code, and subscriber number without spaces or special characters.
What TypeScript and NestJS versions are compatible with Twilio MMS?
NestJS v10+ requires TypeScript v4.8 or higher and Node.js v16 or higher. The Twilio Node.js SDK v5.x supports TypeScript v2.9+ and works with Node.js v20-v22. For optimal compatibility in 2025, use Node.js v20+, TypeScript v4.8+, and NestJS v10+ to ensure you have access to the latest features and security updates.
How do I implement rate limiting for my Twilio MMS API endpoint?
Implement rate limiting using the @nestjs/throttler package. Install it with npm install @nestjs/throttler, then configure ThrottlerModule.forRoot() in your AppModule with ttl (time window in milliseconds) and limit (max requests per window). Apply ThrottlerGuard globally using the APP_GUARD provider to protect all endpoints, or apply it selectively to specific controllers or routes using the @UseGuards() decorator.
Can I send SMS and MMS from the same NestJS endpoint?
Yes, you can send both SMS and MMS from the same endpoint by making the mediaUrls array optional in your DTO. Remove the @ArrayNotEmpty() decorator and handle empty arrays in your service logic. When mediaUrls is empty or omitted, Twilio automatically sends the message as SMS instead of MMS. This provides flexibility for clients to choose between SMS and MMS without requiring separate endpoints.
Frequently asked questions
Use the Twilio Programmable Messaging API integrated with a NestJS service. Create a dedicated MMS service and controller in your NestJS application to handle MMS sending logic and API requests. This service interacts with the Twilio API to deliver messages containing text and media to specified recipients. Configure environment variables to securely store your Twilio credentials.
Twilio's Programmable Messaging API is a cloud communications platform that enables sending SMS, MMS, and other messages. The API provides a way to integrate messaging into your applications, and it's used in this project to send multimedia messages. The guide details how to use the Node.js Twilio helper library within a NestJS application.
NestJS provides a structured and scalable framework that simplifies Twilio integration. By building an MMS service and controller, you can abstract away the complexities of API interaction, implement robust error handling, and ensure maintainability. This approach offers more structure and scalability than directly interacting with the Twilio API.
Use MMS when you need to send rich media content like images, GIFs, or other files alongside text. MMS offers a more engaging and informative user experience compared to SMS, providing richer context. This tutorial details how to integrate this functionality into your NestJS application using Twilio.
You'll need Node.js v18 or later, npm or yarn, the NestJS CLI, a Twilio account with an MMS-enabled phone number (mainly US and Canada), a code editor, and a terminal. A Twilio account is required to obtain necessary credentials and purchase a phone number, after which you can set up your NestJS project using the provided instructions.
Create a .env file in the root directory and store your TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER. Then, in your app.module.ts, import and configure the ConfigModule to load these environment variables, ensuring they're accessible in your NestJS services.
Implement error handling within the sendMms method of your service. Catch potential errors during the Twilio API call, log them using NestJS's Logger, and re-throw them as appropriate NestJS HTTP exceptions. The article suggests mapping specific Twilio error codes for more precise error responses.
Create a Data Transfer Object (DTO) with validation decorators from class-validator. Enable a global validation pipe in your main.ts file to automatically validate incoming requests against your DTO, ensuring data integrity and security.
Yes, the Twilio API supports sending multiple media files (up to 10) in one MMS message. Provide an array of publicly accessible URLs for your media items when calling the Twilio API via your NestJS application. The provided code includes validation to ensure you don't exceed the maximum number of attachments.
Maintain the Twilio client as a singleton in the MmsService, utilize asynchronous operations with async/await, and ensure your database connection pooling is configured efficiently. Keeping request and response payloads small also improves performance. Load testing and profiling tools are also recommended for monitoring and optimizing your setup.
Secure your Twilio credentials as environment variables, implement rate limiting using nestjs-throttler, and consider authentication/authorization for your API endpoint. Always deploy using HTTPS and verify the public accessibility of your media URLs. These measures prevent misuse, abuse, unauthorized access, and data exposure.
Utilize NestJS's Logger to log key events, and integrate with an error tracking service such as Sentry or Bugsnag for monitoring and logging errors that occur during the MMS sending process. Consider storing logs centrally using a log management platform for detailed analytics and observability.
Refer to the Twilio error codes documentation and handle common errors like invalid credentials (20003), invalid phone numbers (21211/21606), or inaccessible media URLs (12300). Logging within your NestJS service aids greatly in debugging and resolving these integration issues.
Build your application for production using npm run build, manage processes with pm2, securely manage environment variables via platform-specific tools, and consider Dockerizing your application for containerized deployment. Never deploy .env files; use platform-specific secret management instead.
Multimedia Messaging Service (MMS) allows you to enrich your user communication by sending images, GIFs, and other media alongside text. Integrating Twilio MMS capabilities into your NestJS applications can significantly enhance user engagement and provide richer context than Short Message Service (SMS) alone.
This guide provides a step-by-step walkthrough for building a robust NestJS application capable of sending MMS messages via the Twilio Programmable Messaging Application Programming Interface (API). You'll learn how to send multimedia messages with Node.js, implement proper validation, handle errors, and deploy a production-ready MMS messaging solution.
Project Overview and Goals
What You'll Build:
You will create a NestJS backend application featuring a dedicated API endpoint (
/mms) that accepts requests to send MMS messages. This endpoint will securely interact with the Twilio API to deliver messages containing text and media (specified by publicly accessible Uniform Resource Locators (URLs)) to designated recipients.Problem Solved:
This project provides a structured, scalable, and maintainable way to integrate Twilio MMS functionality into any NestJS-based system. It abstracts the complexities of direct Twilio API interaction behind a clean NestJS service and controller layer, incorporating best practices for configuration, validation, and error handling.
Technologies Used:
dotenv: Module for loading environment variables from a.envfile.System Architecture:
The basic flow involves a client (like a frontend app, Postman, or another service) making an Hypertext Transfer Protocol (HTTP) POST request to your NestJS API. The NestJS application validates the request, uses the Twilio Software Development Kit (SDK) (configured with credentials from environment variables) to call the Twilio Messaging API, which then handles the delivery of the MMS message to the end user's mobile device.
Prerequisites:
node -v.npm -voryarn -v.npm install -g @nestjs/cli). NestJS v10+ requires Node.js v16+ and TypeScript v4.8+.curl: For testing the API endpoint.How to Set Up Your NestJS Project for Twilio MMS
Initialize your NestJS project and install the necessary dependencies.
Create a New NestJS Project: Open your terminal and run the NestJS CLI command to scaffold a new project. Replace
nestjs-twilio-mmswith your preferred project name.Choose your preferred package manager (npm or yarn) when prompted.
Navigate into Project Directory:
cd nestjs-twilio-mmsInstall Dependencies: Install the official Twilio Node.js helper library and
dotenvfor managing environment variables. You also need NestJS configuration and validation packages.# Using npm npm install twilio dotenv @nestjs/config class-validator class-transformer # Using yarn yarn add twilio dotenv @nestjs/config class-validator class-transformertwilio: The official SDK for interacting with the Twilio API.dotenv: Loads environment variables from a.envfile intoprocess.env.@nestjs/config: Provides configuration management for NestJS applications.class-validator&class-transformer: Used for request data validation via Data Transfer Objects (DTOs).Configure Environment Variables: Create a file named
.envin the root directory of your project. This file will store sensitive credentials and configuration details securely. Never commit this file to version control.Add a
.gitignoreentry if it doesn't exist:Populate the
.envfile with your Twilio credentials and phone number:Replace the placeholder values (
ACxxx…,your_auth_token…,+1xxx…) with your actual credentials and phone number.Load Configuration in AppModule: Modify
src/app.module.tsto load the environment variables using@nestjs/config.// 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 { MmsModule } from './mms/mms.module'; // You will create this soon @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Makes ConfigModule available globally envFilePath: '.env', // Specify the env file path (useful for local dev) // In production, environment variables are typically set directly }), MmsModule, // Import your MMS module ], controllers: [AppController], providers: [AppService], }) export class AppModule {}Setting
isGlobal: truemakes theConfigServiceavailable throughout the application without needing to importConfigModulein other modules.How to Implement MMS Sending Functionality with Twilio SDK
Encapsulate the Twilio interaction logic within a dedicated NestJS service.
Generate the MMS Module and Service: Use the NestJS CLI to generate a new module and service for MMS handling.
nest generate module mms nest generate service mms --no-spec # Use --no-spec to skip generating test file for nowThis creates
src/mms/mms.module.tsandsrc/mms/mms.service.ts. TheMmsModuleis automatically imported intoAppModuleif you generated it after setting up theAppModuleimports.Implement the MmsService: Open
src/mms/mms.service.tsand implement the logic to send MMS messages using the Twilio client.// src/mms/mms.service.ts import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as Twilio from 'twilio'; // Use wildcard import for Twilio namespace import { MessageInstance } from 'twilio/lib/rest/api/v2010/account/message'; @Injectable() export class MmsService { private readonly logger = new Logger(MmsService.name); private twilioClient: Twilio.Twilio; // Type the client correctly private twilioPhoneNumber: string; constructor(private configService: ConfigService) { 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'); if (!accountSid || !authToken || !this.twilioPhoneNumber) { this.logger.error('Twilio configuration (Account SID, Auth Token, Phone Number) is missing or invalid.'); // Throw InternalServerErrorException because this is a server configuration issue throw new InternalServerErrorException('Twilio configuration is missing or invalid.'); } try { this.twilioClient = Twilio(accountSid, authToken); // Initialize Twilio client this.logger.log('Twilio client initialized successfully.'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Failed to initialize Twilio client: ${errorMessage}`, error instanceof Error ? error.stack : undefined); throw new InternalServerErrorException(`Failed to initialize Twilio client: ${errorMessage}`); } } /** * Sends an MMS message using Twilio. * @param to The recipient's phone number in E.164 format (e.g., +15551234567). * @param body The text body of the message. * @param mediaUrls An array of publicly accessible URLs for the media attachments (up to 10, max 5 MB total). * @returns A Promise resolving to the Twilio MessageInstance. */ async sendMms(to: string, body: string, mediaUrls: string[]): Promise<MessageInstance> { this.logger.log(`Attempting to send MMS to ${to} with ${mediaUrls.length} media items.`); if (mediaUrls.length === 0) { this.logger.warn('No media URLs provided. Sending as SMS instead.'); // Note: Twilio automatically sends as SMS if mediaUrl is empty/omitted, // but the DTO currently requires mediaUrls. Modify DTO if SMS fallback is desired. } if (mediaUrls.length > 10) { this.logger.error(`Cannot send more than 10 media items. Provided: ${mediaUrls.length}`); // Use BadRequestException as this is a client-provided data issue throw new BadRequestException('Exceeded maximum number of media attachments (10). Twilio MMS supports up to 10 media files with a combined size limit of 5 MB.'); } try { const message = await this.twilioClient.messages.create({ to: to, from: this.twilioPhoneNumber, body: body, mediaUrl: mediaUrls, // Pass the array directly }); this.logger.log(`MMS sent successfully! Message SID: ${message.sid}`); return message; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Failed to send MMS to ${to}: ${errorMessage}`, error instanceof Error ? error.stack : undefined); // Consider mapping specific Twilio errors (e.g., 21211 invalid "to" number) // to different NestJS exceptions (e.g., BadRequestException) if needed. // For now, rethrow as InternalServerErrorException for simplicity. throw new InternalServerErrorException(`Failed to send MMS via Twilio: ${errorMessage}`); } } }ConfigServiceto safely access your environment variables.ConfigService. Checks ensure variables are present, throwingInternalServerErrorExceptionon failure. The error message is more general now.sendMmsMethod: This asynchronous method takes the recipient (to), message body (body), and an array ofmediaUrls.BadRequestExceptionif more than 10 media URLs are provided.mediaUrlParameter: The Twilio Node.js library accepts an array of strings for themediaUrlparameter.Logger. Errors during message sending are caught, logged, and rethrown asInternalServerErrorException. Specific Twilio errors could be handled more granularly. Improved error message extraction.Provide ConfigService in MmsModule: Ensure
ConfigServiceis available within theMmsModule. Since you madeConfigModuleglobal inAppModule, no extra import is needed here.How to Create an MMS API Endpoint in NestJS
Create an API endpoint to trigger the
MmsService.Generate the MMS Controller:
This creates
src/mms/mms.controller.ts.Create a Data Transfer Object (DTO) for Validation: DTOs define the expected shape of request data and enable automatic validation using
class-validator. Create a filesrc/mms/dto/send-mms.dto.ts.// src/mms/dto/send-mms.dto.ts import { IsString, IsNotEmpty, IsPhoneNumber, IsArray, ArrayMaxSize, ArrayNotEmpty, IsUrl, MaxLength } from 'class-validator'; export class SendMmsDto { @IsPhoneNumber(null, { message: 'Recipient phone number must be a valid E.164 format string (e.g., +15551234567).' }) // Validates E.164 format @IsNotEmpty({ message: 'Recipient phone number (to) cannot be empty.' }) to: string; @IsString() @IsNotEmpty({ message: 'Message body cannot be empty.' }) @MaxLength(1600, { message: 'Message body cannot exceed 1600 characters. Twilio supports concatenated messages up to 1600 chars (split into 153-char segments for GSM-7 or 67-char segments for UCS-2/Unicode).'}) // Twilio's max concatenated message length body: string; @IsArray() @ArrayNotEmpty({ message: 'At least one media URL must be provided for MMS.' }) @ArrayMaxSize(10, { message: 'Cannot attach more than 10 media items. Total size must not exceed 5 MB.' }) @IsUrl({}, { each: true, message: 'Each media URL must be a valid, publicly accessible URL.' }) // Validate each item in the array is a URL mediaUrls: string[]; }@IsPhoneNumber,@IsString,@IsArray,@IsUrl,@ArrayMaxSize, etc., to define validation rules.Implement the MmsController: Open
src/mms/mms.controller.tsand define the POST endpoint.// src/mms/mms.controller.ts import { Controller, Post, Body, HttpCode, HttpStatus, Logger } from '@nestjs/common'; import { MmsService } from './mms.service'; import { SendMmsDto } from './dto/send-mms.dto'; @Controller('mms') // Route prefix for this controller export class MmsController { private readonly logger = new Logger(MmsController.name); constructor(private readonly mmsService: MmsService) {} @Post() // Handles POST requests to /mms @HttpCode(HttpStatus.ACCEPTED) // Return 202 Accepted as sending is async // ValidationPipe is applied globally in main.ts async sendMms(@Body() sendMmsDto: SendMmsDto): Promise<{ messageSid: string; status: string }> { this.logger.log(`Received request to send MMS to: ${sendMmsDto.to}`); const { to, body, mediaUrls } = sendMmsDto; // No try-catch needed here if you let exceptions propagate // NestJS global exception filters (or default handler) will catch errors // from the service or validation pipe. const message = await this.mmsService.sendMms(to, body, mediaUrls); this.logger.log(`MMS queued successfully for SID: ${message.sid}`); // Return the SID and initial status (usually "queued" or "sending") return { messageSid: message.sid, status: message.status }; } }@Controller('mms'): Sets the base route path to/mms.@Post(): Decorator for handling HTTP POST requests.@Body(): Extracts the request body and implicitly validates/transforms it if a global pipe is enabled.@HttpCode(HttpStatus.ACCEPTED): Sets the default response status code to 202 Accepted.MmsServiceis injected.messageSidand initialstatusfrom Twilio.@UsePipes: Enable theValidationPipeglobally.Enable Global Validation Pipe (Recommended): Modify
src/main.tsto enable the validation pipe for all incoming requests.// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe, Logger } from '@nestjs/common'; // Import Logger import { ConfigService } from '@nestjs/config'; // Import ConfigService async function bootstrap() { const app = await NestFactory.create(AppModule); const logger = new Logger('Bootstrap'); // Create logger instance // Enable Cross-Origin Resource Sharing (CORS) app.enableCors({ // WARNING: Allow all origins in dev. Use env variables for specific origins in production! origin: process.env.CORS_ORIGIN || '*', methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', allowedHeaders: 'Content-Type, Accept, Authorization', }); // Example: Add CORS_ORIGIN=https://yourfrontend.com to .env for production // Apply global validation pipe app.useGlobalPipes( new ValidationPipe({ whitelist: true, // Strip properties not in DTO transform: true, // Transform payloads to DTO instances forbidNonWhitelisted: true, // Throw error if extra properties are sent transformOptions: { enableImplicitConversion: true, // Allow basic type conversions }, }), ); const configService = app.get(ConfigService); // Get ConfigService instance const port = configService.get<number>('PORT') || 3000; // Get port from .env or default await app.listen(port); logger.log(`Application is running on: ${await app.getUrl()}`); } bootstrap();Now the
ValidationPipeconfigured here will automatically apply to the@Body()inMmsController.How to Integrate Twilio Credentials and Configuration
TWILIO_ACCOUNT_SIDandTWILIO_AUTH_TOKENare fetched from environment variables using@nestjs/configandConfigService..envlocally; use platform secret management in production.TWILIO_PHONE_NUMBERfetched from environment variables.+1…).Twilio(accountSid, authToken)inMmsServiceconstructor.this.twilioClient.messages.create(…)makes the authenticated request.Best Practices for Error Handling in Twilio MMS Integration
ValidationPipe, returning 400 Bad Request.MmsServiceconstructor, throws 500 Internal Server Error.MmsService, logged, and rethrown (currently as 500 Internal Server Error). Consider mapping specific Twilio error codes (e.g.,21211) to more specific NestJS exceptions (BadRequestException,NotFoundException) in the service if needed.Loggerused in service and controller.async-retryor queue systems (Bull) for retrying transient Twilio API errors.How to Track MMS Messages with Database Integration (Optional)
Track message status and details using Prisma.
Install Prisma:
npm install prisma @prisma/client --save-devInitialize Prisma:
npx prisma init --datasource-provider postgresql(adjust provider). UpdateDATABASE_URLin.env.Define Schema (
prisma/schema.prisma):twilioSidis nowString?(nullable) but still@unique(allowing multiple nulls but only one entry per actual SID). Corrected provider strings.Apply Migrations:
npx prisma migrate dev --name init-mms-tableGenerate Prisma Client:
npx prisma generateCreate Prisma Service (
src/prisma/prisma.service.ts): (Generate withnest g service prisma --no-specand implement standard Prisma service setup). Ensure it's provided and exported in aPrismaModule.Inject and Use PrismaService in MmsService:
// src/mms/mms.service.ts // ... other imports ... import { PrismaService } from '../prisma/prisma.service'; // Adjust path if needed import { Prisma } from '@prisma/client'; // Import Prisma namespace for types import { MessageInstance } from 'twilio/lib/rest/api/v2010/account/message'; // Ensure MessageInstance is imported @Injectable() export class MmsService { private readonly logger = new Logger(MmsService.name); private twilioClient: Twilio.Twilio; private twilioPhoneNumber: string; constructor( private configService: ConfigService, private prisma: PrismaService // Inject PrismaService ) { // ... Twilio client init (as before) ... 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'); if (!accountSid || !authToken || !this.twilioPhoneNumber) { this.logger.error('Twilio configuration (Account SID, Auth Token, Phone Number) is missing or invalid.'); throw new InternalServerErrorException('Twilio configuration is missing or invalid.'); } try { this.twilioClient = Twilio(accountSid, authToken); this.logger.log('Twilio client initialized successfully.'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); this.logger.error(`Failed to initialize Twilio client: ${errorMessage}`, error instanceof Error ? error.stack : undefined); throw new InternalServerErrorException(`Failed to initialize Twilio client: ${errorMessage}`); } } async sendMms(to: string, body: string, mediaUrls: string[]): Promise<MessageInstance> { this.logger.log(`Attempting to send MMS to ${to} with ${mediaUrls.length} media items.`); if (mediaUrls.length > 10) { this.logger.error(`Cannot send more than 10 media items. Provided: ${mediaUrls.length}`); throw new BadRequestException('Exceeded maximum number of media attachments (10). Twilio MMS supports up to 10 media files with a combined size limit of 5 MB.'); } // Prepare record data before sending let messageRecordData: Prisma.SentMmsCreateInput = { twilioSid: null, // Will be updated on success to: to, from: this.twilioPhoneNumber, body: body, mediaUrls: mediaUrls, status: 'pending', // Initial status before sending errorCode: null, errorMessage: null, }; try { const message = await this.twilioClient.messages.create({ to: to, from: this.twilioPhoneNumber, body: body, mediaUrl: mediaUrls, }); this.logger.log(`MMS sent successfully! Message SID: ${message.sid}`); // Update record with SID and status from Twilio messageRecordData.twilioSid = message.sid; messageRecordData.status = message.status; messageRecordData.errorCode = message.errorCode; messageRecordData.errorMessage = message.errorMessage; // Save successful attempt to database (fire-and-forget or await) this.saveMmsRecord(messageRecordData); return message; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // Attempt to get Twilio error code if available (structure might vary) const errorCode = typeof error === 'object' && error !== null && 'code' in error ? Number(error.code) : null; this.logger.error(`Failed to send MMS to ${to}: ${errorMessage}`, error instanceof Error ? error.stack : undefined); // Update the prepared record with failure details messageRecordData.status = 'failed'; messageRecordData.errorCode = isNaN(errorCode) ? null : errorCode; messageRecordData.errorMessage = errorMessage.substring(0, 255); // Limit error message length if needed // Save failed attempt record (twilioSid remains null) this.saveMmsRecord(messageRecordData); // Rethrow original error for controller/global handler throw new InternalServerErrorException(`Failed to send MMS via Twilio: ${errorMessage}`); } } // Helper method to save record and handle potential DB errors private async saveMmsRecord(data: Prisma.SentMmsCreateInput): Promise<void> { try { await this.prisma.sentMms.create({ data }); this.logger.log(`MMS record saved/updated for SID: ${data.twilioSid ?? 'N/A (failed pre-send)'}`); } catch (dbError) { const errorMessage = dbError instanceof Error ? dbError.message : String(dbError); this.logger.error(`Failed to save MMS record to DB: ${errorMessage}`, dbError instanceof Error ? dbError.stack : undefined); // Decide how to handle DB errors – log, alert, add to retry queue? } } }twilioSidasnull) and save the failure record.Security Best Practices for Twilio MMS Applications
class-validatorand the globalValidationPipe..envlocally, platform secrets in production.nestjs-throttler.npm install @nestjs/throttlersrc/app.module.ts):// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; import { APP_GUARD } from '@nestjs/core'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { MmsModule } from './mms/mms.module'; // Import PrismaModule if using Prisma // import { PrismaModule } from './prisma/prisma.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }), ThrottlerModule.forRoot([{ ttl: 60000, // 60 seconds limit: 10, // Max 10 requests per IP per ttl }]), MmsModule, // PrismaModule, // Include if using Prisma ], controllers: [AppController], providers: [ AppService, // Apply ThrottlerGuard globally { provide: APP_GUARD, useClass: ThrottlerGuard }, // PrismaService should be provided within PrismaModule if used ], }) export class AppModule {}/mmsendpoint using NestJS auth strategies (JSON Web Token (JWT), API Keys via Guards) if needed.mediaUrlsmust be publicly accessible for Twilio.MMS Messaging Special Cases and Limitations
string[]formediaUrls(max 10 files, 5 MB total size combined).@ArrayNotEmpty). To allow SMS via this endpoint, remove@ArrayNotEmptyfromSendMmsDtoand handle emptymediaUrlsarray in the service (Twilio handles it automatically ifmediaUrlis empty/omitted).Performance Optimization for Twilio MMS Integration
MmsServiceis efficient.async/awaitused for input/output (I/O).k6,Artilleryto test under load.Monitoring and Observability for MMS Applications
/healthendpoint using@nestjs/terminus.nestjs-prometheus).Common Twilio MMS Errors and How to Fix Them
TWILIO_ACCOUNT_SID/TWILIO_AUTH_TOKENin environment.TWILIO_PHONE_NUMBERin environment; verify number in Twilio Console (belongs to account SID, has MMS capability for US/Canada destinations).@ArrayMaxSize(10)) should prevent exceeding 10 media attachments.@MaxLength(1600)) should prevent this.nestjs-throttlerconfig or investigate client behavior.ConfigModulesetup,.envfile location/presence (for local dev), platform variable configuration (for prod).npm install/yarn install.How to Deploy Your NestJS MMS Application
Build for Production:
npm run build(generatesdistfolder).Running in Production:
NODE_ENV=production node dist/main.js. Usepm2for process management:pm2 start dist/main.js --name nestjs-mms-api -i max.Environment Configuration: Use platform's environment variable/secret management (Amazon Web Services (AWS) Secrets Manager, Kubernetes (K8s) Secrets, Heroku Config Vars, etc.). Do not deploy
.envfiles.Dockerization (Example
Dockerfile):Frequently Asked Questions About Twilio MMS and NestJS
How do I send MMS messages with Twilio and NestJS?
To send MMS messages with Twilio and NestJS, you create a NestJS service that initializes the Twilio client with your Account SID and Auth Token, then call
client.messages.create()with the recipient's phone number, your Twilio number, message body, and an array of media URLs. The media URLs must be publicly accessible so Twilio can retrieve and deliver them to the recipient.What Node.js version is required for Twilio MMS integration in 2025?
For Twilio MMS integration in 2025, you need Node.js v20 or later. Node.js v18 reached end-of-life and is no longer supported by Twilio CLI 6.0.0+. NestJS v10+ requires Node.js v16+ minimum, but v20 is recommended for production deployments to ensure compatibility with the latest Twilio SDK (v5.x supports Node.js v22).
What is the maximum file size for Twilio MMS attachments?
Twilio MMS supports up to 10 media files with a combined total size of 5 MB per message. Messages exceeding 5 MB will be rejected. For individual file types like JPEG, PNG, and GIF, Twilio automatically resizes images as necessary based on carrier specifications. Other messaging channels have different limits: WhatsApp supports up to 16 MB, while Rich Communication Services (RCS) supports 16 MB but falls back to MMS (5 MB limit) if RCS is unavailable.
Does Twilio MMS work internationally outside the US and Canada?
Native MMS is only supported in the US and Canada as of 2025. For international destinations outside US/Canada, Twilio automatically converts MMS to SMS using the MMS Converter feature, which includes a link to the media content that recipients can access via web browser. This ensures global delivery while maintaining the ability to share multimedia content with international recipients.
How do I handle Twilio Error 12300 for media URL accessibility?
Twilio Error 12300 indicates that Twilio could not retrieve media from the provided URL. To fix this error: (1) Verify the URL is publicly accessible without authentication, (2) Ensure the URL returns a valid media file (JPEG, PNG, GIF, etc.), (3) Check that your server allows requests from Twilio's IP addresses, (4) Test the URL in a browser to confirm it loads correctly. Media URLs must be publicly accessible for Twilio to fetch and deliver them.
What character limits apply to MMS messages with Twilio?
Twilio supports messages up to 1600 characters for concatenated SMS/MMS. Messages are automatically split into segments: 153 characters per segment for GSM-7 encoding (standard alphanumeric characters), or 67 characters per segment for UCS-2 encoding (Unicode/emoji). For optimal deliverability, Twilio recommends keeping messages under 320 characters. Use the
@MaxLength(1600)validator in your DTO to prevent exceeding this limit.How do I validate phone numbers for Twilio MMS in NestJS?
Use the
@IsPhoneNumber()decorator fromclass-validatorin your DTO to validate phone numbers in E.164 format (e.g., +15551234567). This prevents Twilio Error 21211 (invalid "To" number). Combine it with@IsNotEmpty()to ensure the field is required. The E.164 format includes a plus sign, country code, and subscriber number without spaces or special characters.What TypeScript and NestJS versions are compatible with Twilio MMS?
NestJS v10+ requires TypeScript v4.8 or higher and Node.js v16 or higher. The Twilio Node.js SDK v5.x supports TypeScript v2.9+ and works with Node.js v20-v22. For optimal compatibility in 2025, use Node.js v20+, TypeScript v4.8+, and NestJS v10+ to ensure you have access to the latest features and security updates.
How do I implement rate limiting for my Twilio MMS API endpoint?
Implement rate limiting using the
@nestjs/throttlerpackage. Install it withnpm install @nestjs/throttler, then configureThrottlerModule.forRoot()in yourAppModulewithttl(time window in milliseconds) andlimit(max requests per window). ApplyThrottlerGuardglobally using theAPP_GUARDprovider to protect all endpoints, or apply it selectively to specific controllers or routes using the@UseGuards()decorator.Can I send SMS and MMS from the same NestJS endpoint?
Yes, you can send both SMS and MMS from the same endpoint by making the
mediaUrlsarray optional in your DTO. Remove the@ArrayNotEmpty()decorator and handle empty arrays in your service logic. WhenmediaUrlsis empty or omitted, Twilio automatically sends the message as SMS instead of MMS. This provides flexibility for clients to choose between SMS and MMS without requiring separate endpoints.