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:
- 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).
- 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.
- 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:
+-----------------+ +----------------------+ +------------------------+ +------------------------+ +-----------+
| 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)
# 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/
)
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
)
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.
# 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.)
# 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)
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
-
Navigate to the IAM service in the AWS Console.
-
Go to Users -> Add users.
-
Enter a User name (e.g.,
mms-sender-app-user
). -
Select Access key - Programmatic access as the credential type.
-
Click Next: Permissions.
-
Choose Attach existing policies directly.
-
Click Create policy. A new tab/window will open.
-
In the policy editor, switch to the JSON tab.
-
Paste the following policy. IMPORTANT: You must replace
your-unique-mms-media-bucket-name
with your actual bucket name andYOUR_AWS_REGION
with the region you are using (must match.env
and S3 bucket region).{ ""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 onResource: ""*""
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""
. -
Click Next: Tags (optional), then Next: Review.
-
Give the policy a Name (e.g.,
MMS-Sender-App-Policy
). -
Click Create policy.
-
Close the policy editor tab/window and return to the user creation workflow.
-
Refresh the policy list and search for the policy you just created (
MMS-Sender-App-Policy
). Select it. -
Click Next: Tags (optional), Next: Review, then Create user.
-
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
- Navigate to the S3 service in the AWS Console.
- Click Create bucket.
- 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. - 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. - 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.
- Bucket Versioning: Disable (unless needed).
- Tags: Optional.
- Default encryption: Keep defaults (SSE-S3).
- 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).
-
Go to your newly created bucket in the S3 console.
-
Select the Permissions tab.
-
Scroll down to Cross-origin resource sharing (CORS) and click Edit.
-
Paste the following JSON configuration. Adjust
AllowedOrigins
for your frontend's actual URL in production.[ { ""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""
) toAllowedOrigins
. -
Click Save changes.
2.4. Acquire and Verify Pinpoint Origination Number
- Navigate to Amazon Pinpoint in the AWS Console.
- If you don't have a Pinpoint project, create one.
- Go to SMS and voice -> Phone numbers.
- 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).
- Follow the prompts to request the number. Note the region it's provisioned in (must match S3/IAM config).
- Once acquired, verify its capabilities. Select the number, and check the Capabilities section. Ensure MMS is listed and Status is Active.
- Copy the full phone number in E.164 format (e.g.,
+18005550199
) and add it to yourbackend/.env
file asPINPOINT_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
:
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:
- Dependencies: Imports Express, CORS, dotenv, AWS SDK clients, presigner, and crypto.
- 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. - Middleware:
cors()
: Enables Cross-Origin Resource Sharing, allowing requests from your frontend's origin. Crucially important.express.json()
: Parses incoming JSON request bodies.
/api/generate-presigned-url
Endpoint:- Accepts
filename
andcontentType
from the request body. - Generates a unique
s3Key
usingcrypto
to avoid filename collisions. It's good practice to include a prefix likemms-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 thes3Key
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.
- Accepts
/api/send-mms
Endpoint:- Accepts
destinationPhoneNumber
,messageBody
, and thes3Key
(obtained from the previous step) from the request body. - Constructs the
mediaUrl
in the requireds3://bucket-name/key
format. - Creates a
SendMediaMessageCommand
with the destination, origination number (from.env
), message body (optional), and theMediaUrls
array containing the S3 URL. - Uses the
pinpointClient
to send the command. - Returns a success response with the AWS
MessageId
or an error response.
- Accepts
- Server Start: Starts the Express server listening on the configured port.
Running the Backend:
# 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
:
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):
.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:
- State: Manages phone number, message, selected file, loading status, status messages, and the S3 key returned after upload.
handleFileChange
: Updates theselectedFile
state when a user chooses a file. Includes basic client-side validation for image type and size. Resets theuploadedS3Key
if a new file is chosen.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 returnedpresignedUrl
ands3Key
. - Step 2 (Upload to S3): Makes a PUT request directly to the
presignedUrl
. The request body is theselectedFile
itself. Crucially, it sets theContent-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 thes3Key
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.
- 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:
# 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.
- Uses
- Frontend (
App.tsx
):- Uses a
try...catch...finally
block inhandleSubmit
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 checkingerror.response
vserror.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.
- Uses a