code examples

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

Developer Guide: Sending MMS with Vite, Node.js, and AWS Pinpoint/S3

A step-by-step guide to building a full-stack application for sending MMS messages with images using Vite (React/Vue), Node.js, AWS S3 for uploads, and AWS Pinpoint for delivery.

Developer Guide: Sending MMS with Vite (React/Vue), Node.js, and AWS Pinpoint/S3

This guide provides a step-by-step walkthrough for building a system that enables users to send Multimedia Messaging Service (MMS) messages, including text and an image, using a modern web frontend built with Vite (React or Vue) and a Node.js backend interacting with AWS services.

Project Overview and Goals

What We'll Build:

We will create a full-stack application consisting of:

  1. A Vite Frontend (React): A simple user interface with a form to input the recipient's phone number, a text message, and upload an image file. (The provided code uses React; adaptation for Vue is left as an exercise for the reader).
  2. A Node.js/Express Backend API: An API layer that handles requests from the frontend. It will generate secure, temporary URLs (presigned URLs) for direct-to-S3 uploads from the browser and then trigger the MMS sending process via AWS Pinpoint.
  3. AWS Integration: Utilizing AWS S3 for storing the media files and AWS Pinpoint SMS and Voice v2 API for the actual MMS delivery.

Problem Solved:

This guide addresses the need to securely and reliably send MMS messages containing media (like images) from a web application, without exposing sensitive AWS credentials on the client-side. It leverages AWS's scalable infrastructure for media storage and message delivery.

Technologies Used:

  • Frontend: Vite, React (examples provided are React-specific; easily adaptable for Vue conceptually)
  • Backend: Node.js, Express.js
  • AWS Services:
    • Pinpoint SMS and Voice v2: For sending MMS messages. Note: MMS sending uses this specific service, sometimes referred to under the broader Pinpoint or SNS umbrella in documentation, but the API endpoint is distinct.
    • S3 (Simple Storage Service): For storing the media files to be included in the MMS.
    • IAM (Identity and Access Management): For securely managing AWS service permissions.
  • AWS SDK for JavaScript v3: For interacting with AWS services from the Node.js backend.

Architecture Diagram:

text
+-----------------+      +----------------------+      +------------------------+      +------------------------+      +-----------+
|  Vite Frontend  |----->| Node.js Backend API  |----->| AWS Credentials (IAM)  |      |                        |      | Recipient |
| (React/Vue)     |      | (Express.js)         |<-----|                        |      |                        |      | (Phone)   |
+-----------------+      +----------------------+      +------------------------+      |                        |      +-----------+
       |                        |                                                     | AWS Pinpoint SMS/Voice |          ^
       | 1. Request Presigned URL| 2. Generate Presigned URL                           | (SendMediaMessage API) |          |
       |                        |    (using S3 SDK & IAM creds)                       |                        |          |
       | 3. Receive URL & Key   |---------------------------------------------------->|                        |----------+
       |                        |                                                     |                        | 6. Send MMS
       | 4. PUT file to S3 <----|                                                     |                        |
       |    (Directly using     |                                      +--------------+                        |
       |     Presigned URL)     |                                      | AWS S3 Bucket|                        |
       |                        | 5. Trigger Send MMS API Call         | (Stores Media|<-----------------------+
       |                        |    (using Pinpoint SDK, S3 Key,      |  File)       | 4b. Receive & Store File
       +------------------------|    IAM creds, Phone #, Message)       +--------------+
                                |                                            |
                                +--------------------------------------------+

Prerequisites:

  • An AWS Account.
  • Node.js (v14.x or later recommended) and npm/yarn installed.
  • AWS CLI installed and configured (optional, but helpful for setup/verification).
  • Basic understanding of React or Vue, Node.js/Express, and asynchronous JavaScript.
  • An MMS-capable phone number acquired through AWS Pinpoint or Amazon Connect (e.g., a US Toll-Free Number). Standard short codes or 10DLC numbers may require specific registration. Verify MMS capability in the AWS console.

Final Outcome:

A functional web application where a user can specify a recipient phone number, type a message, upload an image, and successfully send an MMS containing both the text and image to the recipient via AWS.


1. Setting up the Project Environment

We'll create two main directories: frontend and backend.

1.1. Backend Setup (Node.js/Express)

bash
# Create project directory and navigate into it
mkdir mms-sender-app
cd mms-sender-app

# Create backend directory and initialize Node.js project
mkdir backend
cd backend
npm init -y

# Install necessary dependencies
npm install express cors dotenv @aws-sdk/client-s3 @aws-sdk/s3-request-presigner @aws-sdk/client-pinpoint-sms-voice-v2

# Install development dependency (optional, for auto-reloading)
npm install --save-dev nodemon

1.2. Backend Project Structure (backend/)

text
backend/
├── node_modules/
├── .env                # AWS credentials and config (DO NOT COMMIT)
├── .gitignore          # Node specific ignores
├── package.json
├── package-lock.json
└── server.js           # Main Express application file

1.3. Configure .gitignore (backend/.gitignore)

text
node_modules
.env

1.4. Configure .env (backend/.env)

This file stores sensitive credentials. Ensure it's listed in .gitignore. IMPORTANT: You must replace the placeholder values below with your actual AWS credentials, bucket name, region, and phone number.

ini
# AWS Credentials & Config
# !! Replace with your actual IAM User Access Key ID !!
AWS_ACCESS_KEY_ID=YOUR_IAM_USER_ACCESS_KEY_ID
# !! Replace with your actual IAM User Secret Access Key !!
AWS_SECRET_ACCESS_KEY=YOUR_IAM_USER_SECRET_ACCESS_KEY
# !! Replace with the AWS region where your S3 bucket and Pinpoint number exist (e.g., us-east-1, eu-west-2) !!
AWS_REGION=us-east-1 # IMPORTANT: Use the SAME region as your S3 bucket and Pinpoint number
# !! Replace with the unique name you gave your S3 bucket !!
S3_BUCKET_NAME=your-unique-mms-media-bucket-name
# !! Replace with your MMS-capable number from AWS in E.164 format !!
PINPOINT_ORIGINATION_NUMBER=+1XXXXXXXXXX # Your MMS-capable number from AWS

# API Server Config
PORT=3001
  • Purpose of Variables:
    • AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY: Credentials for your IAM user allowing programmatic access. Must be replaced.
    • AWS_REGION: The AWS region where your S3 bucket and Pinpoint identity reside. Consistency is crucial. Must be replaced.
    • S3_BUCKET_NAME: The unique name of the S3 bucket you will create. Must be replaced.
    • PINPOINT_ORIGINATION_NUMBER: The E.164 formatted phone number you acquired from AWS that is enabled for MMS. Must be replaced.
    • PORT: The port the backend API server will run on.

1.5. Frontend Setup (Vite + React)

(Note: This guide provides React code. For Vue, use npm create vite@latest frontend -- --template vue-ts and adapt the component logic accordingly.)

bash
# Navigate back to the root project directory
cd ..

# Create the frontend project using Vite (React + TypeScript template)
npm create vite@latest frontend -- --template react-ts
cd frontend

# Install frontend dependencies (axios for API calls)
npm install axios

# Start the frontend development server to test later
# This command launches the Vite development server, typically on http://localhost:5173
npm run dev

1.6. Frontend Project Structure (frontend/)

(Structure will vary slightly based on template choices)

text
frontend/
├── node_modules/
├── public/
├── src/
│   ├── App.css
│   ├── App.tsx       # Main application component
│   ├── main.tsx      # Entry point
│   └── ...           # Other components/assets
├── .gitignore
├── index.html
├── package.json
├── package-lock.json
├── tsconfig.json
└── vite.config.ts

2. AWS Setup (IAM, S3, Pinpoint Number)

This is a critical step requiring configuration within the AWS Management Console.

2.1. Create an IAM User

  1. Navigate to the IAM service in the AWS Console.

  2. Go to Users -> Add users.

  3. Enter a User name (e.g., mms-sender-app-user).

  4. Select Access key - Programmatic access as the credential type.

  5. Click Next: Permissions.

  6. Choose Attach existing policies directly.

  7. Click Create policy. A new tab/window will open.

  8. In the policy editor, switch to the JSON tab.

  9. Paste the following policy. IMPORTANT: You must replace your-unique-mms-media-bucket-name with your actual bucket name and YOUR_AWS_REGION with the region you are using (must match .env and S3 bucket region).

    json
    {
        ""Version"": ""2012-10-17"",
        ""Statement"": [
            {
                ""Sid"": ""AllowS3Upload"",
                ""Effect"": ""Allow"",
                ""Action"": [
                    ""s3:PutObject"",
                    ""s3:PutObjectAcl""
                ],
                ""Resource"": ""arn:aws:s3:::your-unique-mms-media-bucket-name/*""
            },
            {
                ""Sid"": ""AllowPinpointMMS"",
                ""Effect"": ""Allow"",
                ""Action"": ""pinpoint-sms-voice:SendMediaMessage"",
                ""Resource"": ""*""
            }
        ]
    }

    Note on s3:PutObjectAcl: Often needed for direct browser uploads using presigned URLs depending on bucket policy/ACLs. Review if necessary for your specific setup. Note on Resource: ""*"" for Pinpoint: You can restrict this further by ARN if needed, e.g., ""arn:aws:sms-voice:YOUR_AWS_REGION:ACCOUNT_ID:phone-number/YOUR_PINPOINT_NUMBER_ID"".

  10. Click Next: Tags (optional), then Next: Review.

  11. Give the policy a Name (e.g., MMS-Sender-App-Policy).

  12. Click Create policy.

  13. Close the policy editor tab/window and return to the user creation workflow.

  14. Refresh the policy list and search for the policy you just created (MMS-Sender-App-Policy). Select it.

  15. Click Next: Tags (optional), Next: Review, then Create user.

  16. CRITICAL: On the success screen, copy the Access key ID and Secret access key. Store these securely in your backend/.env file immediately (replacing the placeholders). You cannot retrieve the secret key again after leaving this screen.

2.2. Create an S3 Bucket

  1. Navigate to the S3 service in the AWS Console.
  2. Click Create bucket.
  3. Enter a Bucket name (must be globally unique, e.g., your-unique-mms-media-bucket-name). Use the same name as in your .env file and IAM policy.
  4. Select the AWS Region. IMPORTANT: This must be the same region as your IAM user credentials are configured for (AWS_REGION in .env) and where your Pinpoint origination number is provisioned.
  5. Block Public Access settings: Keep the defaults (Block all public access) unless you have a specific reason otherwise. We will use presigned URLs for controlled access.
  6. Bucket Versioning: Disable (unless needed).
  7. Tags: Optional.
  8. Default encryption: Keep defaults (SSE-S3).
  9. Click Create bucket.

2.3. Configure S3 Bucket CORS

To allow the frontend (running on localhost during development or your domain in production) to upload directly to the S3 bucket using the presigned URL, you need to configure Cross-Origin Resource Sharing (CORS).

  1. Go to your newly created bucket in the S3 console.

  2. Select the Permissions tab.

  3. Scroll down to Cross-origin resource sharing (CORS) and click Edit.

  4. Paste the following JSON configuration. Adjust AllowedOrigins for your frontend's actual URL in production.

    json
    [
        {
            ""AllowedHeaders"": [
                ""*""
            ],
            ""AllowedMethods"": [
                ""PUT"",
                ""POST""
            ],
            ""AllowedOrigins"": [
                ""http://localhost:5173"",
                ""http://localhost:3000""
            ],
            ""ExposeHeaders"": [
                ""ETag""
            ],
            ""MaxAgeSeconds"": 3000
        }
    ]

    Note: Add your production frontend URL (e.g., ""https://yourapp.com"") to AllowedOrigins.

  5. Click Save changes.

2.4. Acquire and Verify Pinpoint Origination Number

  1. Navigate to Amazon Pinpoint in the AWS Console.
  2. If you don't have a Pinpoint project, create one.
  3. Go to SMS and voice -> Phone numbers.
  4. Click Request phone number. Select the country (e.g., United States) and choose a Toll-free number type, as these generally support MMS out-of-the-box in supported regions (like the US). Standard numbers (10DLC) might require further registration (e.g., Campaign Registry).
  5. Follow the prompts to request the number. Note the region it's provisioned in (must match S3/IAM config).
  6. Once acquired, verify its capabilities. Select the number, and check the Capabilities section. Ensure MMS is listed and Status is Active.
  7. Copy the full phone number in E.164 format (e.g., +18005550199) and add it to your backend/.env file as PINPOINT_ORIGINATION_NUMBER, replacing the placeholder.

3. Implementing the Backend API (Node.js/Express)

We'll create two main endpoints: one to generate the S3 presigned URL for uploads and another to trigger the MMS sending.

backend/server.js:

javascript
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { PinpointSMSVoiceV2Client, SendMediaMessageCommand } from '@aws-sdk/client-pinpoint-sms-voice-v2';
import crypto from 'crypto'; // For generating unique filenames

dotenv.config();

const app = express();
const port = process.env.PORT || 3001;

// --- AWS Clients Configuration ---
// Ensure required environment variables are set
if (!process.env.AWS_REGION || !process.env.AWS_ACCESS_KEY_ID || !process.env.AWS_SECRET_ACCESS_KEY || !process.env.S3_BUCKET_NAME || !process.env.PINPOINT_ORIGINATION_NUMBER) {
    console.error("FATAL ERROR: Missing required AWS environment variables in .env file.");
    process.exit(1); // Stop the server if config is missing
}

const s3Client = new S3Client({
    region: process.env.AWS_REGION,
    credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    },
});

const pinpointClient = new PinpointSMSVoiceV2Client({
    region: process.env.AWS_REGION,
    credentials: {
        accessKeyId: process.env.AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    },
});

// --- Middleware ---
app.use(cors({
    // Configure allowed origins based on your frontend setup
    origin: ['http://localhost:5173', 'http://localhost:3000' /* Add production URL */ ],
}));
app.use(express.json()); // To parse JSON request bodies

// --- API Endpoints ---

/**
 * @route POST /api/generate-presigned-url
 * @desc Generate a presigned URL for direct S3 upload
 * @access Public (or protected if auth is added)
 * @body { filename: string, contentType: string }
 */
app.post('/api/generate-presigned-url', async (req, res) => {
    const { filename, contentType } = req.body;

    if (!filename || !contentType) {
        return res.status(400).json({ error: 'Missing filename or contentType' });
    }

    // Generate a unique key for the S3 object
    const randomBytes = crypto.randomBytes(16).toString('hex');
    const fileExtension = filename.split('.').pop();
    const s3Key = `mms-uploads/${randomBytes}.${fileExtension}`; // Store in a subfolder

    const putCommand = new PutObjectCommand({
        Bucket: process.env.S3_BUCKET_NAME,
        Key: s3Key,
        ContentType: contentType,
        // ACL: 'public-read', // Optional: Only if you need the object to be public, usually not needed for MMS
    });

    try {
        const presignedUrl = await getSignedUrl(s3Client, putCommand, { expiresIn: 300 }); // URL expires in 5 minutes
        console.log('Generated presigned URL:', presignedUrl);
        res.status(200).json({ presignedUrl, s3Key }); // Return URL and the key
    } catch (error) {
        console.error('Error generating presigned URL:', error);
        res.status(500).json({ error: 'Could not generate upload URL', details: error.message });
    }
});

/**
 * @route POST /api/send-mms
 * @desc Send the MMS message via Pinpoint
 * @access Public (or protected if auth is added)
 * @body { destinationPhoneNumber: string, messageBody: string, s3Key: string }
 */
app.post('/api/send-mms', async (req, res) => {
    const { destinationPhoneNumber, messageBody, s3Key } = req.body;

    // Basic validation - consider adding more robust checks (e.g., E.164 format)
    if (!destinationPhoneNumber || !s3Key) {
        // messageBody is optional for MMS-only
        return res.status(400).json({ error: 'Missing destinationPhoneNumber or s3Key' });
    }

    // Construct the S3 URL for the media file
    const mediaUrl = `s3://${process.env.S3_BUCKET_NAME}/${s3Key}`;

    const sendCommand = new SendMediaMessageCommand({
        DestinationPhoneNumber: destinationPhoneNumber, // E.164 format expected
        OriginationIdentity: process.env.PINPOINT_ORIGINATION_NUMBER,
        MessageBody: messageBody || '', // Optional text part
        MediaUrls: [mediaUrl],
        // ConfigurationSetName: 'YourOptionalConfigSet', // Optional: For tracking/events
    });

    try {
        console.log('Sending MMS with command input:', sendCommand.input);
        const response = await pinpointClient.send(sendCommand);
        console.log('MMS Send Response:', response);
        res.status(200).json({ success: true, messageId: response.MessageId });
    } catch (error) {
        console.error('Error sending MMS:', error);
        // Provide more context if possible, but avoid leaking sensitive details
        res.status(500).json({ error: 'Failed to send MMS', details: error.message, code: error.name });
    }
});

// --- Start Server ---
app.listen(port, () => {
    console.log(`Backend server running at http://localhost:${port}`);
});

Explanation:

  1. Dependencies: Imports Express, CORS, dotenv, AWS SDK clients, presigner, and crypto.
  2. AWS Clients: Initializes S3 and Pinpoint SMS/Voice v2 clients using credentials and region from .env. Includes a check for essential environment variables at startup.
  3. Middleware:
    • cors(): Enables Cross-Origin Resource Sharing, allowing requests from your frontend's origin. Crucially important.
    • express.json(): Parses incoming JSON request bodies.
  4. /api/generate-presigned-url Endpoint:
    • Accepts filename and contentType from the request body.
    • Generates a unique s3Key using crypto to avoid filename collisions. It's good practice to include a prefix like mms-uploads/.
    • Creates a PutObjectCommand specifying the bucket, key, and content type.
    • Uses getSignedUrl from @aws-sdk/s3-request-presigner to create a short-lived URL (expiresIn: 300 seconds).
    • Returns the presignedUrl and the s3Key to the frontend. The frontend will use the URL to upload, and the key to tell the backend which file to send in the MMS.
  5. /api/send-mms Endpoint:
    • Accepts destinationPhoneNumber, messageBody, and the s3Key (obtained from the previous step) from the request body.
    • Constructs the mediaUrl in the required s3://bucket-name/key format.
    • Creates a SendMediaMessageCommand with the destination, origination number (from .env), message body (optional), and the MediaUrls array containing the S3 URL.
    • Uses the pinpointClient to send the command.
    • Returns a success response with the AWS MessageId or an error response.
  6. Server Start: Starts the Express server listening on the configured port.

Running the Backend:

bash
# In the backend directory
npm start # Or use 'nodemon server.js' if you installed nodemon for auto-reload

4. Implementing the Frontend (React Example)

This example shows a basic React component. Adapt the state management, styling, and API client (axios) usage as needed for your project structure.

frontend/src/App.tsx:

tsx
import React, { useState, useRef, ChangeEvent, FormEvent } from 'react';
import axios from 'axios';
import './App.css'; // Basic styling

// Define the backend API URL (adjust if your backend runs elsewhere)
// Consider using environment variables for this in production builds (e.g., VITE_API_BASE_URL)
const API_BASE_URL = 'http://localhost:3001/api'; // Use your backend's actual address

function App() {
  const [destinationPhoneNumber, setDestinationPhoneNumber] = useState<string>('');
  const [messageBody, setMessageBody] = useState<string>('');
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
  const [statusMessage, setStatusMessage] = useState<string>('');
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [uploadedS3Key, setUploadedS3Key] = useState<string | null>(null); // Store key after upload

  const fileInputRef = useRef<HTMLInputElement>(null);

  const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
    if (event.target.files && event.target.files[0]) {
      const file = event.target.files[0];
      // Basic client-side validation
      if (!file.type.startsWith('image/')) {
          setStatusMessage('Error: Please select an image file (JPEG, PNG, GIF).');
          setSelectedFile(null);
          if (fileInputRef.current) fileInputRef.current.value = ''; // Clear input
          return;
      }
       // Optional: Add file size check (e.g., 1MB limit for MMS)
      const maxSizeMB = 1; // Example: 1MB limit (check carrier/Pinpoint limits)
      if (file.size > maxSizeMB * 1024 * 1024) {
        setStatusMessage(`Error: File size exceeds ${maxSizeMB}MB limit.`);
        setSelectedFile(null);
        if (fileInputRef.current) fileInputRef.current.value = ''; // Clear input
        return;
      }

      setSelectedFile(file);
      setStatusMessage(`Selected file: ${file.name}`);
      setUploadedS3Key(null); // Reset S3 key if a new file is selected
    }
  };

  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    if (!selectedFile || !destinationPhoneNumber) {
      setStatusMessage('Error: Destination phone number and an image file are required.');
      return;
    }
    // Basic E.164 format check (client-side) - more robust validation needed
    if (!/^\+?[1-9]\d{1,14}$/.test(destinationPhoneNumber)) {
        setStatusMessage('Error: Invalid phone number format. Use E.164 (e.g., +12065550100).');
        return;
    }


    setIsLoading(true);
    setStatusMessage('1/3: Requesting upload URL...');
    setUploadedS3Key(null); // Ensure no stale key is used

    try {
      // --- Step 1: Get Presigned URL from backend ---
      setStatusMessage('1/3: Requesting upload URL...');
      const presignedUrlResponse = await axios.post(`${API_BASE_URL}/generate-presigned-url`, {
        filename: selectedFile.name,
        contentType: selectedFile.type,
      });

      const { presignedUrl, s3Key } = presignedUrlResponse.data;
      console.log('Received presigned URL:', presignedUrl);
      console.log('Received S3 Key:', s3Key);

      if (!presignedUrl || !s3Key) {
        throw new Error(""Backend didn't return a valid presigned URL or S3 key."");
      }

      setStatusMessage('2/3: Uploading file to S3...');

      // --- Step 2: Upload file directly to S3 using the presigned URL ---
      await axios.put(presignedUrl, selectedFile, {
        headers: {
          'Content-Type': selectedFile.type, // Crucial header for S3 PUT
        },
        // Optional: Add progress tracking here using axios config onUploadProgress
      });

      setStatusMessage('3/3: File uploaded. Sending MMS...');
      setUploadedS3Key(s3Key); // Store the key for the send request

      // --- Step 3: Send MMS request to backend ---
      const sendMmsResponse = await axios.post(`${API_BASE_URL}/send-mms`, {
        destinationPhoneNumber: destinationPhoneNumber,
        messageBody: messageBody,
        s3Key: s3Key, // Use the key returned by the first backend call
      });

      console.log('MMS Send Response:', sendMmsResponse.data);
      setStatusMessage(`MMS Sent Successfully! Message ID: ${sendMmsResponse.data.messageId}`);
      // Optionally clear the form after success
      // setDestinationPhoneNumber('');
      // setMessageBody('');
      // setSelectedFile(null);
      // setUploadedS3Key(null);
      // if (fileInputRef.current) fileInputRef.current.value = '';

    } catch (error: any) {
      console.error('MMS Sending Process Error:', error);
      let detailedError = 'An unknown error occurred.';
      if (axios.isAxiosError(error)) {
          if (error.response) {
              // Error from backend API (presigned URL or send-mms)
              detailedError = `Backend Error (${error.response.status}): ${error.response.data?.error || error.response.data?.details || error.message}`;
          } else if (error.request) {
              // Error during S3 upload (network issue, CORS, bad presigned URL, timeout?)
              detailedError = `S3 Upload Error: Could not reach S3 or network issue. Check CORS, presigned URL validity/expiry, and network connection. Original message: ${error.message}`;
          } else {
              // Error setting up the request
              detailedError = `Request Setup Error: ${error.message}`;
          }
      } else if (error instanceof Error) {
         // Error thrown within the try block (e.g., missing URL/key)
         detailedError = `Client-Side Error: ${error.message}`;
      }
      setStatusMessage(`Error: ${detailedError}`);
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className=""App"">
      <h1>Send MMS via AWS Pinpoint & S3</h1>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor=""destination"">Destination Phone Number (E.164):</label>
          <input
            type=""tel""
            id=""destination""
            value={destinationPhoneNumber}
            onChange={(e) => setDestinationPhoneNumber(e.target.value)}
            placeholder=""+12065550100""
            required
            disabled={isLoading}
          />
        </div>
        <div>
          <label htmlFor=""message"">Message Body (Optional):</label>
          <textarea
            id=""message""
            value={messageBody}
            onChange={(e) => setMessageBody(e.target.value)}
            rows={3}
            disabled={isLoading}
          />
        </div>
        <div>
          <label htmlFor=""file"">Image File:</label>
          <input
            type=""file""
            id=""file""
            ref={fileInputRef}
            onChange={handleFileChange}
            accept=""image/jpeg, image/png, image/gif"" // Be specific about accepted types
            required
            disabled={isLoading}
          />
        </div>
        <button type=""submit"" disabled={isLoading || !selectedFile}>
          {isLoading ? 'Sending...' : 'Send MMS'}
        </button>
      </form>
      {statusMessage && <p className={`status ${statusMessage.startsWith('Error') ? 'error' : ''}`}>{statusMessage}</p>}
    </div>
  );
}

export default App;

frontend/src/App.css (Basic Styling):

css
.App {
  max-width: 500px;
  margin: 40px auto;
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 8px;
  font-family: sans-serif;
}

.App h1 {
  text-align: center;
  color: #333;
  margin-bottom: 25px;
}

.App div {
  margin-bottom: 15px;
}

.App label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
  color: #555;
}

.App input[type=""tel""],
.App textarea,
.App input[type=""file""] {
  width: 100%;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-sizing: border-box;
  font-size: 1rem;
}

.App input[type=""file""] {
  padding: 5px;
}

.App textarea {
  resize: vertical;
  min-height: 60px;
}

.App button {
  display: block;
  width: 100%;
  background-color: #007bff;
  color: white;
  padding: 12px 15px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 1rem;
  transition: background-color 0.2s ease;
  margin-top: 20px;
}

.App button:hover:not(:disabled) {
  background-color: #0056b3;
}

.App button:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}

.status {
  margin-top: 20px;
  padding: 12px;
  border-radius: 4px;
  background-color: #e9ecef;
  border: 1px solid #ced4da;
  font-size: 0.9rem;
  word-wrap: break-word;
}

.status.error {
  background-color: #f8d7da;
  color: #721c24;
  border-color: #f5c6cb;
}

Explanation:

  1. State: Manages phone number, message, selected file, loading status, status messages, and the S3 key returned after upload.
  2. handleFileChange: Updates the selectedFile state when a user chooses a file. Includes basic client-side validation for image type and size. Resets the uploadedS3Key if a new file is chosen.
  3. handleSubmit:
    • Prevents default form submission.
    • Performs basic validation (required fields, basic E.164 check).
    • Sets loading state and status message.
    • Step 1 (Get Presigned URL): Makes a POST request to /api/generate-presigned-url with the filename and type. Stores the returned presignedUrl and s3Key.
    • Step 2 (Upload to S3): Makes a PUT request directly to the presignedUrl. The request body is the selectedFile itself. Crucially, it sets the Content-Type header to match the file type – S3 relies on this.
    • Step 3 (Send MMS): Makes a POST request to /api/send-mms with the destination number, message body, and the s3Key received in Step 1.
    • Updates status messages throughout the process.
    • Includes robust error handling using a try...catch...finally block, attempting to distinguish between backend errors and S3 upload errors.
  4. Form: Standard HTML form elements bound to the React state. The submit button is disabled during loading or if no file is selected. Includes basic E.164 format validation hint.

Running the Frontend:

bash
# In the frontend directory
npm run dev

Navigate to the URL provided by Vite (usually http://localhost:5173).


5. Error Handling and Logging

  • Backend (server.js):
    • Uses try...catch blocks around AWS SDK calls and within endpoint handlers.
    • Logs errors to the console using console.error. For production, integrate a dedicated logging library (like Winston or Pino) to log structured data to files or a logging service (e.g., AWS CloudWatch Logs).
    • Returns specific HTTP status codes (400 for client errors, 500 for server errors) and JSON error messages including details where possible (error.message, error.name). Avoid leaking sensitive stack traces in production responses.
    • Includes a startup check for essential environment variables.
  • Frontend (App.tsx):
    • Uses a try...catch...finally block in handleSubmit to manage the multi-step process.
    • Updates the UI with user-friendly status and error messages via setStatusMessage.
    • Attempts to differentiate between backend API errors and S3 upload errors using axios.isAxiosError and checking error.response vs error.request.
    • Logs detailed errors to the browser console (console.error) for debugging.
    • Includes basic client-side validation (file type, size, phone format) before making API calls.

Frequently Asked Questions

How to send MMS messages from React app?

Use a Node.js backend with the AWS SDK to interact with Pinpoint and S3. The frontend uploads the image to S3 via a presigned URL and the backend triggers the MMS sending via Pinpoint, ensuring secure credential management.

What is the role of AWS S3 in MMS sending?

AWS S3 stores the media files (images) included in the MMS message. The Node.js backend generates presigned URLs, allowing the frontend to upload files directly to S3 without exposing AWS credentials.

Why does the frontend need presigned URLs for S3?

Presigned URLs allow direct browser uploads to S3 without exposing your AWS secret keys on the client-side. The backend generates these temporary URLs with specific permissions and expiry times, enhancing security.

When should I configure CORS for my S3 bucket?

Configure CORS for your S3 bucket when you're making requests from a different domain (like your frontend app's domain) to your S3 bucket. This is crucial for direct uploads from the browser using presigned URLs.

Can I use a standard short code for sending MMS with Pinpoint?

Standard short codes or 10DLC numbers may require additional registration or may not be universally MMS-capable. Toll-free numbers are generally recommended for MMS and are easier to configure for this purpose. Always verify the capabilities of your number in the Pinpoint console.

How to generate presigned URLs for S3 uploads?

On your Node.js backend, use the `@aws-sdk/s3-request-presigner` library. Create a `PutObjectCommand` for the desired S3 object, then call `getSignedUrl` with the S3 client, the command, and expiry options.

What is AWS Pinpoint SMS and Voice v2 used for?

Pinpoint SMS and Voice v2 is used to send SMS and MMS messages, including media attachments like images. It provides a reliable and scalable way to deliver messages from your application.

How to set up AWS IAM user for MMS application?

Create a new IAM user with programmatic access and attach a policy allowing S3 `PutObject`, `PutObjectAcl` (if necessary), and `pinpoint-sms-voice:SendMediaMessage` actions, specifying the S3 bucket ARN and region in the IAM policy resource definition.

What AWS region should I choose for S3 and Pinpoint?

Use the same AWS region for your S3 bucket, IAM user, and Pinpoint phone number. Consistency across these services is crucial for the proper functioning of the application.

Why is my frontend getting CORS errors when uploading to S3?

Ensure you have correctly configured CORS on your S3 bucket. The allowed origins in the CORS configuration must include your frontend app's origin (domain, port, protocol).

How to install necessary AWS SDK packages for Node.js backend?

Use npm: `npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner @aws-sdk/client-pinpoint-sms-voice-v2` installs the required packages to interact with S3 and Pinpoint.

What is the project structure for the Node.js backend?

The backend project includes `server.js` (main application), `.env` (environment variables), `package.json`, `node_modules`, and `.gitignore`. The `.env` file stores sensitive AWS credentials and configuration.

How to handle errors when sending MMS messages?

Implement `try...catch` blocks around AWS SDK calls in the backend. On the frontend, use `try...catch` and consider `axios.isAxiosError` to distinguish backend errors from S3 issues. Provide informative status updates on the frontend UI.

What phone number format is required for sending MMS?

Use the E.164 format for phone numbers when sending MMS messages with AWS Pinpoint, e.g., +12065550100. Ensure proper formatting both in the frontend input and in the backend validation.