code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

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.

Send MMS Messages with Twilio and NestJS: Complete Integration Guide

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.

mermaid
sequenceDiagram
    participant Client
    participant NestJS API
    participant Twilio API
    participant Mobile Device

    Client->>+NestJS API: POST /mms (to, body, mediaUrls)
    NestJS API->>NestJS API: Validate Request (DTO)
    NestJS API->>NestJS API: Load Twilio Credentials (.env / Env Vars)
    NestJS API->>+Twilio API: client.messages.create({to, from, body, mediaUrl: [...]})
    Twilio API->>Twilio API: Process & Send MMS
    Twilio API-->>-NestJS API: Return Message SID / Status
    Twilio API->>+Mobile Device: Deliver MMS
    Mobile Device-->>-Twilio API: Delivery Confirmation (async)
    NestJS API-->>-Client: Return Success/Error Response (e.g., { messageSid: "SM..." })

Prerequisites:

  • 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.
  • NestJS CLI: Installed globally (npm install -g @nestjs/cli). NestJS v10+ requires Node.js v16+ and TypeScript v4.8+.
  • Twilio Account:
    • Sign up for a free Twilio account here.
    • Obtain your Account Security Identifier (SID) and Auth Token from the Twilio Console dashboard.
    • 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.

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

  2. Navigate into Project Directory:

    bash
    cd nestjs-twilio-mms
  3. 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).
  4. 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:

      dotenv
      # .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.

  5. Load Configuration in AppModule: Modify src/app.module.ts to load the environment variables using @nestjs/config.

    typescript
    // 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: 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.

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

  2. 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.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}`);
        }
      }
    }
    • 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.
  3. 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.

  1. Generate the MMS Controller:

    bash
    nest generate controller mms --no-spec

    This creates src/mms/mms.controller.ts.

  2. 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.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[];
    }
    • Use decorators like @IsPhoneNumber, @IsString, @IsArray, @IsUrl, @ArrayMaxSize, etc., to define validation rules.
    • Messages provide user-friendly error feedback.
  3. Implement the MmsController: Open src/mms/mms.controller.ts and define the POST endpoint.

    typescript
    // 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.
    • Dependency Injection: MmsService is injected.
    • Response: Returns the messageSid and initial status from Twilio.
    • Removed @UsePipes: Enable the ValidationPipe globally.
  4. Enable Global Validation Pipe (Recommended): Modify src/main.ts to enable the validation pipe for all incoming requests.

    typescript
    // 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 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.
    • Obtaining: Twilio Console.
    • Security: Use .env locally; use platform secret management in production.
  • Twilio Phone Number: TWILIO_PHONE_NUMBER fetched from environment variables.
  • 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.
    • Global Exception Filter (Optional): Implement a custom filter for centralized error formatting and handling. See NestJS Documentation – Exception Filters.
  • Logging:
    • NestJS Logger used in service and controller.
    • Logs cover initialization, requests, sending attempts, success (with SID), and failures.
    • Levels/Formats: Consider adjusting levels and using structured JavaScript Object Notation (JSON) logging (e.g., with Pino) in production.
  • Retry Mechanisms:
    • Consider libraries like async-retry or queue systems (Bull) for retrying transient Twilio API errors.
    • Caution: Only retry potentially recoverable errors (network issues, temporary Twilio outages), not permanent ones (invalid number, auth failure). Check Twilio error codes. Avoid creating duplicate messages.

How to Track MMS Messages with Database Integration (Optional)

Track message status and details using Prisma.

  1. Install Prisma: npm install prisma @prisma/client --save-dev

  2. Initialize Prisma: npx prisma init --datasource-provider postgresql (adjust provider). Update DATABASE_URL in .env.

  3. Define Schema (prisma/schema.prisma):

    prisma
    // 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.
  4. Apply Migrations: npx prisma migrate dev --name init-mms-table

  5. Generate Prisma Client: npx prisma generate

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

  7. Inject and Use PrismaService in MmsService:

    typescript
    // 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?
          }
      }
    }
    • 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.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 {}
  • 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 mediaUrls must 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).
  • Twilio Usage Monitoring: Use the Twilio Console Usage section and set up alerts.
  • Dashboards: Visualize key metrics (API performance, MMS volume/failures, Twilio API latency).

Common Twilio MMS Errors and How to Fix Them

  • 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

  1. Build for Production: npm run build (generates dist folder).

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

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

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

How to send MMS messages with NestJS?

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.

What is the Twilio Programmable Messaging API?

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.

Why use NestJS for sending MMS messages?

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.

When should I use MMS instead of SMS?

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.

What are the prerequisites for setting up this project?

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.

How to set up Twilio credentials in a NestJS project?

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.

How do I handle Twilio API errors in my NestJS service?

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.

How to validate incoming MMS requests in NestJS?

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.

Can I send multiple media files in a single MMS message?

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.

How can I improve the performance of MMS sending in NestJS?

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.

What security considerations should I take when sending MMS with Twilio and NestJS?

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.

How do I implement logging and error tracking for my MMS application?

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.

How to troubleshoot Twilio errors when using NestJS?

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.

What are best practices for deploying a NestJS application with Twilio MMS functionality?

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.