Send MMS with Node.js, NestJS, and Infobip
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. We will cover 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 will 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 the MMS message.
Project Overview and Goals
Goal: To 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 recommended) 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.
- 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 often requires specific sender registration or profile setup within the Infobip portal, especially for sending to certain countries or networks. Ensure you have configured this if necessary. Consult Infobip documentation or support for details.
1. Setting up the Project
Let's initialize a new NestJS project and install the necessary dependencies.
-
Create a new NestJS project: Open your terminal and run:
nest new infobip-mms-sender
Choose your preferred package manager (npm or yarn) when prompted.
-
Navigate into the project directory:
cd infobip-mms-sender
-
Install required dependencies: We need the Infobip SDK, configuration management, and validation libraries.
# 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 underdependencies
in yourpackage.json
, notdevDependencies
, as they are needed at runtime.
-
Set up Environment Variables: Create a
.env
file in the project root directory. This file will store sensitive credentials and configuration. Never commit this file to version control. Add it to your.gitignore
file if it's not already there.# .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 likexxxxx.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.
-
Configure the ConfigModule: Modify
src/app.module.ts
to load and make the environment variables available throughout the application using@nestjs/config
.// 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 us to injectConfigService
anywhere without importingConfigModule
repeatedly. -
Enable Global Validation Pipe: Modify
src/main.ts
to automatically validate incoming request payloads based on DTOs (Data Transfer Objects) we will define later.// 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 Core Functionality (MMS Service)
We'll create a dedicated module and service to handle the logic for interacting with the Infobip SDK.
-
Generate the MMS module and service: Use the NestJS CLI to scaffold the necessary files:
nest generate module mms nest generate service mms
This creates
src/mms/mms.module.ts
andsrc/mms/mms.service.ts
(along with a spec file for testing). Remember we already importedMmsModule
intoAppModule
in the previous step. -
Implement the MmsService: Open
src/mms/mms.service.ts
. This service will encapsulate all interactions with the Infobip SDK for sending MMS.// 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 includesto
and acontent
object withmediaUrl
and optionaltext
. Consult the official Infobip MMS API documentation for all available fields.
- Why
3. Building the API Layer (MMS Controller)
Now, let's expose the MMS sending functionality via a REST API endpoint.
-
Generate the MMS controller:
nest generate controller mms
This creates
src/mms/mms.controller.ts
. -
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 filesend-mms.dto.ts
.// 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.
-
Implement the MmsController: Open
src/mms/mms.controller.ts
and define the API 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: /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 aSendMmsDto
instance) into thesendMmsDto
parameter. Validation happens automatically thanks to theValidationPipe
configured inmain.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.
-
Testing the Endpoint with cURL: Start the application:
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.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):
{ "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:
{ "statusCode": 400, "message": [ "to must be a valid phone number" ], "error": "Bad Request" }
4. Integrating with Third-Party Services (Infobip Details)
This section focuses specifically on the Infobip integration aspect.
-
Obtaining Credentials:
- Log in to your Infobip Portal.
- 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.
- Base URL: Your unique Base URL is typically displayed prominently on the dashboard homepage after logging in. Copy this URL.
- 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 andConfigService
provide a type-safe way to access these values within your application (demonstrated inMmsService
). -
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 likenestjs-retry
orasync-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.
- Retries: Implement retry logic within
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
inmain.ts
, returning 400 Bad Request responses. - Infobip API Errors: Caught in
MmsService
'stry/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. UseHttpException
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).
- Validation Errors: Handled automatically by
-
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
orWinston
with structured logging (JSON format) and log rotation/shipping (e.g., usingpino-multi-stream
, sending logs to Datadog, ELK stack). NestJS has integrations for these (NestJS Docs: Logging).
- Built-in Logger: NestJS's
-
Retry Mechanisms (Conceptual Example): Modify
MmsService
to include basic retry logic. (Note: A dedicated library is often better).// 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):
// 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:
- Install TypeORM dependencies:
npm install @nestjs/typeorm typeorm pg
(for PostgreSQL). - Configure
TypeOrmModule
inapp.module.ts
with database connection details (read fromConfigService
). - Inject the
Repository<MmsLog>
intoMmsService
. - Before calling
infobipClient.channels.mms.send
, create and save anMmsLog
entity with statusPENDING
. - After a successful submission, update the entity with the
infobipMessageId
andinfobipBulkId
. - If the submission fails immediately, update the entity with status
FAILED
and the reason. - 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 (usinginfobipMessageId
), and update itsstatus
. (Infobip Docs: Delivery Reports).
- Install TypeORM dependencies:
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 viaSendMmsDto
and the globalValidationPipe
. This prevents malformed data and potential injection attacks at the entry point.whitelist: true
andforbidNonWhitelisted: 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
andConfigService
. 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.
- Handled via
-
Rate Limiting:
- Protect against brute-force attacks and abuse. Use
@nestjs/throttler
. - Install:
npm install @nestjs/throttler
- Configure in
app.module.ts
:// 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
andlimit
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.
- Protect against brute-force attacks and abuse. Use
-
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
:// 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();
- Use the
-
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
oryarn audit
. - Keep dependencies updated.
- Regularly audit your dependencies for known vulnerabilities:
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 thedist
folder. Run the application usingnode 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.