code examples

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

Send MMS with Node.js, NestJS, and Infobip API | Complete Guide

Build a production-ready MMS service with Node.js and NestJS using the Infobip API. Learn setup, validation, error handling, and deployment with code examples.

Send MMS with Node.js, NestJS, and Infobip API

This guide provides a step-by-step walkthrough for building a production-ready service using Node.js and the NestJS framework to send Multimedia Messaging Service (MMS) messages via the Infobip API. You'll learn everything from initial project setup to deployment considerations, focusing on best practices for security, error handling, and maintainability.

By the end of this guide, you'll have a functional NestJS application with an API endpoint capable of accepting MMS requests (recipient number, media URL, and optional text) and using the official Infobip Node.js SDK to send MMS messages.

Project Overview and Goals

Goal: Create a robust backend service that enables sending MMS messages containing media (like images or videos) and text to specified phone numbers using Infobip's communication platform.

Problem Solved: Provides a reusable, reliable, and scalable way to integrate MMS sending capabilities into larger applications, abstracting the direct interaction with the Infobip API into a clean, testable NestJS service.

Technologies:

  • Node.js: The underlying JavaScript runtime environment.
  • NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Chosen for its modular architecture, dependency injection, TypeScript support, and built-in tooling.
  • Infobip API: The third-party service used for sending MMS messages.
  • @infobip-api/sdk: The official Infobip Node.js SDK for simplified API interaction.
  • TypeScript: Enhances JavaScript with static typing for better maintainability and fewer runtime errors.
  • dotenv / @nestjs/config: For managing environment variables securely.
  • class-validator / class-transformer: For robust request payload validation.

System Architecture:

+-------------+ +-----------------+ +---------------+ +-------------+ +--------------+ | Client | ----> | NestJS MMS API | ----> | MmsService | ----> | Infobip SDK | ---> | Infobip API | | (e.g., Web, | | (Controller) | | (Our Logic) | | (@infobip...) | | (MMS Gateway)| | Mobile App) | +-----------------+ +---------------+ +-------------+ +--------------+ +-------------+ | | | | Uses V V +---------------------+ +-------------------+ | Validation (Pipe) | | ConfigService | | (class-validator) | | (@nestjs/config) | +---------------------+ +-------------------+

Prerequisites:

  • Node.js v14 or later (required by @infobip-api/sdk) and npm/yarn installed.
  • An active Infobip account with API key credentials. Create a free trial account here.
  • Basic understanding of TypeScript, REST APIs, and Node.js development.
  • NestJS CLI installed globally: npm install -g @nestjs/cli
  • Access to a publicly hosted media file (image, video, etc.) for testing. Infobip needs to be able to fetch this URL.
  • MMS Media Requirements:
    • Maximum file size: 600 KB (varies by carrier; most US carriers limit to 300–600 KB)
    • Supported formats: JPEG, PNG, GIF (images); MP4, 3GP (video); AMR, MP3 (audio); vCard (contacts)
    • Media URL must be publicly accessible via HTTPS with valid SSL certificate
    • URL must respond within 5 seconds
  • A phone number capable of receiving MMS messages for testing (during the free trial, this must be the number you registered with).
  • Important: MMS sending requires specific sender registration or profile setup within the Infobip portal, especially for sending to certain countries or networks. In the US, this typically requires 10DLC registration or a short code. Ensure you have configured this before sending. Consult Infobip documentation or support for details.

1. Setting Up Your NestJS MMS Project

Initialize a new NestJS project and install the necessary dependencies.

  1. Create a new NestJS project: Open your terminal and run:

    bash
    nest new infobip-mms-sender

    Choose your preferred package manager (npm or yarn) when prompted.

  2. Navigate into the project directory:

    bash
    cd infobip-mms-sender
  3. Install required dependencies: Install the Infobip SDK, configuration management, and validation libraries:

    bash
    # Using npm
    npm install @infobip-api/sdk @nestjs/config class-validator class-transformer
    
    # Or using yarn
    yarn add @infobip-api/sdk @nestjs/config class-validator class-transformer
    • @infobip-api/sdk: The official SDK to interact with Infobip APIs.
    • @nestjs/config: For handling environment variables smoothly.
    • class-validator & class-transformer: To validate incoming request bodies using decorators. Note: Ensure these are listed under dependencies in your package.json, not devDependencies, as they are needed at runtime.
  4. Set up Environment Variables: Create a .env file in the project root directory. This file stores sensitive credentials and configuration. Never commit this file to version control. Add it to your .gitignore file if it's not already there.

    dotenv
    # .env
    
    # Infobip Credentials
    INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY
    INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL
    
    # Optional: Default sender if applicable/required by Infobip for MMS
    # INFOBIP_MMS_SENDER_ID=YourSenderID
    • INFOBIP_API_KEY: Obtain this from your Infobip account dashboard under API Keys.
    • INFOBIP_BASE_URL: Find your unique Base URL on the Infobip dashboard homepage after logging in. It looks something like xxxxx.api.infobip.com.
    • INFOBIP_MMS_SENDER_ID: MMS often requires a registered sender ID or short code. Add this if required by your Infobip setup and regulations.
  5. Configure the ConfigModule: Modify src/app.module.ts to load and make the environment variables available throughout the application using @nestjs/config.

    typescript
    // src/app.module.ts
    import { Module } from '@nestjs/common';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { ConfigModule } from '@nestjs/config'; // Import ConfigModule
    import { MmsModule } from './mms/mms.module'; // We will create this next
    
    @Module({
      imports: [
        ConfigModule.forRoot({
          isGlobal: true, // Make ConfigModule available globally
          envFilePath: '.env', // Specify the env file path
        }),
        MmsModule, // Import the MmsModule
      ],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}

    Setting isGlobal: true allows you to inject ConfigService anywhere without importing ConfigModule repeatedly.

  6. Enable Global Validation Pipe: Modify src/main.ts to automatically validate incoming request payloads based on DTOs (Data Transfer Objects) you'll define later.

    typescript
    // src/main.ts
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { ValidationPipe } from '@nestjs/common'; // Import ValidationPipe
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
    
      // Enable global validation pipe
      app.useGlobalPipes(new ValidationPipe({
        whitelist: true, // Strip properties not defined in DTO
        transform: true, // Automatically transform payloads to DTO instances
        forbidNonWhitelisted: true, // Throw error if non-whitelisted properties are present
      }));
    
      await app.listen(3000);
      console.log(`Application is running on: ${await app.getUrl()}`);
    }
    bootstrap();

2. Implementing the MMS Service with Infobip SDK

Create a dedicated module and service to handle the logic for interacting with the Infobip SDK.

  1. Generate the MMS module and service: Use the NestJS CLI to scaffold the necessary files:

    bash
    nest generate module mms
    nest generate service mms

    This creates src/mms/mms.module.ts and src/mms/mms.service.ts (along with a spec file for testing). Remember we already imported MmsModule into AppModule in the previous step.

  2. Implement the MmsService: Open src/mms/mms.service.ts. This service will encapsulate all interactions with the Infobip SDK for sending MMS.

    typescript
    // src/mms/mms.service.ts
    import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common';
    import { ConfigService } from '@nestjs/config';
    import { Infobip, AuthType } from '@infobip-api/sdk'; // Import Infobip SDK components
    import { SendMmsDto } from './dto/send-mms.dto'; // We will create this DTO next
    
    @Injectable()
    export class MmsService {
      private readonly logger = new Logger(MmsService.name);
      private infobipClient: Infobip;
      // private mmsSenderId?: string; // Uncomment if using a sender ID
    
      constructor(private configService: ConfigService) {
        const apiKey = this.configService.get<string>('INFOBIP_API_KEY');
        const baseUrl = this.configService.get<string>('INFOBIP_BASE_URL');
        // this.mmsSenderId = this.configService.get<string>('INFOBIP_MMS_SENDER_ID'); // Uncomment
    
        if (!apiKey || !baseUrl) {
          throw new Error('Infobip API Key or Base URL not configured in .env file');
        }
    
        this.infobipClient = new Infobip({
          baseUrl: baseUrl,
          apiKey: apiKey,
          authType: AuthType.ApiKey,
        });
    
        this.logger.log('Infobip client initialized.');
      }
    
      /**
       * Sends an MMS message using the Infobip API.
       * @param sendMmsDto - The MMS details (to, mediaUrl, text).
       * @returns The response from the Infobip API.
       * @throws BadRequestException for validation errors or client-side issues.
       * @throws InternalServerErrorException for server-side or Infobip API errors.
       */
      async sendMms(sendMmsDto: SendMmsDto): Promise<any> {
        const { to, mediaUrl, text } = sendMmsDto;
    
        this.logger.log(`Attempting to send MMS to: ${to}`);
    
        // Construct the payload according to Infobip MMS API documentation
        // Reference: https://www.infobip.com/docs/api/channels/mms/send-mms-message
        const payload = {
          // from: this.mmsSenderId, // Uncomment if using sender ID
          to: to,
          content: {
            mediaUrl: mediaUrl,
            text: text, // Text is optional in the content block for MMS
          },
          // Add other optional parameters like notifyUrl, validityPeriod, etc. if needed
          // Example: notifyUrl: 'https://your-callback-url.com/mms-status'
        };
    
        try {
          // Use the SDK's MMS channel method (e.g., channels.mms.send)
          // **IMPORTANT**: Verify the exact method path (`channels.mms.send`) against the
          // current @infobip-api/sdk documentation as SDKs can change.
          const response = await this.infobipClient.channels.mms.send(payload);
    
          this.logger.log(`MMS submitted successfully to ${to}. Message ID: ${response.data?.messages?.[0]?.messageId}, Bulk ID: ${response.data?.bulkId}`);
          // Log relevant parts of the response, avoid logging sensitive data
          this.logger.debug(`Infobip API Response: ${JSON.stringify(response.data)}`);
    
          return response.data; // Return the body of the response
    
        } catch (error: any) { // Explicitly type error as any or unknown for checking properties
          this.logger.error(`Failed to send MMS to ${to}: ${error.message}`, error.stack);
    
          // Handle specific Infobip errors if possible, based on error structure
          // The SDK might throw errors with response details
          if (error.response?.data) {
             this.logger.error(`Infobip Error Details: ${JSON.stringify(error.response.data)}`);
             // **Note**: The error structure (`error.response.data.requestError.serviceException`)
             // is assumed based on common patterns. Always inspect the actual error object
             // received from the SDK in case of failures, or use SDK-provided error type guards
             // if available, as API error structures can change.
             const errorData = error.response.data;
             const serviceException = errorData?.requestError?.serviceException;
             const errorMessage = serviceException?.text || 'Infobip API error';
             const messageId = serviceException?.messageId || 'UNKNOWN';
    
             // Throw specific NestJS exceptions based on error type
             if (messageId.includes('BAD_REQUEST') || error.response.status === 400) {
                 throw new BadRequestException(`Infobip error: ${errorMessage} (ID: ${messageId})`);
             } else {
                 throw new InternalServerErrorException(`Infobip API failed: ${errorMessage} (ID: ${messageId})`);
             }
          }
    
          // Generic fallback error
          throw new InternalServerErrorException(`Failed to send MMS due to an unexpected error.`);
        }
      }
    }
    • Why ConfigService? Securely retrieves credentials from environment variables, avoiding hardcoding.
    • Why Logger? Essential for monitoring requests and diagnosing issues in production.
    • Why Initialize in constructor? Sets up the Infobip client once when the service is instantiated.
    • Why async/await? API calls are asynchronous network operations.
    • Why try/catch? Gracefully handles network issues or API errors from Infobip.
    • Why Specific Exceptions? Translates Infobip errors into standard NestJS HTTP exceptions (BadRequestException, InternalServerErrorException) for consistent API responses.
    • Payload Structure: The payload object directly mirrors the structure required by the Infobip MMS API endpoint (/mms/1/single or /mms/1/bulk depending on the SDK method). Crucially, it includes to and a content object with mediaUrl and optional text. Consult the official Infobip MMS API documentation for all available fields.

3. Building the API Layer (MMS Controller)

Now, expose the MMS sending functionality via a REST API endpoint.

  1. Generate the MMS controller:

    bash
    nest generate controller mms

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

  2. Create the Data Transfer Object (DTO): Define the expected shape and validation rules for the incoming request body. Create a new directory src/mms/dto and a file send-mms.dto.ts.

    typescript
    // src/mms/dto/send-mms.dto.ts
    import { IsNotEmpty, IsString, IsPhoneNumber, IsUrl, IsOptional } from 'class-validator';
    
    export class SendMmsDto {
      @IsNotEmpty()
      @IsPhoneNumber(null) // Use null for basic E.164 format validation, specify region if needed
      @IsString()
      readonly to: string; // Recipient phone number in E.164 format (e.g., +14155552671)
    
      @IsNotEmpty()
      @IsUrl({ require_protocol: true, protocols: ['http', 'https'] }) // Ensure it's a valid HTTP/HTTPS URL
      @IsString()
      readonly mediaUrl: string; // Publicly accessible URL of the media file
    
      @IsOptional() // Text is often optional for MMS
      @IsString()
      @IsNotEmpty() // If provided, it should not be empty
      readonly text?: string; // Optional text message to accompany the media
    }
    • @IsNotEmpty(): Ensures the field is provided.
    • @IsPhoneNumber(): Validates if the string is a valid phone number (basic E.164 check by default).
    • @IsUrl(): Validates if the string is a valid URL (enforcing HTTP/HTTPS).
    • @IsOptional(): Marks the field as optional.
    • @IsString(): Ensures the field is a string.
  3. Implement the MmsController: Open src/mms/mms.controller.ts and define the API 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: /mms
    export class MmsController {
      private readonly logger = new Logger(MmsController.name);
    
      constructor(private readonly mmsService: MmsService) {}
    
      @Post('send') // Route: POST /mms/send
      @HttpCode(HttpStatus.ACCEPTED) // Return 202 Accepted as sending is async
      async sendMms(@Body() sendMmsDto: SendMmsDto): Promise<{ message: string; details: any }> {
        this.logger.log(`Received request to send MMS to: ${sendMmsDto.to}`);
    
        // DTO validation is handled automatically by the global ValidationPipe
    
        const infobipResponse = await this.mmsService.sendMms(sendMmsDto);
    
        // Return a user-friendly response confirming submission
        return {
          message: 'MMS submission accepted by Infobip.',
          details: infobipResponse, // Include the response from Infobip API
        };
      }
    }
    • @Controller('mms'): Defines the base route /mms for all methods in this controller.
    • @Post('send'): Defines a POST endpoint at /mms/send.
    • @Body(): Injects the validated request body (transformed into a SendMmsDto instance) into the sendMmsDto parameter. Validation happens automatically thanks to the ValidationPipe configured in main.ts.
    • @HttpCode(HttpStatus.ACCEPTED): Sets the default HTTP status code to 202, signifying that the request has been accepted for processing, but the processing (actual delivery) hasn't necessarily completed. This is appropriate for asynchronous operations like sending messages.
    • Dependency Injection: MmsService is injected into the controller's constructor by NestJS.
  4. Testing the Endpoint with cURL: Start the application:

    bash
    npm run start:dev

    Open another terminal and use curl (or a tool like Postman) to send a request. Replace placeholders with your actual test number, a valid public media URL, and optionally some text.

    bash
    curl -X POST http://localhost:3000/mms/send \
    -H "Content-Type: application/json" \
    -d '{
      "to": "+14155552671",
      "mediaUrl": "https://www.infobip.com/wp-content/uploads/2022/08/infobip-logo-graph-og.png",
      "text": "Check out this cool logo!"
    }'

    Expected Successful Response (Status Code 202):

    json
    {
      "message": "MMS submission accepted by Infobip.",
      "details": {
        "bulkId": "some-bulk-id-from-infobip",
        "messages": [
          {
            "to": "+14155552671",
            "status": {
              "groupId": 1,
              "groupName": "PENDING",
              "id": 7,
              "name": "PENDING_ENROUTE",
              "description": "Message sent to next instance"
            },
            "messageId": "some-unique-message-id-from-infobip"
          }
        ]
      }
    }

    Example Validation Error Response (Status Code 400): If you provide an invalid phone number:

    json
    {
      "statusCode": 400,
      "message": [
        "to must be a valid phone number"
      ],
      "error": "Bad Request"
    }

4. Integrating with Infobip API (Configuration & Setup)

This section focuses specifically on the Infobip integration aspect.

  • Obtaining Credentials:

    1. Log in to your Infobip Portal.
    2. API Key: Navigate to the ""Developers"" section (or similar, UI might change) -> API Keys. Create a new API key if you don't have one. Copy the key value securely.
    3. Base URL: Your unique Base URL is typically displayed prominently on the dashboard homepage after logging in. Copy this URL.
    4. MMS Sender Setup: This is crucial and varies. You might need to:
      • Register an Alphanumeric Sender ID (if supported for MMS in your region).
      • Purchase and configure a Short Code or Virtual Long Number (VLN).
      • Check specific country regulations within the Infobip portal.
      • Consult Infobip Documentation on ""MMS Global Coverage and Connectivity"" or contact their support for requirements specific to your target countries.
  • Secure Storage (.env): As covered in Setup, store these credentials (INFOBIP_API_KEY, INFOBIP_BASE_URL, INFOBIP_MMS_SENDER_ID if applicable) in the .env file. Ensure .gitignore includes .env.

  • Accessing Credentials (ConfigService): The @nestjs/config module and ConfigService provide a type-safe way to access these values within your application (demonstrated in MmsService).

  • Environment Variables Explained:

    • INFOBIP_API_KEY: (String, Required) Your secret key for authenticating API requests with Infobip. Format: Typically a long alphanumeric string. Obtain from Infobip Portal > Developers > API Keys.
    • INFOBIP_BASE_URL: (String, Required) The unique domain assigned to your Infobip account for API calls. Format: xxxxx.api.infobip.com. Obtain from Infobip Portal Dashboard.
    • INFOBIP_MMS_SENDER_ID: (String, Optional/Conditional) The identifier (e.g., short code, VLN, or registered alphanumeric ID) that the MMS will appear to be sent from. Format: Varies (numeric, alphanumeric). Obtain/Configure via Infobip Portal based on regional requirements and your setup.
  • Fallback Mechanisms: The current implementation relies directly on the Infobip API. For higher availability:

    • Retries: Implement retry logic within MmsService for transient network errors or specific Infobip error codes (e.g., 5xx server errors). Use exponential backoff. Libraries like nestjs-retry or async-retry can help.
    • Alternative Providers: For critical systems, you might integrate a second MMS provider and switch based on Infobip's availability (requires more complex abstraction).
    • Queuing: Implement a message queue (e.g., Redis with BullMQ, RabbitMQ) to decouple the API request from the actual sending process. If Infobip is down, messages wait in the queue.

5. Implementing Error Handling, Logging, and Retries

Robust error handling and logging are critical for production systems.

  • Error Handling Strategy:

    • Validation Errors: Handled automatically by ValidationPipe in main.ts, returning 400 Bad Request responses.
    • Infobip API Errors: Caught in MmsService's try/catch block. Analyze the error structure provided by the SDK.
    • Specific NestJS Exceptions: Translate Infobip errors (or other internal errors) into standard NestJS exceptions (BadRequestException, NotFoundException, InternalServerErrorException, etc.) for consistent API responses. Use HttpException for custom scenarios.
    • Global Exception Filter (Optional): For advanced, centralized error handling and formatting across the entire application, you could implement a global exception filter. (NestJS Docs: Exception Filters).
  • Logging:

    • Built-in Logger: NestJS's Logger is used (@nestjs/common). It provides basic console logging with levels (log, error, warn, debug, verbose).
    • Context: Use new Logger(ServiceName.name) for context (e.g., [MmsService] Log message...).
    • What to Log:
      • Incoming requests (controller level, avoid logging full sensitive DTOs).
      • Key actions (e.g., "Attempting to send MMS to X").
      • Success confirmations (including messageId, bulkId).
      • Errors with stack traces and context (e.g., recipient number).
      • Infobip API error details (if available).
    • Production Logging: For production, consider using more robust logging libraries like Pino or Winston with structured logging (JSON format) and log rotation/shipping (e.g., using pino-multi-stream, sending logs to Datadog, ELK stack). NestJS has integrations for these (NestJS Docs: Logging).
  • Retry Mechanisms (Conceptual Example): Modify MmsService to include basic retry logic. (Note: A dedicated library is often better).

    typescript
    // src/mms/mms.service.ts (Conceptual Retry Addition)
    import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common';
    import { ConfigService } from '@nestjs/config';
    import { Infobip, AuthType } from '@infobip-api/sdk';
    import { SendMmsDto } from './dto/send-mms.dto';
    import { setTimeout } from 'timers/promises'; // For async delay
    
    @Injectable()
    export class MmsService {
      private readonly logger = new Logger(MmsService.name);
      private infobipClient: Infobip;
      // private mmsSenderId?: string;
    
      constructor(private configService: ConfigService) {
        const apiKey = this.configService.get<string>('INFOBIP_API_KEY');
        const baseUrl = this.configService.get<string>('INFOBIP_BASE_URL');
        // this.mmsSenderId = this.configService.get<string>('INFOBIP_MMS_SENDER_ID');
    
        if (!apiKey || !baseUrl) {
          throw new Error('Infobip API Key or Base URL not configured in .env file');
        }
    
        this.infobipClient = new Infobip({
          baseUrl: baseUrl,
          apiKey: apiKey,
          authType: AuthType.ApiKey,
        });
        this.logger.log('Infobip client initialized.');
      }
    
      async sendMms(sendMmsDto: SendMmsDto): Promise<any> {
          const { to, mediaUrl, text } = sendMmsDto;
          const MAX_RETRIES = 3;
          const INITIAL_DELAY_MS = 500;
    
          for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
              this.logger.log(`Attempt ${attempt} to send MMS to: ${to}`);
              const payload = {
                // from: this.mmsSenderId, // Uncomment if using sender ID
                to: to,
                content: { mediaUrl: mediaUrl, text: text },
              };
    
              try {
                  // **IMPORTANT**: Verify the exact method path (`channels.mms.send`) against the
                  // current @infobip-api/sdk documentation as SDKs can change.
                  const response = await this.infobipClient.channels.mms.send(payload);
                  this.logger.log(`MMS submitted successfully on attempt ${attempt}.`);
                  this.logger.debug(`Infobip API Response: ${JSON.stringify(response.data)}`);
                  return response.data;
              } catch (error: any) { // Explicitly type error
                  this.logger.warn(`Attempt ${attempt} failed for ${to}: ${error.message}`);
    
                  // Decide if the error is retryable (e.g., network errors, 5xx server errors)
                  const isRetryable = this.isErrorRetryable(error);
    
                  if (isRetryable && attempt < MAX_RETRIES) {
                      const delay = INITIAL_DELAY_MS * Math.pow(2, attempt - 1); // Exponential backoff
                      this.logger.log(`Retrying in ${delay}ms...`);
                      await setTimeout(delay); // Wait before next attempt
                  } else {
                      this.logger.error(`Failed to send MMS to ${to} after ${attempt} attempts.`);
                      // Handle non-retryable errors or final failure (re-throw as before)
                      if (error.response?.data) {
                         // **Note**: Inspect the actual error object structure.
                          const errorData = error.response.data;
                          const serviceException = errorData?.requestError?.serviceException;
                          const errorMessage = serviceException?.text || 'Infobip API error';
                          const messageId = serviceException?.messageId || 'UNKNOWN';
                          if (messageId.includes('BAD_REQUEST') || error.response?.status === 400) {
                              throw new BadRequestException(`Infobip error: ${errorMessage} (ID: ${messageId})`);
                          } else {
                              throw new InternalServerErrorException(`Infobip API failed after retries: ${errorMessage} (ID: ${messageId})`);
                          }
                      }
                      throw new InternalServerErrorException(`Failed to send MMS after ${attempt} attempts.`);
                  }
              }
          }
           // Should not be reached if MAX_RETRIES > 0, but needed for TS
           throw new InternalServerErrorException(`Failed to send MMS after ${MAX_RETRIES} attempts.`);
      }
    
      private isErrorRetryable(error: any): boolean {
          // Basic check: network errors or Infobip 5xx errors are often retryable
          // Inspect error.code, error.response.status etc.
          if (!error.response) return true; // Assume network error if no response object
          const status = error.response.status;
          return status >= 500 && status <= 599; // Retry on server errors
      }
    }

6. Creating a Database Schema and Data Layer

While this specific service only sends MMS, a real-world application would likely need to track the status and history of sent messages.

  • Purpose: Store details about each MMS sending attempt, including recipient, content (or reference), Infobip message ID, bulk ID, submission status, and potentially delivery status updates (if using webhooks).

  • Technology Choice: Use a database like PostgreSQL, MySQL, or MongoDB. Integrate with NestJS using TypeORM (most popular) or Mongoose (for MongoDB).

  • Schema (Conceptual - e.g., using TypeORM Entities):

    typescript
    // src/mms/entities/mms-log.entity.ts (Example)
    import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
    
    export enum MmsStatus {
      PENDING = 'PENDING', // Submitted to Infobip
      SENT = 'SENT',       // Confirmed sent by Infobip (intermediate)
      DELIVERED = 'DELIVERED', // Confirmed delivered to handset
      FAILED = 'FAILED',     // Failed to send or deliver
      REJECTED = 'REJECTED', // Rejected by Infobip/Carrier
      UNKNOWN = 'UNKNOWN',   // Status unknown or webhook not received
    }
    
    @Entity('mms_logs')
    export class MmsLog {
      @PrimaryGeneratedColumn('uuid')
      id: string;
    
      @Index()
      @Column({ nullable: true })
      infobipMessageId?: string;
    
      @Index()
      @Column({ nullable: true })
      infobipBulkId?: string;
    
      @Column()
      recipientNumber: string;
    
      @Column()
      mediaUrl: string;
    
      @Column({ type: 'text', nullable: true })
      textContent?: string;
    
      @Column({ nullable: true })
      senderId?: string;
    
      @Index()
      @Column({
        type: 'enum',
        enum: MmsStatus,
        default: MmsStatus.PENDING,
      })
      status: MmsStatus;
    
      @Column({ type: 'jsonb', nullable: true }) // Store last raw status from Infobip
      lastInfobipStatusPayload?: object;
    
      @Column({ type: 'text', nullable: true }) // Store error details if failed
      failureReason?: string;
    
      @CreateDateColumn()
      submittedAt: Date; // When we sent it to Infobip
    
      @UpdateDateColumn()
      lastUpdatedAt: Date; // When the status was last updated (e.g., via webhook)
    }
  • Implementation:

    1. Install TypeORM dependencies: npm install @nestjs/typeorm typeorm pg (for PostgreSQL).
    2. Configure TypeOrmModule in app.module.ts with database connection details (read from ConfigService).
    3. Inject the Repository<MmsLog> into MmsService.
    4. Before calling infobipClient.channels.mms.send, create and save an MmsLog entity with status PENDING.
    5. After a successful submission, update the entity with the infobipMessageId and infobipBulkId.
    6. If the submission fails immediately, update the entity with status FAILED and the reason.
    7. Webhooks: To get delivery status updates, configure a webhook URL in Infobip pointing to another endpoint in your NestJS app. This endpoint would receive status updates, find the corresponding MmsLog record (using infobipMessageId), and update its status. (Infobip Docs: Delivery Reports).

7. Adding Security Features

Security is paramount, especially when dealing with APIs and user data.

  • Input Validation and Sanitization:

    • class-validator / class-transformer: Already implemented via SendMmsDto and the global ValidationPipe. This prevents malformed data and potential injection attacks at the entry point. whitelist: true and forbidNonWhitelisted: true are important settings.
    • Sanitization: While validation helps, explicit sanitization might be needed depending on how data is used later (e.g., if displaying user-provided text). Libraries like sanitize-html can be used if rendering content. For this service, strict validation is the primary defense.
  • API Key Security:

    • Handled via .env and ConfigService. Ensure the .env file has restricted permissions on the server and is never committed to git.
    • Consider using a secrets management service (like AWS Secrets Manager, HashiCorp Vault) for production environments instead of .env files.
  • Rate Limiting:

    • Protect against brute-force attacks and abuse. Use @nestjs/throttler.
    • Install:
      bash
      npm install @nestjs/throttler
    • Configure in app.module.ts:
      typescript
      // src/app.module.ts
      import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
      import { APP_GUARD } from '@nestjs/core';
      // ... other imports
      
      @Module({
        imports: [
          // ... other modules (ConfigModule, MmsModule)
          ThrottlerModule.forRoot([{
            ttl: 60000, // Time-to-live (milliseconds) - 1 minute
            limit: 10,  // Max requests per TTL per IP
          }]),
        ],
        // ... controllers, providers
        providers: [
          AppService,
          // Apply ThrottlerGuard globally
          {
            provide: APP_GUARD,
            useClass: ThrottlerGuard,
          },
        ],
      })
      export class AppModule {}
    • This basic setup limits each IP address to 10 requests per minute across all endpoints. Configure ttl and limit based on your expected usage and security needs. You can also apply rate limiting more granularly using the @Throttle() decorator on specific controllers or routes.
  • HTTPS:

    • Always use HTTPS in production. This is typically handled by a reverse proxy (like Nginx or Caddy) or your hosting platform (e.g., AWS Load Balancer, Heroku), which terminates SSL and forwards requests to your Node.js application over HTTP locally. Ensure your NestJS app is configured to trust the proxy headers if necessary.
  • Helmet:

    • Use the helmet middleware (via @nestjs/helmet) to set various security-related HTTP headers (e.g., X-Content-Type-Options, Strict-Transport-Security, X-Frame-Options).
    • Install: npm install helmet @nestjs/helmet
    • Enable in main.ts:
      typescript
      // src/main.ts
      import helmet from 'helmet';
      // ... other imports
      
      async function bootstrap() {
        const app = await NestFactory.create(AppModule);
        app.use(helmet()); // Apply helmet middleware
        // ... other app configurations (ValidationPipe, etc.)
        await app.listen(3000);
        // ...
      }
      bootstrap();
  • Authentication/Authorization (If Applicable):

    • If this API is not public, implement proper authentication (e.g., API keys for client applications, JWT for users) and authorization (ensuring the authenticated entity has permission to send MMS). NestJS provides robust mechanisms for this (NestJS Docs: Authentication).
  • Dependency Security:

    • Regularly audit your dependencies for known vulnerabilities: npm audit or yarn audit.
    • Keep dependencies updated.

8. Deployment Considerations

Moving from development to production requires careful planning.

  • Environment Configuration: Use environment variables (.env locally, system environment variables or secrets management in production) for all configuration, especially sensitive data like API keys and database credentials. Do not hardcode production values.
  • Build Process: Create a production build: npm run build. This compiles TypeScript to JavaScript in the dist folder. Run the application using node dist/main.js.
  • Process Management: Run your Node.js application using a process manager like PM2, systemd, or within a container orchestrator (Docker/Kubernetes). This handles restarting the app on crashes, managing logs, and utilizing multiple CPU cores.
  • HTTPS: As mentioned in Security, ensure traffic is served over HTTPS, typically via a reverse proxy or load balancer.
  • Monitoring & Logging: Set up robust logging (shipping logs to a central system like ELK, Datadog, CloudWatch) and application performance monitoring (APM) tools to track errors, performance bottlenecks, and system health.
  • Database Migrations (If using a DB): Use a migration tool (like TypeORM migrations) to manage database schema changes reliably across different environments.

Conclusion

You have successfully built a NestJS application capable of sending MMS messages via the Infobip API. This service includes essential features like configuration management, input validation, basic error handling, and logging. We've covered the core implementation from project setup to controller and service logic, including interacting with the Infobip SDK. Remember to consult the official Infobip API documentation for specific payload options and sender requirements.


Further Enhancements

  • Implement Delivery Status Webhooks: Create an endpoint to receive delivery reports from Infobip and update the status in your database (if implemented).
  • Advanced Retry Logic: Use a dedicated library like nestjs-retry for more sophisticated retry strategies.
  • Message Queuing: Decouple the API request from the sending process using a queue (e.g., BullMQ, RabbitMQ) for better resilience and scalability.
  • Comprehensive Testing: Add unit tests for the service and controller logic, and integration tests to verify the flow (potentially mocking the Infobip SDK).
  • Rate Limiting per User/API Key: If authenticating clients, implement rate limiting based on the authenticated entity rather than just IP address.
  • Secrets Management: Integrate with a proper secrets management solution for production environments.

Frequently Asked Questions

How to send MMS with NestJS and Infobip?

Create a NestJS service that uses the Infobip Node.js SDK (@infobip-api/sdk) to interact with the Infobip API. This service should handle constructing the MMS payload (recipient, media URL, text) and making the API call to send the message. A controller exposes this functionality via a REST endpoint, typically accepting a POST request with the MMS details.

What is the Infobip Node.js SDK used for?

The Infobip Node.js SDK (`@infobip-api/sdk`) simplifies interaction with the Infobip API. It provides methods for various communication channels, including MMS, making it easier to send messages without manually constructing HTTP requests and handling responses. Be sure to check the official Infobip SDK documentation for the most up-to-date usage.

Why use NestJS for sending MMS messages?

NestJS provides a structured, maintainable framework for building server-side applications. Its modular architecture, dependency injection, and TypeScript support make it ideal for creating robust and scalable MMS sending services. This framework also facilitates testing and integration with other systems.

When should I configure MMS sender settings in Infobip?

MMS sender setup (like registering a Sender ID or short code) is crucial and must be done *before* you can effectively send MMS messages, especially to certain countries. This is often a requirement of regulatory bodies and telecommunication networks. Consult Infobip's documentation for specific requirements based on your target regions.

Can I send MMS messages with Node.js without NestJS?

Yes, you can use the Infobip Node.js SDK directly in a plain Node.js application without a framework. However, NestJS simplifies the overall structure and provides benefits such as dependency injection and a modular design, resulting in more maintainable code. It's generally recommended for larger projects.

How to handle Infobip API errors in NestJS?

Implement a `try/catch` block around the Infobip SDK call in your NestJS service. Inspect the `error` object provided by the SDK, often containing details from the Infobip API response, and use appropriate status codes and messages in your response. Translate these into standard NestJS HTTP exceptions for consistency.

What is class-validator used for in the MMS service?

`class-validator` is used for validating incoming requests using decorators. Create a Data Transfer Object (DTO) with validation decorators (e.g., `@IsNotEmpty`, `@IsPhoneNumber`, `@IsUrl`). NestJS's `ValidationPipe` automatically validates requests against the DTO, ensuring data integrity and security.

Why is a 202 Accepted status code used for the send MMS endpoint?

Sending MMS is an asynchronous operation; the server accepts the request but doesn't immediately complete the message delivery. A 202 Accepted indicates the request has been received and is being processed in the background, allowing for eventual delivery confirmation via webhooks or other mechanisms.

How to store Infobip API credentials securely in NestJS?

Use environment variables, stored in a `.env` file locally. Never commit this file to version control. For production, use system environment variables or a dedicated secrets management service (AWS Secrets Manager, HashiCorp Vault) for increased security.

What is the purpose of a message queue in the context of sending MMS?

A message queue (like Redis with BullMQ or RabbitMQ) decouples the API request from the actual message sending. If the Infobip API is temporarily unavailable, messages queue up and are sent later when the service is restored, increasing system resilience.

How to implement rate limiting for the MMS API?

Use the `@nestjs/throttler` module. Configure it globally in `app.module.ts` to limit requests per IP or apply it to specific routes using the `@Throttle()` decorator. This helps prevent abuse and protects your application from excessive load.

What is the recommended Node.js version for this project?

Node.js version 14 or later is recommended for compatibility with NestJS, the Infobip SDK, and other dependencies. Make sure your environment meets this requirement to avoid potential issues.

How to add retry logic for failed MMS submissions?

In your `MmsService`, wrap the Infobip API call in a loop that retries a set number of times with exponential backoff. You can implement this manually or use a dedicated library such as `nestjs-retry` or `async-retry` for more robust handling.

How to handle different MMS statuses from Infobip?

Use webhooks. Configure a webhook URL in the Infobip portal that points to an endpoint in your NestJS application. This endpoint will receive delivery reports and allow you to update the message status in your database based on the webhook payload.

How do I test the send MMS endpoint locally?

After starting your NestJS application (`npm run start:dev`), use a tool like `curl` or Postman to send a POST request to the `/mms/send` endpoint. The request body should be JSON containing the recipient number, media URL, and optional text. Replace placeholder values with real test data.