Production-Ready Guide: Sending MMS with NestJS and AWS Pinpoint
This guide provides a comprehensive, step-by-step walkthrough for building a production-ready system to send Multimedia Messaging Service (MMS) messages using NestJS and the AWS Pinpoint SMS and Voice API v2. We'll cover everything from project setup and core implementation to error handling, security, and deployment.
By the end of this tutorial, you will have a functional NestJS API endpoint capable of accepting a destination phone number, a message body, and a media file, uploading the file to Amazon S3, and sending an MMS message via AWS Pinpoint. This solves the need for applications to programmatically send rich media content to users' mobile devices, enhancing engagement beyond simple text messages.
Technologies Used:
- NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Chosen for its modular architecture, dependency injection, and excellent TypeScript support.
- AWS Pinpoint SMS and Voice API v2: The AWS service used for sending SMS and MMS messages. We use v2 specifically for its
SendMediaMessage
capability. - AWS SDK for JavaScript v3: Used to interact with AWS services (Pinpoint and S3). Chosen for its modularity and async/await support.
- Amazon S3: Used for storing the media files that will be attached to the MMS messages.
- TypeScript: Provides static typing for better code quality and maintainability.
- Dotenv / NestJS ConfigModule: For managing environment variables securely.
- Multer: Middleware for handling
multipart/form-data
, used for file uploads. - Class-validator / Class-transformer: For robust request data validation.
- uuid: For generating unique identifiers (e.g., for S3 filenames).
- aws-sdk-client-mock / aws-sdk-client-mock-jest: For mocking AWS SDK clients in unit tests.
System Architecture:
[Client Application] --(HTTP POST Request + Media File)--> [NestJS API Endpoint]
|
v
[MMS Controller] --(Validates Request, Handles File Upload)--> [MMS Service]
|
v 1. Upload Media
+--------------------------------------------------------+----------------------------------------------------------+
| | |
v v 2. Send MMS Command v
[AWS Service] ---> [Amazon S3 Bucket] [AWS Service] ---> [AWS Pinpoint SMS & Voice API v2] [Logger]
^ (Stores Media File) | (Sends MMS via Carrier Networks) (Records Events/Errors)
| |
+-------------------(Provides S3 URI)--------------------+
|
v
[Recipient's Mobile Device]
Prerequisites:
- Node.js (v16 or later recommended) and npm/yarn installed.
- An AWS account with IAM permissions to manage Pinpoint, S3, and IAM itself.
- AWS CLI installed and configured (optional but helpful for setup).
- NestJS CLI installed (
npm install -g @nestjs/cli
). - An MMS-capable phone number or short code registered as an Origination Identity in AWS Pinpoint SMS and Voice v2. Crucially, this identity must be in the same AWS region you configure your application and S3 bucket.
- Basic understanding of TypeScript, NestJS concepts, and REST APIs.
1. Setting Up the Project
Let's initialize our NestJS project and install necessary dependencies.
1. Create NestJS Project:
Open your terminal and run:
nest new nestjs-aws-mms
cd nestjs-aws-mms
Choose your preferred package manager (npm or yarn) when prompted.
2. Install Dependencies:
We need packages for AWS SDK v3, configuration management, validation, file uploads, and unique IDs.
# Core Dependencies
npm install @aws-sdk/client-pinpoint-sms-voice-v2 @aws-sdk/client-s3 @nestjs/config dotenv class-validator class-transformer uuid
# Development Dependencies
npm install --save-dev @types/multer @types/uuid aws-sdk-client-mock aws-sdk-client-mock-jest
3. Environment Variables Setup:
Create a .env
file in the project root for storing sensitive configuration. Never commit this file to version control. Add a .env.example
file to track required variables.
# .env - Fill with your actual values
AWS_REGION=us-east-1 # Replace with the region of your Pinpoint identity and S3 bucket
AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY
AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_KEY
AWS_S3_BUCKET_NAME=your-mms-media-bucket-name # Replace with your unique S3 bucket name
AWS_PINPOINT_ORIGINATION_IDENTITY=arn:aws:sms-voice:us-east-1:111122223333:phone-number/pnum-xxxxxxxx # Or your registered phone number/short code
# .env.example - Example structure for repository
AWS_REGION=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_S3_BUCKET_NAME=
AWS_PINPOINT_ORIGINATION_IDENTITY=
Ensure your .gitignore
file includes .env
.
4. AWS IAM Setup:
Create an IAM user (or use an existing one/role) with programmatic access. Attach a policy granting the necessary permissions.
-
Minimal Permissions Policy Example (
mms-sender-policy.json
):{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "sms-voice:SendMediaMessage", "Resource": "*" // Or scope down to your specific Origination Identity ARN }, { "Effect": "Allow", "Action": [ "s3:PutObject", "s3:PutObjectAcl" // Required if you need specific ACLs, often needed for public read if Pinpoint needs it ], "Resource": "arn:aws:s3:::YOUR_MMS_MEDIA_BUCKET_NAME/*" // Replace bucket name } // IMPORTANT: Pinpoint needs permission to READ the object. // This is often handled via bucket policies or ensuring the object ACL allows read access // for the Pinpoint service principal or by making objects public-read (use with caution). // A bucket policy is generally preferred. See S3 Setup section. ] }
-
Create the user/role and attach this policy.
-
Record the
AWS_ACCESS_KEY_ID
andAWS_SECRET_ACCESS_KEY
in your.env
file.
Why these permissions?
sms-voice:SendMediaMessage
: Allows sending MMS messages via Pinpoint v2.s3:PutObject
: Allows uploading the media file to your S3 bucket.s3:PutObjectAcl
: Might be needed depending on your bucket's access control strategy.
5. AWS S3 Bucket Setup:
-
Create Bucket: Use the AWS console or CLI to create an S3 bucket in the same AWS Region as your Pinpoint Origination Identity. Choose a unique name.
aws s3api create-bucket --bucket YOUR_MMS_MEDIA_BUCKET_NAME --region YOUR_AWS_REGION --create-bucket-configuration LocationConstraint=YOUR_AWS_REGION # Example: aws s3api create-bucket --bucket my-unique-mms-media --region us-east-1
-
Block Public Access: Generally recommended to keep "Block all public access" settings enabled unless you have a specific reason not to.
-
Bucket Policy for Pinpoint Access: Pinpoint needs permission to read the objects you upload. Add a bucket policy to grant this access. Replace
YOUR_MMS_MEDIA_BUCKET_NAME
andYOUR_AWS_ACCOUNT_ID
.{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowPinpointRead", "Effect": "Allow", "Principal": { // Use the regional service principal for sms-voice "Service": "sms-voice.amazonaws.com" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::YOUR_MMS_MEDIA_BUCKET_NAME/*", "Condition": { "StringEquals": { "aws:SourceAccount": "YOUR_AWS_ACCOUNT_ID" } } } ] }
- Navigate to your bucket > Permissions > Bucket policy > Edit, paste the JSON, and save.
- Why this policy? It securely grants the Pinpoint service itself read access to objects in your bucket, scoped to your account, without making objects public.
6. AWS Pinpoint Origination Identity:
- Navigate to the "Pinpoint SMS and Voice v2" console (ensure you are in the correct region).
- Go to "Phone numbers" or "Short codes".
- Register or select an existing number/code.
- Crucially, verify that the number/code is "Active" and has "MMS" listed under capabilities. If not, you may need to request MMS enablement for your account/number via AWS Support.
- Copy the phone number (E.164 format) or its full ARN and place it in the
AWS_PINPOINT_ORIGINATION_IDENTITY
variable in your.env
file.
7. Configure NestJS ConfigModule:
Import and configure ConfigModule
in your main application module (app.module.ts
) to load environment variables.
// 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 { AwsModule } from './aws/aws.module';
import { MmsModule } from './mms/mms.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true, // Make ConfigService available globally
envFilePath: '.env', // Specify the env file
}),
AwsModule, // Add our custom AWS module
MmsModule, // Add our custom MMS module
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Why ConfigModule
? It provides a structured way to access environment variables throughout the application via dependency injection, avoiding direct process.env
access and enhancing testability.
2. Implementing Core Functionality
We'll create dedicated modules and services for AWS interactions and MMS logic.
1. AWS Module & Service:
This centralizes AWS SDK client initialization.
-
Create Module/Service:
nest g module aws nest g service aws
-
Implement AWS Service (
src/aws/aws.service.ts
):// src/aws/aws.service.ts import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { PinpointSMSVoiceV2Client, PinpointSMSVoiceV2ClientConfig, } from '@aws-sdk/client-pinpoint-sms-voice-v2'; import { S3Client, S3ClientConfig } from '@aws-sdk/client-s3'; @Injectable() export class AwsService implements OnModuleInit { private readonly logger = new Logger(AwsService.name); private _pinpointClient: PinpointSMSVoiceV2Client; private _s3Client: S3Client; constructor(private configService: ConfigService) {} onModuleInit() { const region = this.configService.get<string>('AWS_REGION'); const accessKeyId = this.configService.get<string>('AWS_ACCESS_KEY_ID'); const secretAccessKey = this.configService.get<string>( 'AWS_SECRET_ACCESS_KEY', ); if (!region || !accessKeyId || !secretAccessKey) { this.logger.error( 'AWS credentials or region not fully configured in environment variables.', ); throw new Error( 'AWS environment variables (REGION, ACCESS_KEY_ID, SECRET_ACCESS_KEY) are required.', ); } const commonConfig = { region, credentials: { accessKeyId, secretAccessKey, }, }; this._pinpointClient = new PinpointSMSVoiceV2Client( commonConfig as PinpointSMSVoiceV2ClientConfig, // Cast needed due to potential credential provider types ); this.logger.log('PinpointSMSVoiceV2Client initialized.'); this._s3Client = new S3Client(commonConfig as S3ClientConfig); this.logger.log('S3Client initialized.'); } get pinpointClient(): PinpointSMSVoiceV2Client { if (!this._pinpointClient) { throw new Error('Pinpoint client not initialized.'); } return this._pinpointClient; } get s3Client(): S3Client { if (!this._s3Client) { throw new Error('S3 client not initialized.'); } return this._s3Client; } }
-
Update AWS Module (
src/aws/aws.module.ts
):// src/aws/aws.module.ts import { Module, Global } from '@nestjs/common'; import { AwsService } from './aws.service'; @Global() // Make AwsService injectable anywhere without importing AwsModule @Module({ providers: [AwsService], exports: [AwsService], // Export AwsService to be used by other modules }) export class AwsModule {}
Why this structure?
- Centralizes client creation logic.
- Uses
ConfigService
for credentials, avoiding hardcoding. OnModuleInit
ensures clients are ready when the module initializes.@Global()
makes theAwsService
easily injectable across the app.
2. MMS Module & Service:
This module handles the specific logic for uploading to S3 and sending MMS via Pinpoint.
-
Create Module/Service:
nest g module mms nest g service mms
-
Implement MMS Service (
src/mms/mms.service.ts
):// src/mms/mms.service.ts import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { AwsService } from '../aws/aws.service'; import { PutObjectCommand, PutObjectCommandInput } from '@aws-sdk/client-s3'; import { SendMediaMessageCommand, SendMediaMessageCommandInput, } from '@aws-sdk/client-pinpoint-sms-voice-v2'; import { v4 as uuidv4 } from 'uuid'; // For generating unique filenames @Injectable() export class MmsService { private readonly logger = new Logger(MmsService.name); private readonly s3Bucket: string; private readonly pinpointOriginationId: string; constructor( private readonly awsService: AwsService, private readonly configService: ConfigService, ) { this.s3Bucket = this.configService.get<string>('AWS_S3_BUCKET_NAME'); this.pinpointOriginationId = this.configService.get<string>( 'AWS_PINPOINT_ORIGINATION_IDENTITY', ); if (!this.s3Bucket || !this.pinpointOriginationId) { throw new Error('S3 Bucket Name or Pinpoint Origination ID missing in config.'); } } /** * Uploads a media file to the configured S3 bucket. * @param file - The file buffer and metadata (Express.Multer.File) * @returns The S3 URI of the uploaded file (s3://bucket/key) */ async uploadMediaToS3( file: Express.Multer.File, ): Promise<string> { const s3Client = this.awsService.s3Client; // Generate a unique key (filename) to prevent overwrites const fileExtension = file.originalname.split('.').pop(); const uniqueKey = `mms-media/${uuidv4()}${fileExtension ? '.' + fileExtension : ''}`; const params: PutObjectCommandInput = { Bucket: this.s3Bucket, Key: uniqueKey, Body: file.buffer, ContentType: file.mimetype, // ACL: 'public-read', // Only needed if bucket policy isn't used/sufficient for Pinpoint access // Bucket Policy approach is generally preferred. }; try { this.logger.log(`Uploading media to S3: ${this.s3Bucket}/${uniqueKey}`); await s3Client.send(new PutObjectCommand(params)); const s3Uri = `s3://${this.s3Bucket}/${uniqueKey}`; this.logger.log(`Successfully uploaded media to ${s3Uri}`); return s3Uri; } catch (error) { this.logger.error(`Failed to upload media to S3: ${error.message}`, error.stack); throw new InternalServerErrorException('Failed to upload media file.'); } } /** * Sends an MMS message using AWS Pinpoint SMS & Voice API v2. * @param destinationPhoneNumber - E.164 formatted phone number * @param messageBody - Optional text message body * @param mediaUrls - Array containing the S3 URI of the media file * @returns The Message ID from Pinpoint */ async sendMms( destinationPhoneNumber: string, messageBody: string | undefined, mediaUrls: string[], ): Promise<string | undefined> { const pinpointClient = this.awsService.pinpointClient; const params: SendMediaMessageCommandInput = { DestinationPhoneNumber: destinationPhoneNumber, OriginationIdentity: this.pinpointOriginationId, MessageBody: messageBody, MediaUrls: mediaUrls, // ConfigurationSetName: 'YourOptionalConfigSetName' // If using configuration sets for tracking }; try { this.logger.log(`Sending MMS to ${destinationPhoneNumber} via Pinpoint.`); const command = new SendMediaMessageCommand(params); const response = await pinpointClient.send(command); this.logger.log(`MMS Send Command successful. MessageId: ${response.MessageId}`); // IMPORTANT: Success here means AWS accepted the request, not final delivery. // Check CloudWatch logs or configure event destinations for delivery status. return response.MessageId; } catch (error) { this.logger.error(`Failed to send MMS via Pinpoint: ${error.message}`, error.stack); // Consider more specific error handling based on AWS error codes throw new InternalServerErrorException('Failed to send MMS message.'); } } }
-
Update MMS Module (
src/mms/mms.module.ts
):// src/mms/mms.module.ts import { Module } from '@nestjs/common'; import { MmsService } from './mms.service'; import { MmsController } from './mms.controller'; // AwsModule is global, so no need to import here if AwsService is needed by MmsService @Module({ controllers: [MmsController], // We will create this next providers: [MmsService], exports: [MmsService], }) export class MmsModule {}
Why this structure?
- Separates MMS concerns (upload, send) from generic AWS client setup.
- Injects
AwsService
andConfigService
for dependencies. - Uses
uuid
for unique filenames in S3, preventing collisions. - Provides clear methods for
uploadMediaToS3
andsendMms
. - Includes basic logging and error handling.
3. Building the API Layer
We'll create a controller with an endpoint to receive MMS requests and trigger the service.
-
Create Controller:
nest g controller mms
-
Create DTO (Data Transfer Object) for Validation:
Create a file
src/mms/dto/send-mms.dto.ts
:// src/mms/dto/send-mms.dto.ts import { IsNotEmpty, IsString, IsPhoneNumber, IsOptional, MaxLength } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; // If using Swagger export class SendMmsDto { @IsNotEmpty() @IsPhoneNumber(null) // Use null for international format validation (E.164) @ApiProperty({ description: 'Destination phone number in E.164 format (e.g., +12065550100)', example: '+12065550100', }) destinationPhoneNumber: string; @IsOptional() @IsString() @MaxLength(1600) // Check AWS Pinpoint limits for MMS body size @ApiPropertyOptional({ description: 'Text body of the message (optional)', example: 'Check out this cool picture!', maxLength: 1600, }) messageBody?: string; // Note: The file itself will be handled by Multer, not validated here directly. // We rely on Multer's file filter for basic type checks. }
-
Implement MMS Controller (
src/mms/mms.controller.ts
):// src/mms/mms.controller.ts import { Controller, Post, UseInterceptors, UploadedFile, Body, ParseFilePipe, FileTypeValidator, MaxFileSizeValidator, HttpCode, HttpStatus, Logger, BadRequestException, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { MmsService } from './mms.service'; import { SendMmsDto } from './dto/send-mms.dto'; import { ApiTags, ApiConsumes, ApiBody, ApiResponse } from '@nestjs/swagger'; // Optional: For Swagger docs // Define supported MIME types and max size (check AWS Pinpoint limits) const SUPPORTED_MIME_TYPES = /image\/(jpeg|png|gif)|video\/(mp4|3gpp)/; // Example types const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024; // Example: 5MB @ApiTags('MMS') // Optional: For Swagger @Controller('mms') export class MmsController { private readonly logger = new Logger(MmsController.name); constructor(private readonly mmsService: MmsService) {} @Post('send') @HttpCode(HttpStatus.ACCEPTED) // Use 202 Accepted as sending is asynchronous @UseInterceptors(FileInterceptor('mediaFile')) // 'mediaFile' is the field name in the form-data @ApiConsumes('multipart/form-data') // Optional: For Swagger @ApiBody({ // Optional: For Swagger description: 'MMS details and media file', schema: { type: 'object', properties: { destinationPhoneNumber: { type: 'string', format: 'e164', example: '+12065550100' }, messageBody: { type: 'string', example: 'Hello from NestJS MMS!' }, mediaFile: { type: 'string', format: 'binary', description: 'The media file (image/video) to send.', }, }, required: ['destinationPhoneNumber', 'mediaFile'], }, }) @ApiResponse({ status: 202, description: 'MMS request accepted for processing.'}) @ApiResponse({ status: 400, description: 'Bad Request (validation error, invalid file).'}) @ApiResponse({ status: 500, description: 'Internal server error.'}) async sendMms( @Body() sendMmsDto: SendMmsDto, // DTO for text fields, validated automatically @UploadedFile( new ParseFilePipe({ // Validate the uploaded file validators: [ new FileTypeValidator({ fileType: SUPPORTED_MIME_TYPES }), new MaxFileSizeValidator({ maxSize: MAX_FILE_SIZE_BYTES }), ], fileIsRequired: true, // Ensure a file is actually uploaded }), ) file: Express.Multer.File, ) { this.logger.log( `Received MMS request for ${sendMmsDto.destinationPhoneNumber} with file: ${file.originalname}`, ); if (!file) { // Should be caught by ParseFilePipe, but good practice to check throw new BadRequestException('Media file is required.'); } try { // 1. Upload media to S3 const mediaUrl = await this.mmsService.uploadMediaToS3(file); // 2. Send MMS via Pinpoint const messageId = await this.mmsService.sendMms( sendMmsDto.destinationPhoneNumber, sendMmsDto.messageBody, [mediaUrl], // Pinpoint expects an array of URLs ); this.logger.log(`MMS request processed successfully. Message ID: ${messageId}`); return { message: 'MMS request accepted for processing.', messageId: messageId, // Return the message ID for tracking }; } catch (error) { this.logger.error(`Error processing send MMS request: ${error.message}`, error.stack); // Service layer should throw appropriate exceptions, re-throw or handle here throw error; // Let NestJS default exception filter handle it or implement custom filter } } }
Why this structure?
- Uses
@nestjs/platform-express
'sFileInterceptor
to handlemultipart/form-data
. - Uses
ParseFilePipe
with validators (FileTypeValidator
,MaxFileSizeValidator
) for robust file validation before hitting the service logic. - Uses the
SendMmsDto
and relies onValidationPipe
(enabled globally later) for validating text fields. - Injects
MmsService
to delegate the core logic. - Returns
202 Accepted
as the process involves external asynchronous systems. - Includes basic logging.
- (Optional) Swagger decorators help document the API.
- Uses
Enable Global Validation Pipe:
Modify src/main.ts
to enable automatic validation using the DTOs.
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
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
forbidNonWhitelisted: true, // Throw error if extra properties are sent
transform: true, // Automatically transform payloads to DTO instances
transformOptions: {
enableImplicitConversion: true, // Allow basic type conversions
},
}),
);
// Optional: Enable CORS if your client is on a different origin
// app.enableCors({ origin: 'http://your-client-domain.com' });
// Optional: Get port from config or default
const configService = app.get(ConfigService);
const port = configService.get<number>('PORT') || 3000;
await app.listen(port);
logger.log(`Application listening on port ${port}`);
logger.log(`API Endpoint for MMS: POST /mms/send`);
}
bootstrap();
Testing the Endpoint:
You can use curl
or Postman to test.
-
Curl Example:
curl -X POST http://localhost:3000/mms/send \ -F "destinationPhoneNumber=+12065550100" \ -F "messageBody=Hello from curl MMS!" \ -F "mediaFile=@/path/to/your/image.jpg" # Replace with a valid E.164 number and path to an actual image file
-
Postman:
- Set method to
POST
. - Set URL to
http://localhost:3000/mms/send
. - Go to the
Body
tab, selectform-data
. - Add a
KEY
nameddestinationPhoneNumber
, enter a valid E.164 number inVALUE
. - (Optional) Add
KEY
messageBody
, enter text inVALUE
. - Add
KEY
mediaFile
. In theVALUE
column, click the dropdown and selectFile
. Then clickSelect Files
and choose your media file. - Click
Send
.
- Set method to
Expected Response (Success - 202 Accepted):
{
"message": "MMS request accepted for processing.",
"messageId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" // Actual Message ID from Pinpoint
}
Expected Response (Validation Error - 400 Bad Request):
{
"message": [
"destinationPhoneNumber must be a valid phone number"
// or file validation errors
],
"error": "Bad Request",
"statusCode": 400
}
4. Integrating with Third-Party Services (AWS)
This section summarizes the key integration points already covered:
- AWS Credentials: Securely managed via
.env
andConfigModule
. Accessed inAwsService
to initialize SDK clients. - S3 Bucket: Created in the correct region, name configured in
.env
. Bucket Policy allows Pinpoint read access. Upload logic inMmsService
. - Pinpoint Origination Identity: Registered in the correct region, MMS-enabled, ARN/number configured in
.env
. Used inMmsService
'sSendMediaMessageCommand
. - Region Consistency: Emphasized that the S3 Bucket, Pinpoint Identity, and the AWS region configured in the application (
AWS_REGION
in.env
) must all be the same. - Fallback Mechanisms: Currently, the implementation doesn't include automatic fallbacks (e.g., sending SMS if MMS fails). This would require:
- Catching specific Pinpoint errors.
- Implementing an SMS sending function (potentially using Pinpoint's
SendTextMessageCommand
or the standard SNSPublishCommand
if preferred for SMS). - Adding logic to trigger the fallback based on the error.
5. Error Handling, Logging, and Retry Mechanisms
- Logging: Implemented using NestJS built-in
Logger
in services and controllers. Logs key events (request received, upload start/finish, send start/finish) and errors with stack traces.- Enhancement: Consider adding a request correlation ID (e.g., using
nestjs-cls
) to trace requests across logs. - Log Format: For production, configure logging to output JSON for easier parsing by log aggregation tools (e.g., CloudWatch Logs, Datadog, Splunk).
- Enhancement: Consider adding a request correlation ID (e.g., using
- Error Handling:
- Uses
ValidationPipe
andParseFilePipe
for input validation errors (400 Bad Request). - Service layer (
MmsService
) catches specific AWS SDK errors during S3 upload and Pinpoint send. - Throws
InternalServerErrorException
(500) for service failures, which can be caught by NestJS's default exception filter or a custom one. - Enhancement: Implement a custom exception filter to standardize error responses and potentially map specific AWS errors (e.g.,
ThrottlingException
) to appropriate HTTP status codes (e.g., 429 Too Many Requests).
- Uses
- Retry Mechanisms:
- The AWS SDK v3 has built-in retry logic with exponential backoff for many transient errors (e.g., throttling, network issues). This is configurable when creating the clients in
AwsService
, but the defaults are often sufficient. - For application-level retries (e.g., if a dependency is temporarily down), you could implement:
- A simple loop with delays in the service (less robust).
- Integrate with a job queue system (e.g., BullMQ, AWS SQS) to enqueue failed MMS sends for later retries. This is the recommended approach for production resilience.
- The AWS SDK v3 has built-in retry logic with exponential backoff for many transient errors (e.g., throttling, network issues). This is configurable when creating the clients in
6. Database Schema and Data Layer (Optional Enhancement)
While not strictly required for sending MMS, tracking sent messages is crucial for auditing, analytics, and status checking.
- Schema Idea (using TypeORM or Prisma):
- Entity:
MmsLog
- Fields:
id
(Primary Key, UUID)messageId
(String, Indexed, Nullable - from Pinpoint response)destinationPhoneNumber
(String, Indexed)originationIdentity
(String)messageBody
(Text, Nullable)mediaS3Uri
(String)status
(Enum: PENDING, ACCEPTED, SENT, FAILED, DELIVERED - Requires event handling from Pinpoint)pinpointResponse
(JSON, Nullable - Store raw response/error)createdAt
(Timestamp)updatedAt
(Timestamp)
- Entity:
- Implementation:
- Install database dependencies (
@nestjs/typeorm typeorm pg
or@prisma/client
). - Configure
TypeOrmModule
or Prisma service. - Create the
MmsLog
entity/model. - Inject the repository/client into
MmsService
. - Create records in
MmsService
before calling Pinpoint (status: PENDING
) and update with themessageId
andstatus: ACCEPTED
upon successful API call acceptance. - Delivery Status: To get actual
SENT
,FAILED
,DELIVERED
status, you need to configure Pinpoint Event Destinations (e.g., sending events to an SQS queue or Lambda function) and process these events to update theMmsLog
status.
- Install database dependencies (