Send MMS Messages with NestJS and MessageBird
This guide provides a step-by-step walkthrough for building a production-ready NestJS application capable of sending Multimedia Messaging Service (MMS) messages using the MessageBird API. We'll cover project setup, core implementation, error handling, security, testing, and deployment.
By the end of this tutorial, you will have a robust NestJS service and API endpoint to send MMS messages, complete with media attachments, to recipients in the US and Canada.
Target Audience: Developers familiar with Node.js, TypeScript, and the NestJS framework. Basic understanding of REST APIs is assumed.
Technologies Used:
- NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications.
- TypeScript: A typed superset of JavaScript that compiles to plain JavaScript.
- MessageBird SDK: The official Node.js library for interacting with the MessageBird API.
- dotenv: For managing environment variables.
- class-validator & class-transformer: For request payload validation.
Project Overview and Goals
Goal: To create a reusable NestJS module that encapsulates the logic for sending MMS messages via MessageBird, exposed through a simple REST API endpoint.
Problem Solved: Provides a structured, maintainable, and testable way to integrate MMS sending capabilities into any NestJS application, handling configuration, API interaction, and basic error management.
System Architecture:
+-------------+ +---------------------+ +-------------------+ +-----------------+
| Client | ---> | NestJS API Endpoint | ---> | NestJS MMS Service | ---> | MessageBird API |
| (e.g., curl)| | (MMS Controller) | | (MessageBird SDK) | | (rest.messagebird.com/mms) |
+-------------+ +---------------------+ +-------------------+ +-----------------+
| | |
| POST /mms/send | Initializes SDK, | Sends MMS Request
| (JSON Payload) | calls sendMms method |
| | | Receives Response/Status
|<--------------------|<----------------------------|<-----------------
| | |
| HTTP Response | Returns result/error |
| (Success/Error) | |
Prerequisites:
- Node.js (LTS version recommended)
- npm or yarn package manager
- NestJS CLI installed (
npm install -g @nestjs/cli
) - A MessageBird account (Sign up here)
- A MessageBird API Access Key (Live or Test)
- An MMS-enabled virtual mobile number purchased from MessageBird (currently limited to US/Canada for sending MMS).
1. Setting up the Project
Let's start by creating a new NestJS project and installing the necessary dependencies.
-
Create a new NestJS project: Open your terminal and run:
nest new nestjs-messagebird-mms cd nestjs-messagebird-mms
-
Install Dependencies: We need the MessageBird SDK and modules for configuration and validation.
npm install messagebird @nestjs/config dotenv class-validator class-transformer # or using yarn: # yarn add messagebird @nestjs/config dotenv class-validator class-transformer
-
Configure Environment Variables: Sensitive information like API keys should not be hardcoded. We'll use a
.env
file.-
Create a file named
.env
in the project root directory. -
Add the following variables_ replacing the placeholder values with your actual MessageBird credentials:
# .env # Obtain from MessageBird Dashboard: Developers -> API access -> Live API Key (or Test API Key) MESSAGEBIRD_ACCESS_KEY=YOUR_MESSAGEBIRD_ACCESS_KEY # Your MMS-enabled virtual number from MessageBird (E.164 format) # Obtain from MessageBird Dashboard: Numbers MESSAGEBIRD_ORIGINATOR=+1XXXXXXXXXX
-
Important: Add
.env
to your.gitignore
file to prevent committing secrets.# .gitignore (ensure this line exists or add it) .env
-
-
Set up Configuration Module: NestJS provides a
ConfigModule
to load environment variables.-
Import and configure
ConfigModule
in your main application module (src/app.module.ts
). Make it global so it's available everywhere.// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { MmsModule } from './mms/mms.module'; // We will create this next @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Make config available globally envFilePath: '.env', // Specify the env file path }), MmsModule, // Import our upcoming MMS module ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
Why
.env
andConfigModule
? This approach separates configuration from code, making it easy to manage different environments (development, staging, production) and keeps sensitive credentials secure. -
2. Implementing Core Functionality (MMS Service)
We'll create a dedicated service to handle the interaction with the MessageBird API.
-
Generate the MMS Module and Service: Use the NestJS CLI to scaffold the necessary files.
nest generate module mms nest generate service mms --no-spec # --no-spec skips generating a test file for now
This creates
src/mms/mms.module.ts
andsrc/mms/mms.service.ts
. -
Implement the
MmsService
: This service will initialize the MessageBird client and contain the logic for sending MMS messages.- Inject
ConfigService
to access environment variables. - Import the
messagebird
library. - Create a method
sendMms
that accepts recipient(s), message body, media URLs, and an optional subject.
// src/mms/mms.service.ts import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as MessageBird from 'messagebird'; // Use namespace import @Injectable() export class MmsService { private readonly logger = new Logger(MmsService.name); private messagebird: MessageBird.MessageBird; // Use the correct type (Verify against SDK) private originator: string; constructor(private configService: ConfigService) { const accessKey = this.configService.get<string>('MESSAGEBIRD_ACCESS_KEY'); this.originator = this.configService.get<string>('MESSAGEBIRD_ORIGINATOR'); if (!accessKey || !this.originator) { this.logger.error('MessageBird Access Key or Originator not configured in .env file'); throw new InternalServerErrorException('MMS service configuration is incomplete.'); } // Initialize MessageBird client using initClient method this.messagebird = MessageBird.initClient(accessKey); this.logger.log('MessageBird client initialized.'); } /** * Sends an MMS message using the MessageBird API. * @param recipients - Array of recipient phone numbers in E.164 format. * @param body - The text body of the MMS. * @param mediaUrls - Array of publicly accessible URLs for media attachments. * @param subject - (Optional) The subject line of the MMS. * @param reference - (Optional) A client reference string. * @returns The response object from the MessageBird API. * @throws BadRequestException if input validation fails. * @throws InternalServerErrorException if the API call fails. */ async sendMms( recipients: string[], body: string, mediaUrls: string[], subject?: string, reference?: string, ): Promise<any> { // TODO: Define a specific TypeScript interface for the MessageBird MMS response instead of 'any' // Basic input validation (complementary to DTO validation) if (!body && (!mediaUrls || mediaUrls.length === 0)) { throw new BadRequestException('MMS must have either a body or at least one mediaUrl.'); } if (mediaUrls && mediaUrls.length > 10) { // Note: DTO validation should ideally catch this first throw new BadRequestException('MMS cannot have more than 10 media attachments.'); } const params: MessageBird.MmsParams = { originator: this.originator, recipients: recipients, body: body, mediaUrls: mediaUrls, subject: subject, // Optional reference: reference, // Optional }; this.logger.log(`Attempting to send MMS to ${recipients.join(', ')} from ${this.originator}`); return new Promise((resolve, reject) => { // Note: Verify the exact method (e.g., `mms.create` or potentially `messages.create` with specific options) // against the current MessageBird Node.js SDK documentation, as SDKs can evolve. // This code assumes `mms.create` exists. this.messagebird.mms.create(params, (err, response) => { if (err) { this.logger.error('MessageBird API error:', err); // Provide more specific error handling based on err type if possible reject(new InternalServerErrorException(`Failed to send MMS: ${err.message || 'Unknown API error'}`)); } else { this.logger.log(`MMS sent successfully. Message ID: ${response.id}`); resolve(response); } }); }); } }
Why this structure? Encapsulating the MessageBird logic in a dedicated service makes the code modular, easier to test (by mocking the service), and reusable across different parts of your application (e.g., controllers, background jobs).
- Inject
-
Update the
MmsModule
: Ensure theMmsService
is provided and exported by the module.// src/mms/mms.module.ts import { Module } from '@nestjs/common'; import { MmsService } from './mms.service'; // Import MmsController later when created // import { MmsController } from './mms.controller'; @Module({ // controllers: [MmsController], // Uncomment when controller is added providers: [MmsService], exports: [MmsService], // Export if needed by other modules }) export class MmsModule {}
3. Building the API Layer (MMS Controller)
Now, let's expose the sendMms
functionality through a REST API endpoint.
-
Generate the MMS Controller:
nest generate controller mms --no-spec
This creates
src/mms/mms.controller.ts
. -
Create a Data Transfer Object (DTO) for Validation: DTOs define the expected shape of the request body and enable automatic validation using
class-validator
.- Create a directory
src/mms/dto
and a filesend-mms.dto.ts
inside it.
// src/mms/dto/send-mms.dto.ts import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; // For Swagger documentation import { IsArray, IsString, IsNotEmpty, IsPhoneNumber, ArrayMaxSize, ArrayMinSize, IsOptional, MaxLength, IsUrl, ValidateIf } from 'class-validator'; export class SendMmsDto { @ApiProperty({ description: 'Array of recipient phone numbers in E.164 format (e.g., +12223334444). Max 50.', example: ['+15551234567', '+15559876543'], type: [String], }) @IsArray() @ArrayMinSize(1) @ArrayMaxSize(50) // Verify this limit with MessageBird docs @IsPhoneNumber(undefined, { each: true, message: 'Each recipient must be a valid phone number in E.164 format' }) recipients: string[]; @ApiPropertyOptional({ description: 'The text body of the MMS message. Required if mediaUrls is empty or not provided. Max 2000 chars.', example: 'Check out this cool logo!', maxLength: 2000, // Verify this limit with MessageBird docs }) @IsOptional() @ValidateIf(o => !o.mediaUrls || o.mediaUrls.length === 0) // Require body if mediaUrls is absent/empty @IsNotEmpty({ message: 'Body cannot be empty if mediaUrls is not provided or empty.' }) @IsString() @MaxLength(2000) body?: string; @ApiPropertyOptional({ description: 'Array of publicly accessible URLs for media attachments. Required if body is empty or not provided. Max 10 URLs.', example: ['https://developers.messagebird.com/img/logos/mb-400.jpg'], type: [String], maxLength: 10, }) @IsOptional() @ValidateIf(o => !o.body) // Require mediaUrls if body is absent/empty @IsNotEmpty({ message: 'mediaUrls cannot be empty if body is not provided or empty.' }) @IsArray() @ArrayMaxSize(10) // Verify this limit with MessageBird docs @IsUrl({}, { each: true, message: 'Each media URL must be a valid URL' }) mediaUrls?: string[]; @ApiPropertyOptional({ description: 'The subject line of the MMS message. Max 256 chars.', example: 'Company Logo', maxLength: 256, // Verify this limit with MessageBird docs }) @IsOptional() @IsString() @MaxLength(256) subject?: string; @ApiPropertyOptional({ description: 'A client reference string for tracking.', example: 'campaign-xyz-123', }) @IsOptional() @IsString() reference?: string; // Note: The validation logic to ensure *at least one* of body or mediaUrls is present // is now handled by the @ValidateIf decorators above. The service layer provides a fallback check. }
Why DTOs? They provide clear contracts for your API, enable automatic request validation (preventing invalid data from reaching your service), and integrate well with tools like Swagger for API documentation. The
@ValidateIf
decorators allow conditional validation. - Create a directory
-
Implement the
MmsController
:- Inject the
MmsService
. - Create a POST endpoint
/mms/send
. - Use the
SendMmsDto
with the@Body()
decorator and enable validation globally.
// src/mms/mms.controller.ts import { Controller, Post, Body, ValidationPipe, UsePipes, Logger, HttpException, HttpStatus } from '@nestjs/common'; import { MmsService } from './mms.service'; import { SendMmsDto } from './dto/send-mms.dto'; import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger'; // For Swagger @ApiTags('MMS') // Group endpoints in Swagger UI @Controller('mms') export class MmsController { private readonly logger = new Logger(MmsController.name); constructor(private readonly mmsService: MmsService) {} @Post('send') @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) // Enable validation @ApiOperation({ summary: 'Send an MMS message' }) // Swagger description @ApiBody({ type: SendMmsDto }) // Describe the body for Swagger @ApiResponse({ status: 201, description: 'MMS sent successfully, returns MessageBird response.' }) @ApiResponse({ status: 400, description: 'Bad Request - Invalid input data.' }) @ApiResponse({ status: 500, description: 'Internal Server Error - Failed to send MMS.' }) async sendMms(@Body() sendMmsDto: SendMmsDto) { this.logger.log(`Received request to send MMS: ${JSON.stringify(sendMmsDto)}`); try { // DTO validation handles most cases, service layer handles the rest. const result = await this.mmsService.sendMms( sendMmsDto.recipients, sendMmsDto.body ?? '', // Provide empty string if body is undefined (should be validated by DTO) sendMmsDto.mediaUrls ?? [], // Provide empty array if mediaUrls is undefined (should be validated by DTO) sendMmsDto.subject, sendMmsDto.reference, ); // Consider mapping the result to a cleaner response DTO if needed return result; // NestJS automatically sets status to 201 for POST } catch (error) { this.logger.error(`Error sending MMS: ${error.message}`, error.stack); if (error instanceof HttpException) { throw error; // Re-throw known HTTP exceptions (like BadRequestException from service) } // Throw a generic server error for unknown issues (e.g., SDK internal errors) throw new HttpException('Failed to send MMS due to an internal error.', HttpStatus.INTERNAL_SERVER_ERROR); } } }
- Inject the
-
Enable Global Validation Pipe: To make the
ValidationPipe
work automatically for all incoming requests with DTOs, add it insrc/main.ts
.// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe, Logger } from '@nestjs/common'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; // Import Swagger async function bootstrap() { const app = await NestFactory.create(AppModule); const logger = new Logger('Bootstrap'); // 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 provided // Consider adding `disableErrorMessages: process.env.NODE_ENV === 'production'` // to avoid leaking validation details in production errors. })); // --- Swagger Setup --- const config = new DocumentBuilder() .setTitle('NestJS MessageBird MMS API') .setDescription('API for sending MMS messages via MessageBird') .setVersion('1.0') .addTag('MMS') // Match the tag used in the controller .build(); const document = SwaggerModule.createDocument(app, config); SwaggerModule.setup('api-docs', app, document); // Serve Swagger UI at /api-docs logger.log('Swagger UI available at /api-docs'); // --- End Swagger Setup --- const port = process.env.PORT || 3000; await app.listen(port); logger.log(`Application listening on port ${port}`); } bootstrap();
-
Register the Controller: Add
MmsController
to thecontrollers
array insrc/mms/mms.module.ts
.// src/mms/mms.module.ts import { Module } from '@nestjs/common'; import { MmsService } from './mms.service'; import { MmsController } from './mms.controller'; // Import the controller @Module({ imports: [], // ConfigModule is global, no need to import here controllers: [MmsController], // Add the controller providers: [MmsService], exports: [MmsService], }) export class MmsModule {}
4. Integrating with Third-Party Services (MessageBird)
This section revisits the specifics of the MessageBird integration.
- Configuration: Done via
.env
andConfigModule
(Section 1). - API Keys: Stored securely in
.env
and accessed viaConfigService
.- How to obtain:
- Log in to your MessageBird Dashboard.
- Navigate to Developers in the left sidebar (Verify this path in the current Dashboard UI).
- Click on API access (Verify this path).
- Copy your Live API Key (or switch to Test mode for the Test API Key). Use the Live key for actual sending.
- How to obtain:
- Originator Number: Stored in
.env
.- How to obtain/ensure MMS enabled:
- In the MessageBird Dashboard, navigate to Numbers (Verify this path).
- Purchase a US or Canadian number if you don't have one.
- Ensure the number's capabilities include MMS. Not all numbers support MMS. You might need to specifically look for or request MMS enablement for a number. Verify this process in the Dashboard.
- How to obtain/ensure MMS enabled:
- SDK Initialization: Handled in the
MmsService
constructor (Section 2). - API Interaction: Encapsulated within the
MmsService.sendMms
method (Section 2). Remember to verify the SDK method (mms.create
) against current documentation. - Fallback Mechanisms: This simple example doesn't include automatic fallback (e.g., sending an SMS if MMS fails or is unsupported). For production, you might consider:
- Checking recipient capabilities beforehand (if possible via MessageBird's Lookup API - separate integration).
- Implementing logic to attempt SMS via
messagebird.messages.create
if the MMS call fails with specific error codes indicating incompatibility.
5. Error Handling, Logging, and Retry Mechanisms
- Consistent Error Strategy: We use NestJS's built-in
HttpException
for API-level errors (like validation failures handled byValidationPipe
or explicitBadRequestException
throws in the service) andInternalServerErrorException
for service-level or unexpected SDK/API errors. Custom Exception Filters can be created for more advanced, application-wide error formatting. - Logging: NestJS's built-in
Logger
is used in the service and controller to log key events (initialization, requests, success, errors). For production, consider integrating more advanced logging libraries (like Pino or Winston) and sending logs to a centralized platform (e.g., Datadog, Logstash, CloudWatch).- Log Analysis: In a real system, you'd filter logs by context (
MmsService
,MmsController
), timestamp, and severity (Error, Log, Warn) to trace requests and diagnose issues. Searching for specific MessageBird error messages or transaction IDs logged would be crucial.
- Log Analysis: In a real system, you'd filter logs by context (
- Retry Mechanisms:
- MessageBird Internal Retries: MessageBird itself handles retries for delivering the message to the carrier network if temporary issues occur.
- Application-Level Retries: Retrying the API call from our application might be necessary for transient network errors between our server and MessageBird. This example does not implement retries for the
messagebird.mms.create
call. For critical applications, you could add this using:- A library like
nestjs-retry
orasync-retry
. - Custom logic with exponential backoff within the
MmsService.sendMms
method's error handling block, carefully deciding which errors are retryable (e.g., network timeouts vs. invalid recipient errors).
- A library like
6. Database Schema and Data Layer (Optional)
While not strictly required for sending an MMS, storing message details is vital for tracking, reporting, and correlating status updates (if receiving webhooks).
-
Why add a database?
- Log every outgoing MMS attempt.
- Store the
messageId
returned by MessageBird. - Record recipient, content, and timestamp.
- Update the status based on MessageBird webhooks (requires implementing a webhook receiver endpoint).
-
Suggested Schema (using TypeORM entities as an example):
// src/mms/entities/mms-log.entity.ts (Example using TypeORM) import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index, UpdateDateColumn } from 'typeorm'; // Note: Verify these statuses against actual MessageBird webhook documentation if implementing status updates. export enum MmsStatus { PENDING = 'pending', // Initial state before sending API call SENT = 'sent', // API call successful, MB accepted it DELIVERED = 'delivered', // Confirmed delivery via webhook FAILED = 'failed', // API call failed or delivery failed via webhook BUFFERED = 'buffered', // Status from MB EXPIRED = 'expired', // Status from MB REJECTED = 'rejected', // Status from MB // Add other relevant MessageBird statuses as needed } @Entity('mms_logs') export class MmsLog { @PrimaryGeneratedColumn('uuid') id: string; @Index() @Column({ nullable: true }) // Store the ID returned by MessageBird messageBirdId?: string; @Column('simple-array') recipients: string[]; @Column() originator: string; @Column({ type: 'text', nullable: true }) subject?: string; @Column({ type: 'text', nullable: true }) body?: string; @Column('simple-array', { nullable: true }) mediaUrls?: string[]; @Column({ nullable: true }) reference?: string; @Index() @Column({ type: 'enum', enum: MmsStatus, default: MmsStatus.PENDING, }) status: MmsStatus; @Column({ type: 'text', nullable: true }) // Store error details if sending failed errorMessage?: string; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() // Automatically update timestamp on modification updatedAt: Date; @Column({ nullable: true }) // Store status update time from webhook explicitely if needed statusUpdatedAt?: Date; }
-
Implementation:
- Set up a database connection (e.g., using
@nestjs/typeorm
andpg
ormysql2
). - Define the
MmsLog
entity. - Inject the
Repository<MmsLog>
intoMmsService
. - In
sendMms
:- Create and save an
MmsLog
entity withstatus: MmsStatus.PENDING
before calling the MessageBird API. - If the API call succeeds, update the log entry with the
messageBirdId
and setstatus: MmsStatus.SENT
. - If the API call fails, update the log entry with
status: MmsStatus.FAILED
and store theerrorMessage
.
- Create and save an
- Set up a database connection (e.g., using
-
Migrations: Use TypeORM migrations or Prisma Migrate to manage schema changes.
7. Security Features
- Input Validation: Handled by
class-validator
DTOs and the globalValidationPipe
(Section 3). This prevents malformed data and potential injection attacks through request payloads. Thewhitelist: true
andforbidNonWhitelisted: true
options are crucial. - API Key Security: Storing the
MESSAGEBIRD_ACCESS_KEY
in.env
and adding.env
to.gitignore
prevents accidental exposure (Section 1). Use environment variable management features of your deployment platform. - Rate Limiting: Protect your API from abuse. Use
@nestjs/throttler
to limit the number of requests per IP address or user.Configure it innpm install @nestjs/throttler
app.module.ts
:// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; import { APP_GUARD } from '@nestjs/core'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { MmsModule } from './mms/mms.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }), ThrottlerModule.forRoot([{ ttl: 60000, // Time-to-live in milliseconds (e.g., 60 seconds) limit: 10, // Max requests per TTL per IP (adjust as needed) }]), MmsModule, // HealthModule if added ], controllers: [AppController], // Add HealthController if added providers: [ AppService, { provide: APP_GUARD, // Apply rate limiting globally useClass: ThrottlerGuard, }, ], }) export class AppModule {}
- Authentication/Authorization: This guide assumes the API endpoint is internal or protected by other means (e.g., API Gateway, firewall). If exposed directly to end-users, implement proper authentication (e.g., JWT, OAuth) using NestJS Guards to ensure only authorized clients can send MMS.
- HTTPS: Always deploy your NestJS application behind a reverse proxy (like Nginx or Caddy) configured for HTTPS, or use a PaaS that handles TLS termination.
8. Handling Special Cases
- Media Attachments:
- URL Accessibility: URLs provided in
mediaUrls
must be publicly accessible over the internet. MessageBird needs to fetch them. - Size Limit: Each file must be under 1MB (1024KB) - Verify this limit.
- File Types: Refer to the MessageBird documentation for the extensive list of supported MIME types (audio, video, image, text, pdf, etc.). Ensure your media files conform.
- Count Limit: Maximum 10 media URLs per message - Verify this limit.
- Fetch Timeout: MessageBird needs to fetch the media within ~5 seconds - Verify this timeout. Ensure your media hosting is fast and reliable.
- URL Accessibility: URLs provided in
- Body and Media: At least one of
body
ormediaUrls
must be provided. The DTO validation enforces this. - Character Limits: Subject (256 chars), Body (2000 chars) - Verify these limits. The DTO enforces this.
- Recipient Format: Must be E.164 (e.g.,
+15551234567
). The DTO validator checks this. Max 50 recipients per request - Verify this limit. - Originator: Must be an MMS-enabled number from your MessageBird account, formatted in E.164. Using an invalid or non-MMS originator will result in errors.
- US/Canada Only: Sending MMS via MessageBird is currently restricted to these countries. Attempting to send elsewhere will fail. Verify current regional availability.
9. Performance Optimizations
- Batching Recipients: The MessageBird API allows sending to up to 50 recipients (verify limit) in a single
/mms
POST request. If sending the same message to multiple users, batch them into arrays of up to 50 for therecipients
parameter in your DTO/service call. This significantly reduces the number of API calls. - Asynchronous Processing: For high-volume sending, consider moving the
mmsService.sendMms
call to a background job queue (e.g., using BullMQ and@nestjs/bull
). The API endpoint would simply queue the job and return immediately (HTTP 202 Accepted), improving responsiveness. The background worker then handles the actual API interaction. - Connection Pooling: Ensure your database connection (if used) employs pooling to efficiently manage connections under load. This is typically handled by TypeORM or Prisma configurations.
- Resource Usage: Monitor CPU and memory usage of your NestJS application under load. Optimize code paths, potentially using Node.js clustering or deploying multiple instances behind a load balancer.
10. Monitoring, Observability, and Analytics
- Health Checks: Implement a health check endpoint using
@nestjs/terminus
. This allows load balancers or monitoring systems to verify the application's status.Create a simple health controller:npm install @nestjs/terminus @nestjs/axios # @nestjs/axios provides HttpModule
Remember to create a// src/health/health.controller.ts import { Controller, Get } from '@nestjs/common'; import { HealthCheckService, HttpHealthIndicator, HealthCheck, HealthCheckResult } from '@nestjs/terminus'; import { ConfigService } from '@nestjs/config'; import { Public } from '../auth/decorators/public.decorator'; // Example if you have global auth guard @Controller('health') export class HealthController { constructor( private health: HealthCheckService, private http: HttpHealthIndicator, private configService: ConfigService, ) {} @Get() @HealthCheck() // @Public() // Use if you have a global AuthGuard and need this public check(): Promise<HealthCheckResult> { // Check if the app itself is reachable (basic check on loopback) // Use the actual host/port if running behind proxy in production checks const url = `http://localhost:${this.configService.get('PORT', 3000)}`; return this.health.check([ () => this.http.pingCheck('nestjs-app', url, { timeout: 2000 }), // Add other checks here, e.g., database connectivity, MessageBird API status (if available) // () => this.db.pingCheck('database'), // Example if using TypeOrmHealthIndicator ]); } }
HealthModule
, importTerminusModule
andHttpModule
, provide theHealthController
, and importHealthModule
into yourAppModule
. - Structured Logging: As mentioned in Section 5, use structured logging (JSON format) with correlation IDs to trace requests across services.
- Metrics: Integrate a metrics library (like
prom-client
for Prometheus) to expose key performance indicators (KPIs) such as request latency, error rates, MMS sent count, MessageBird API response times, and queue lengths (if using background jobs). - Distributed Tracing: For complex systems, implement distributed tracing (e.g., using OpenTelemetry) to visualize the entire lifecycle of a request as it flows through your NestJS app, queues, and external services like MessageBird.
- Alerting: Set up alerts based on logs and metrics (e.g., high error rates, high latency, health check failures, low queue throughput) using tools like Prometheus Alertmanager, Grafana Alerting, or Datadog Monitors.
11. Testing Strategies
Testing is crucial for ensuring the reliability of your MMS sending functionality.
- Unit Tests:
MmsService
: Mock theConfigService
and themessagebird
client. Test thesendMms
method logic:- Verify it calls
messagebird.mms.create
with the correct parameters based on input. - Test input validation logic (e.g., throwing
BadRequestException
if body and media are missing). - Test handling of successful API responses (resolving the promise).
- Test handling of API errors (rejecting the promise with
InternalServerErrorException
).
- Verify it calls
MmsController
: Mock theMmsService
. Test thesendMms
endpoint:- Verify it calls
mmsService.sendMms
with data from the validated DTO. - Test successful response handling.
- Test error handling (re-throwing
HttpException
or wrapping errors).
- Verify it calls
- DTOs: Unit tests for custom
class-validator
decorators are possible but often covered by integration/e2e tests.
- Integration Tests:
- Test the interaction between the
MmsController
andMmsService
without mocking the service entirely, but potentially still mocking the external MessageBird SDK call within the service. This ensures the controller correctly invokes the service and handles its responses/errors. - If using a database (Section 6), test the service's interaction with the
MmsLog
repository (e.g., verifying logs are created/updated correctly). Use a test database.
- Test the interaction between the
- End-to-End (E2E) Tests:
- Use NestJS's built-in E2E testing framework (
@nestjs/testing
with Supertest). - Send actual HTTP requests to your running application's
/mms/send
endpoint. - Mocking External API: Crucially, mock the MessageBird API at the HTTP level (e.g., using
nock
ormsw
) or by providing a mock implementation of themessagebird
client during test setup. Do not hit the actual MessageBird API in automated tests to avoid costs and side effects. - Test various scenarios: valid requests, invalid requests (triggering DTO validation errors), requests causing service errors (mocking MessageBird SDK errors), etc.
- Assert the HTTP status codes and response bodies.
- Use NestJS's built-in E2E testing framework (
12. Deployment Considerations
- Environment Configuration: Use environment variables (
.env
locally, platform-specific configuration in production) forMESSAGEBIRD_ACCESS_KEY
,MESSAGEBIRD_ORIGINATOR
,PORT
, database credentials, etc. Never commit.env
files. - Build Process: Use
npm run build
to compile TypeScript to JavaScript (dist
folder). - Running in Production: Run the compiled application using
node dist/main.js
. Use a process manager like PM2 or rely on your container orchestrator (Docker, Kubernetes) to manage the Node.js process (restarts, scaling). - Containerization (Docker):
- Create a
Dockerfile
to build an image containing your application code and dependencies. - Use a multi-stage build to keep the final image small.
- Example
Dockerfile
:# Stage 1: Build the application FROM node:18-alpine AS builder WORKDIR /usr/src/app COPY package*.json ./ RUN npm ci --only=production --ignore-scripts --prefer-offline COPY . . RUN npm run build # Stage 2: Create the final production image FROM node:18-alpine WORKDIR /usr/src/app # Copy only necessary files from builder stage COPY /usr/src/app/node_modules ./node_modules COPY /usr/src/app/dist ./dist # Add package.json for metadata if needed, but node_modules are already copied # COPY --from=builder /usr/src/app/package.json ./ # Expose the port the app runs on EXPOSE 3000 # Command to run the application CMD [""node"", ""dist/main""]
- Create a
.dockerignore
file similar to.gitignore
to exclude unnecessary files from the build context.
- Create a
- Platform Choices:
- PaaS (Heroku, Vercel (for serverless functions), Render): Simpler deployment, often handle scaling, HTTPS, and environment variables.
- IaaS (AWS EC2, Google Compute Engine): More control, requires manual setup of infrastructure (load balancers, databases, networking).
- Container Orchestration (Kubernetes, AWS ECS, Google Cloud Run): Best for complex, scalable applications using containers.
- CI/CD: Implement a Continuous Integration/Continuous Deployment pipeline (e.g., using GitHub Actions, GitLab CI, Jenkins) to automate testing, building, and deploying your application on code changes.
Conclusion
You have successfully built a NestJS application capable of sending MMS messages using the MessageBird API. We covered:
- Project setup with NestJS CLI and dependency management.
- Secure configuration using
.env
andConfigModule
. - Modular service design (
MmsService
) for API interaction. - Robust API endpoint creation (
MmsController
) with DTO validation. - Integration specifics with MessageBird (API keys, originator).
- Strategies for error handling, logging, and optional retries.
- Optional database integration for logging and tracking.
- Security best practices (validation, API keys, rate limiting, HTTPS).
- Handling special cases and limitations of MMS/MessageBird.
- Performance optimization techniques (batching, async processing).
- Monitoring and observability considerations (health checks, metrics).
- Comprehensive testing strategies (unit, integration, E2E).
- Deployment considerations (Docker, platforms, CI/CD).
This foundation provides a reliable and maintainable way to incorporate MMS functionality into your NestJS projects. Remember to consult the official MessageBird API documentation for the latest details on limits, supported features, and error codes.