Send MMS Messages with Vite, React/Vue, Node.js and Infobip API
Complete full-stack guide to building an MMS messaging application using Vite (React/Vue), Node.js/Express, and Infobip API. Includes file upload handling, validation, and production deployment.
This guide walks you through building a system to send Multimedia Messaging Service (MMS) messages via the Infobip API. You'll create a web interface using Vite (with React examples, easily adaptable for Vue) and a Node.js backend API using Express.
Project Overview and Goals
What You'll Build:
A full-stack application that enables users to send MMS messages (text + image) to a specified phone number using Infobip's communication platform.
Frontend: A web form built with Vite and React (or Vue) to input the recipient's phone number, message text, and select an image file.
Backend: A Node.js/Express API that receives data from the frontend, securely handles the media file, validates input, and interacts with the Infobip MMS API to send the message.
Problem Solved:
This guide provides a straightforward, secure, and reliable method to programmatically send MMS messages from a web application, leveraging Infobip's infrastructure. It delivers a practical blueprint for integrating rich media messaging into notification systems, marketing tools, or customer support platforms.
Backend: Node.js, Express.js, Axios (chosen for direct control over the multipart request), Multer (for file uploads), dotenv (for environment variables), cors, libphonenumber-js (for phone number validation)
Third-Party Service: Infobip API (specifically the MMS endpoint)
Why these technologies?
Vite: Delivers an extremely fast development server and optimized build process for modern frontend frameworks like React and Vue.
React/Vue: Popular, component-based libraries for building interactive user interfaces.
Node.js/Express: The standard, efficient stack for building backend APIs with JavaScript, well-suited for handling web requests and integrating with external services.
Axios: A promise-based HTTP client that simplifies requests to the backend API and the Infobip API. Axios provides fine-grained control over constructing the multipart/form-data requests required by the Infobip binary MMS endpoint. (The official infobip-api-node-sdk is an alternative but may require separate investigation for binary MMS upload handling.)
Multer: Essential Node.js middleware for handling multipart/form-data file uploads.
libphonenumber-js: Robust library for parsing and validating phone numbers.
Infobip: A leading Communications Platform as a Service (CPaaS) provider with robust APIs for various channels, including MMS.
System Architecture:
Text
+-----------------+ +----------------------+ +----------------+
| Vite Frontend |----->| Node.js/Express API |----->| Infobip MMS API |
| (React/Vue) | | (Handles Auth, | | |
| - Input Form | | File Upload, | | |
| - File Selection| | Validation, API Call)| | |
+-----------------+ +----------------------+ +----------------+
| User selects file | API receives form data | API sends request
| & submits form | & file, validates, | with media & data
| (multipart/form-data)| forwards to Infobip | to recipient
Prerequisites:
Node.js and npm (or yarn): Install Node.js on your development machine. Download from https://nodejs.org/
Infobip Account: Register for an Infobip account. A free trial account works for testing. You'll need your API Key and Base URL. Sign up at https://www.infobip.com/signup
Basic knowledge: Familiarity with JavaScript, Node.js, React or Vue, command-line usage, and REST APIs
Test Phone Number: For free trial accounts, use the verified phone number associated with your Infobip account
Git & GitHub Account: Optional, for version control and deployment
Final Outcome:
By the end of this guide, you will have:
A functional Vite frontend allowing users to specify a recipient, text message, and image.
A secure Node.js backend API endpoint (/send-mms) that handles file uploads, validates input, and communicates with Infobip.
Working integration with the Infobip MMS API to send messages.
Basic error handling, input validation, and configuration management using environment variables.
1. Setting up the Project
Create a monorepo-like structure with separate client and server directories within a root project folder. This keeps dependencies and configurations distinct.
Add .env files in both server and client (if used) to your .gitignore.
2. Set up the Backend (Node.js/Express):
Bash
# Create the server directorymkdir server
cd server
# Initialize Node.js project
npm init -y
# Install dependencies
npm install express axios dotenv multer cors libphonenumber-js form-data
# Install development dependencies (optional but recommended)
npm install --save-dev nodemon jest supertest
express: Web framework for Node.js
axios: HTTP client to call the Infobip API
dotenv: Loads environment variables from a .env file
multer: Middleware for handling multipart/form-data (file uploads)
cors: Middleware to enable Cross-Origin Resource Sharing
libphonenumber-js: For validating recipient phone numbers
form-data: Library to construct multipart/form-data payloads programmatically
nodemon: Utility that automatically restarts the node application when file changes are detected
Create a file named .env in the server directory. Never commit this file to version control.
.env
# server/.env
# Infobip Credentials (Get from Infobip Portal: https://portal.infobip.com/settings/accounts/api-keys)
INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY
INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL
# Your Infobip registered phone number (sender ID for MMS, check Infobip docs for specifics)
INFOBIP_SENDER_NUMBER=YOUR_INFOBIP_SENDER_PHONE_NUMBER
# Server Configuration
PORT=5001 # Port the backend API will run on
CLIENT_URL=http://localhost:5173 # Development URL of the Vite frontend
Replace placeholders with your actual Infobip credentials and sender number. The Base URL typically looks like xxxxxx.api.infobip.com.
CLIENT_URL is used for CORS configuration. 5173 is the default Vite dev server port. Adjust if needed. For production, this should be your deployed frontend URL.
5. Set up the Frontend (Vite + React):
Navigate back to the root project directory:
Bash
# From the server directorycd ..
Create the Vite project (choose React when prompted):
Bash
# Creates a 'client' directory with the Vite project
npm create vite@latest client -- --template react
# Or for Vue: npm create vite@latest client -- --template vuecd client
# Install dependencies
npm install
# Install axios for making API calls from the frontend
npm install axios
6. Frontend Environment File (Optional but Recommended):
For deployment flexibility, create a .env file in the client directory for the backend URL:
.env
# client/.env
# URL of the backend API
VITE_BACKEND_API_URL=http://localhost:5001
Add client/.env to your .gitignore.
7. Project Structure:
Your project structure should now look similar to this:
Text
infobip-mms-app/
├── .gitignore
├── client/
│ ├── node_modules/
│ ├── public/
│ ├── src/
│ │ ├── App.css
│ │ ├── App.jsx
│ │ ├── index.css
│ │ └── main.jsx
│ ├── .env # Optional: Frontend env vars (add to .gitignore!)
│ ├── index.html
│ ├── package.json
│ └── vite.config.js
└── server/
├── node_modules/
├── tests/ # For tests (Section 13)
├── .env # IMPORTANT: Contains secrets (add to .gitignore!)
├── package.json
└── server.js # Will be created in next step
Architectural Decisions & Configuration:
Separation of Concerns: Keeping frontend (client) and backend (server) code separate clarifies responsibilities and simplifies dependency management.
Environment Variables (.env): Storing secrets (API keys) and configuration (ports, URLs) outside the codebase is crucial for security and deployment flexibility.
CORS: The cors middleware on the backend explicitly allows requests only from the frontend URL specified in server/.env.
Multer: Used for parsing multipart/form-data requests containing file uploads.
Phone Number Validation: Using libphonenumber-js on the backend ensures recipient numbers are in a valid format before attempting to send.
2. Implementing Core Functionality (Backend API)
Build the Express server and the endpoint to handle MMS sending. This implementation uses axios for direct control over constructing the multipart/form-data request required by the Infobip binary MMS endpoint.
1. Create the Server File (server/server.js):
JavaScript
// server/server.jsrequire('dotenv').config(); // Load .env variables earlyconst express = require('express');
const cors = require('cors');
const multer = require('multer');
const axios = require('axios');
constFormData = require('form-data'); // To construct multipart payloadconst { parsePhoneNumberFromString } = require('libphonenumber-js');
const fs = require('fs');
const path = require('path');
const app = express();
const port = process.env.PORT || 5001;
// --- Middleware ---// CORS Configurationconst corsOptions = {
origin: process.env.CLIENT_URL, // Allow only the frontend originoptionsSuccessStatus: 200// For legacy browser compatibility
};
app.use(cors(corsOptions));
// Basic JSON parsing (though less relevant for our multipart endpoint)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Multer Configuration (for handling file uploads)// **Warning:** `memoryStorage` loads the entire file into RAM. This is simple but// not suitable for production environments handling large files or high traffic,// as it can lead to excessive memory consumption. For production, strongly// consider using `multer.diskStorage` or streaming uploads if possible.const storage = multer.memoryStorage();
const upload = multer({
storage: storage,
limits: { fileSize: 10 * 1024 * 1024 }, // Example: 10MB limit (Adjust based on Infobip limits)fileFilter: (req, file, cb) => {
// Basic image type check (enhance as needed)const allowedTypes = /jpeg|jpg|png|gif/;
const mimetype = allowedTypes.test(file.mimetype);
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
if (mimetype && extname) {
returncb(null, true);
}
cb(newError(`Error: File upload only supports the following filetypes - ${allowedTypes}`));
}
}).single('mediaFile'); // Expects a single file field named 'mediaFile' in the form data// --- Infobip Configuration ---constINFOBIP_API_KEY = process.env.INFOBIP_API_KEY;
constINFOBIP_BASE_URL = process.env.INFOBIP_BASE_URL;
constINFOBIP_SENDER_NUMBER = process.env.INFOBIP_SENDER_NUMBER; // Your registered Infobip senderif (!INFOBIP_API_KEY || !INFOBIP_BASE_URL || !INFOBIP_SENDER_NUMBER) {
console.error(""FATALERROR: InfobipAPIKey, BaseURL, or SenderNumber is missing in .env file."");
process.exit(1); // Exit if essential config is missing
}
// --- Routes ---
app.get('/health', (req, res) => {
res.status(200).json({ status: 'UP' });
});
app.post('/send-mms', (req, res) => {
upload(req, res, async (err) => {
// Handle Multer errors (file size, type, etc.)if (err instanceof multer.MulterError) {
console.error('Multer Error:', err);
return res.status(400).json({ success: false, error: 'File upload error: ' + err.message });
} elseif (err) {
console.error('File Filter Error:', err);
return res.status(400).json({ success: false, error: err.message });
}
// Check if file and other fields existif (!req.file) {
return res.status(400).json({ success: false, error: 'No media file uploaded.' });
}
const { to, text } = req.body;
if (!to || !text) {
return res.status(400).json({ success: false, error: 'Recipient number (to) and text message are required.' });
}
// Validate phone numberconst phoneNumber = parsePhoneNumberFromString(to); // Assumes E.164 if no default country specifiedif (!phoneNumber || !phoneNumber.isValid()) {
console.warn(`Invalid phone number received: ${to}`);
return res.status(400).json({ success: false, error: 'Invalid recipient phone number format. Please use international E.164 format (e.g., +447123456789).' });
}
const formattedNumber = phoneNumber.number; // Use the validated E.164 number// --- Prepare Infobip API Request ---// MMS Binary endpoint (Verify against current Infobip documentation: https://www.infobip.com/docs/api)const mmsEndpoint = `${INFOBIP_BASE_URL}/mms/1/binary`;
// **Crucially, always verify this endpoint and the required payload structure// against the current official Infobip API documentation before deployment.**// Construct the multipart/form-data payload using 'form-data' libraryconst formData = newFormData();
// Append the JSON part (Infobip typically expects this as 'head') - CHECK DOCSconst head = {
from: INFOBIP_SENDER_NUMBER,
to: formattedNumber, // Use validated numbersubject: 'MMS from App', // Optional subject// Add text here if required by the binary endpoint structure, or it might be separate// text: text, // Check Infobip docs if text goes in 'head' or as a separate part
};
formData.append('head', JSON.stringify(head), { contentType: 'application/json' });
// Append the text part if needed as a separate field (CHECK DOCS)// formData.append('text', text);// Append the binary file part (Infobip typically expects this as 'media') - CHECK DOCS
formData.append('media', req.file.buffer, {
filename: req.file.originalname,
contentType: req.file.mimetype,
});
// --- Make API Call to Infobip ---try {
console.log(`Attempting to send MMS via Infobip to: ${formattedNumber}`);
const response = await axios.post(mmsEndpoint, formData, {
headers: {
'Authorization': `App ${INFOBIP_API_KEY}`,
...formData.getHeaders() // IMPORTANT: Let form-data set the Content-Type and boundary
},
timeout: 30000, // 30 seconds timeout
});
console.log('Infobip API Response Status:', response.status);
console.log('Infobip API Response Data:', response.data);
res.status(response.status).json({ success: true, data: response.data });
} catch (error) {
const timestamp = newDate().toISOString();
let errorMsg = 'Infobip API Error';
let errorDetails = error.message;
let infobipResponse = null;
let infobipStatus = 500; // Default to internal server errorif (error.response) {
// Error response received from Infobip
errorMsg = 'Error response from Infobip API';
errorDetails = error.response.data;
infobipStatus = error.response.status;
infobipResponse = error.response.data;
console.error(`[${timestamp}] ${errorMsg} - Status: ${infobipStatus}`, errorDetails);
} elseif (error.request) {
// Request made but no response received (timeout, network issue)
errorMsg = 'No response received from Infobip API';
console.error(`[${timestamp}] ${errorMsg}`, error.request);
infobipStatus = 504; // Gateway Timeout might be appropriate
} else {
// Error setting up the requestconsole.error(`[${timestamp}] ${errorMsg} - Setup Error:`, error.message);
}
// Log structured errorconst errorLog = {
timestamp: timestamp,
message: errorMsg,
errorDetails: errorDetails,
infobipStatus: infobipStatus,
requestData: { to: formattedNumber } // Log non-sensitive parts
};
console.error(""DetailedErrorLog:"", JSON.stringify(errorLog));
res.status(infobipStatus).json({ success: false, error: infobipResponse || { message: errorMsg } });
}
});
});
// --- Start Server ---// Only start listening if the script is run directly (not required by a test runner)if (require.main === module) {
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
console.log(`Accepting requests from: ${process.env.CLIENT_URL}`);
});
}
// Export the app instance for testing purposesmodule.exports = app;
Code Explanation:
Dependencies & Setup: Includes form-data and libphonenumber-js. Loads .env, creates Express app.
Phone Validation: Uses libphonenumber-js to parse and validate the to number. Rejects if invalid, otherwise uses the formatted E.164 number.
Prepare Infobip Request:
Defines mmsEndpoint with a comment to verify it. Emphasizes checking official docs.
Uses form-data library to build the multipart/form-data payload.
Appends the head (JSON part) and media (file part) according to typical Infobip structure. Crucially reinforces checking Infobip docs for the exact required field names ('head', 'media', 'text' placement).
Call Infobip API:
Uses axios.post.
Sets Authorization header.
Spreads formData.getHeaders() into axios headers config to correctly set Content-Type: multipart/form-data; boundary=....
Includes enhanced try...catch for logging structured error details (distinguishing between Infobip response errors, network errors, and setup errors).
Response: Sends Infobip's response or a structured error back to the frontend.
Start Server & Export: Starts the server only if run directly, and exports app for use in tests.
3. Building the Frontend (Vite + React)
Create the React component for the form.
1. Create the Form Component (client/src/App.jsx):
Replace the contents of client/src/App.jsx with the following:
JSX
// client/src/App.jsximportReact, { useState } from'react';
import axios from'axios';
import'./App.css'; // We'll create this for basic stylingfunctionApp() {
const [to, setTo] = useState('');
const [text, setText] = useState('');
const [mediaFile, setMediaFile] = useState(null);
const [statusMessage, setStatusMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
// Get backend URL from environment variable, with fallback for local devconst backendUrl = import.meta.env.VITE_BACKEND_API_URL || 'http://localhost:5001';
const sendMmsEndpoint = `${backendUrl}/send-mms`;
consthandleFileChange = (event) => {
const file = event.target.files[0];
// Optional: Add basic client-side size checkconst maxSize = 10 * 1024 * 1024; // 10MB (match backend or slightly lower)if (file && file.size > maxSize) {
setStatusMessage(`Error: File size exceeds ${maxSize / 1024 / 1024}MB limit.`);
setIsError(true);
setMediaFile(null);
event.target.value = null; // Clear the inputreturn;
}
setMediaFile(file);
setStatusMessage(''); // Clear status on new file selectionsetIsError(false);
};
consthandleSubmit = async (event) => {
event.preventDefault();
if (!to || !text || !mediaFile) {
setStatusMessage('Please fill in all fields and select a valid media file.');
setIsError(true);
return;
}
// Basic client-side check for E.164 format (improves UX, but backend validation is key)// This regex is basic, libphonenumber-js on backend is more robust.if (!/^\+?[1-9]\d{1,14}$/.test(to)) {
setStatusMessage('Please enter the recipient number in E.164 format (e.g., +447123456789).');
setIsError(true);
return;
}
setIsLoading(true);
setStatusMessage('Sending MMS...');
setIsError(false);
// Use FormData to send multipart dataconst formData = newFormData();
formData.append('to', to);
formData.append('text', text);
formData.append('mediaFile', mediaFile); // Key must match Multer config on backendtry {
const response = await axios.post(sendMmsEndpoint, formData, {
headers: {
// 'Content-Type': 'multipart/form-data' // Axios usually sets this automatically for FormData
},
timeout: 45000, // 45 seconds (allow for backend processing + Infobip call)
});
console.log('Backend Response:', response.data);
// Extract a meaningful status from the Infobip response if possibleconst infobipStatus = response.data?.data?.messages?.[0]?.status?.name || 'SUBMITTED';
setStatusMessage(`Successfully sent MMS! Infobip status: ${infobipStatus}`);
setIsError(false);
// Optionally clear the form// setTo('');// setText('');// setMediaFile(null);// event.target.reset(); // Reset file input if needed
} catch (error) {
console.error('Error sending MMS:', error);
let errorMessage = 'Failed to send MMS.';
if (error.response) {
// Error from backend APIconst backendError = error.response.data?.error;
errorMessage += ` Server Error (${error.response.status}): ${backendError?.message || JSON.stringify(backendError || error.response.data)}`;
} elseif (error.request) {
// Request made but no response received (network issue, backend down?)
errorMessage += ' No response received from the server. Is it running or unreachable?';
} else {
// Error setting up the request
errorMessage += ` Request Setup Error: ${error.message}`;
}
setStatusMessage(errorMessage);
setIsError(true);
} finally {
setIsLoading(false);
}
};
return (
<divclassName="app-container"><h1>Send Infobip MMS</h1><formonSubmit={handleSubmit}className="mms-form"><divclassName="form-group"><labelhtmlFor="to">Recipient Phone:</label><inputtype="tel"id="to"value={to}onChange={(e) => setTo(e.target.value)}
placeholder="e.g., +447123456789 (E.164 format)"
required
disabled={isLoading}
/>
</div><divclassName="form-group"><labelhtmlFor="text">Message Text:</label><textareaid="text"value={text}onChange={(e) => setText(e.target.value)}
rows="3"
required
disabled={isLoading}
></textarea></div><divclassName="form-group"><labelhtmlFor="mediaFile">Media File (Image):</label><inputtype="file"id="mediaFile"accept="image/jpeg, image/png, image/gif" // MatchbackendfilteronChange={handleFileChange}required // Fileinputrequiredattributemightbehavedifferentlyacrossbrowsersdisabled={isLoading}
/>
{mediaFile && <pclassName="file-info">Selected: {mediaFile.name} ({(mediaFile.size / 1024 / 1024).toFixed(2)} MB)</p>}
</div><buttontype="submit"disabled={isLoading || !mediaFile}>
{isLoading ? 'Sending...' : 'Send MMS'}
</button></form>
{statusMessage && (
<divclassName={`status-message ${isError ? 'error' : 'success'}`}>
{statusMessage}
</div>
)}
<pclassName="api-url-note">Sending requests to: <code>{sendMmsEndpoint}</code></p></div>
);
}
exportdefaultApp;
2. Add Basic Styling (client/src/App.css):
Create a file client/src/App.css (or update the existing one):
Ensure the App.css file is imported in your main entry point:
JSX
// client/src/main.jsximportReactfrom'react'importReactDOMfrom'react-dom/client'importAppfrom'./App.jsx'import'./index.css'// Keep default index.css if neededimport'./App.css'// Import your App specific stylesReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode><App /></React.StrictMode>,
)
Frontend Code Explanation:
State: Manages form inputs (to, text, mediaFile), loading/status messages.
Backend URL: Reads VITE_BACKEND_API_URL from environment variables (via import.meta.env) with a fallback to http://localhost:5001 for easy local development. A note about setting this variable (in client/.env) is added in Section 1.
handleFileChange: Updates mediaFile state, includes an optional client-side size check for better UX.
handleSubmit:
Prevents default submission.
Performs client-side validation (required fields, basic phone format check - backend validation is still essential).
Sets loading state.
Creates FormData and appends fields.
Uses axios.post to send data to the backend endpoint derived from the environment variable.
Handles success and displays status from Infobip via the backend response.
Handles errors gracefully, displaying informative messages based on the error type (backend response, network error, etc.).
Resets loading state in finally.
JSX: Renders the form with appropriate input types (tel, file with accept), provides feedback during loading, and displays status messages. Includes a note showing the API endpoint being used.
4. Integrating with Infobip (Recap & Credentials)
This section focuses on ensuring the Infobip details are correctly obtained and used.
Navigate to the API Key Management section (e.g., ""Developers"" -> ""API Keys"").
Create a new API key if needed. Give it a descriptive name.
Copy the API Key: This is your INFOBIP_API_KEY. Treat it securely.
Find your Base URL: Usually displayed near API keys or on the developer overview page (e.g., xxxxxx.api.infobip.com). This is your INFOBIP_BASE_URL.
Identify Sender Number: For MMS, locate the specific number enabled for MMS sending on your Infobip account (e.g., under ""Numbers""). For free trials, this is often your verified personal number. This is your INFOBIP_SENDER_NUMBER. Check Section 8 for more details on sender ID requirements.
2. Secure Storage (server/.env):
As configured in Step 1, place these values securely in your server/.env file:
The backend code (server/server.js) loads these values using require('dotenv').config() and accesses them via process.env.
Environment Variable Explanation:
INFOBIP_API_KEY: (String) Your secret API key from the Infobip Portal.
INFOBIP_BASE_URL: (String) Your account-specific API domain from the Infobip Portal.
INFOBIP_SENDER_NUMBER: (String) The E.164 formatted phone number or sender ID provisioned for MMS sending in your Infobip account.
PORT: (Number) Network port for the backend server (default: 5001).
CLIENT_URL: (String) Full URL of the allowed frontend origin for CORS (e.g., http://localhost:5173 for dev, your production frontend URL for deployment).
Security: Never hardcode API keys directly in your source code. Always use environment variables loaded from a .env file (which is excluded from version control via .gitignore) or secure configuration management systems in production.
Frequently asked questions
Set up a Node.js/Express backend with the Infobip API. This involves installing necessary libraries like axios, multer, and libphonenumber-js, configuring your .env file with Infobip credentials, and creating an API endpoint to handle MMS sending logic. The backend will manage file uploads, validate inputs, and interact with the Infobip MMS API.
The Infobip MMS API is part of Infobip's CPaaS (Communications Platform as a Service) offering. It allows developers to send multimedia messages (text and images) programmatically through their platform. This API requires specific request formatting, including multipart/form-data for handling binary media files, and authentication using API keys.
Vite is a modern frontend build tool that offers a significantly faster development experience and optimized build process for frameworks like React and Vue. Its speed and efficiency make it a great choice for building the user interface for sending MMS messages through Infobip.
Phone number validation using libphonenumber-js should always be performed on the backend before sending any messages through the Infobip API. This ensures data integrity and avoids unnecessary API calls with invalid recipient numbers. Client-side validation can enhance UX but shouldn't replace server-side checks.
Yes, the provided example uses React, but it's easily adaptable to Vue.js. Vite supports both frameworks, and the backend API remains the same regardless of the frontend framework used. The core concepts and setup remain the same, just the frontend components need adjustment.
Use the multer middleware in your Node.js backend to handle multipart/form-data requests, which are used for file uploads. multer.memoryStorage can be used for testing. The frontend should send the image file as part of the form data to the backend endpoint, matching the field name used by Multer.
Axios is used as the HTTP client for both frontend and backend communication with the Infobip API. It simplifies making HTTP requests and provides control over constructing multipart/form-data payloads required by Infobip's MMS endpoint.
The monorepo-like structure, with separate 'client' and 'server' directories, keeps frontend and backend code cleanly separated. This improves code organization, simplifies dependency management, and makes it easier to work on each part of the application independently.
Configure CORS (Cross-Origin Resource Sharing) on your backend using the cors middleware. Allow requests only from the specific URL of your frontend (e.g., http://localhost:5173 in development) by specifying the origin in the cors options.
Environment variables store sensitive data (like API keys) and configuration settings outside of your codebase. This enhances security and makes deployment easier by allowing configuration changes without modifying the code itself. Use a .env file locally and a secure system in production.
The request to the Infobip MMS API must be a multipart/form-data request. It typically includes a 'head' section with JSON data (sender, recipient, subject, etc.) and a 'media' section containing the image file. Always check the official Infobip documentation for the latest structure.
Log in to your Infobip account portal. Go to the API Key Management section (often under 'Developers' or 'Settings'). Create a new API key or use an existing one, copy its value, and locate your account-specific Base URL, which is usually displayed in the same area.
The Infobip Sender Number is the phone number or alphanumeric sender ID that you have registered and configured within your Infobip account for sending MMS messages. It is a required parameter in the API request and identifies the sender of the message.
Implement robust error handling in both frontend and backend. The backend should catch errors during file upload, validation, and API calls, returning informative messages. The frontend should handle errors from the backend gracefully and display user-friendly error messages.
This guide walks you through building a system to send Multimedia Messaging Service (MMS) messages via the Infobip API. You'll create a web interface using Vite (with React examples, easily adaptable for Vue) and a Node.js backend API using Express.
Project Overview and Goals
What You'll Build:
A full-stack application that enables users to send MMS messages (text + image) to a specified phone number using Infobip's communication platform.
Problem Solved:
This guide provides a straightforward, secure, and reliable method to programmatically send MMS messages from a web application, leveraging Infobip's infrastructure. It delivers a practical blueprint for integrating rich media messaging into notification systems, marketing tools, or customer support platforms.
Technologies Used:
dotenv(for environment variables),cors,libphonenumber-js(for phone number validation)Why these technologies?
multipart/form-datarequests required by the Infobip binary MMS endpoint. (The officialinfobip-api-node-sdkis an alternative but may require separate investigation for binary MMS upload handling.)multipart/form-datafile uploads.libphonenumber-js: Robust library for parsing and validating phone numbers.System Architecture:
Prerequisites:
Final Outcome:
By the end of this guide, you will have:
/send-mms) that handles file uploads, validates input, and communicates with Infobip.1. Setting up the Project
Create a monorepo-like structure with separate
clientandserverdirectories within a root project folder. This keeps dependencies and configurations distinct.1. Create Project Root Directory:
mkdir infobip-mms-app cd infobip-mms-app # Optional: Initialize Git # git init # echo "node_modules/" >> .gitignore # echo ".env*" >> .gitignore # echo "client/dist/" >> .gitignore # echo "client/node_modules/" >> .gitignore # echo "server/node_modules/" >> .gitignoreAdd
.envfiles in bothserverandclient(if used) to your.gitignore.2. Set up the Backend (Node.js/Express):
# Create the server directory mkdir server cd server # Initialize Node.js project npm init -y # Install dependencies npm install express axios dotenv multer cors libphonenumber-js form-data # Install development dependencies (optional but recommended) npm install --save-dev nodemon jest supertestexpress: Web framework for Node.jsaxios: HTTP client to call the Infobip APIdotenv: Loads environment variables from a.envfilemulter: Middleware for handlingmultipart/form-data(file uploads)cors: Middleware to enable Cross-Origin Resource Sharinglibphonenumber-js: For validating recipient phone numbersform-data: Library to constructmultipart/form-datapayloads programmaticallynodemon: Utility that automatically restarts the node application when file changes are detectedjest,supertest: For backend testing (Section 13)3. Configure Backend Scripts (
server/package.json):Open
server/package.jsonand modify thescriptssection. Ensuremainpoints toserver.jsor your entry file.{ "name": "server", "version": "1.0.0", "description": "", "main": "server.js", "scripts": { "start": "node server.js", "dev": "nodemon server.js", "test": "jest" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "axios": "^...", "cors": "^...", "dotenv": "^...", "express": "^...", "form-data": "^...", "libphonenumber-js": "^...", "multer": "^..." }, "devDependencies": { "jest": "^...", "nodemon": "^...", "supertest": "^..." } }4. Create Backend Environment File (
server/.env):Create a file named
.envin theserverdirectory. Never commit this file to version control.xxxxxx.api.infobip.com.CLIENT_URLis used for CORS configuration.5173is the default Vite dev server port. Adjust if needed. For production, this should be your deployed frontend URL.5. Set up the Frontend (Vite + React):
Navigate back to the root project directory:
# From the server directory cd ..Create the Vite project (choose React when prompted):
# Creates a 'client' directory with the Vite project npm create vite@latest client -- --template react # Or for Vue: npm create vite@latest client -- --template vue cd client # Install dependencies npm install # Install axios for making API calls from the frontend npm install axios6. Frontend Environment File (Optional but Recommended):
For deployment flexibility, create a
.envfile in theclientdirectory for the backend URL:Add
client/.envto your.gitignore.7. Project Structure:
Your project structure should now look similar to this:
Architectural Decisions & Configuration:
client) and backend (server) code separate clarifies responsibilities and simplifies dependency management..env): Storing secrets (API keys) and configuration (ports, URLs) outside the codebase is crucial for security and deployment flexibility.corsmiddleware on the backend explicitly allows requests only from the frontend URL specified inserver/.env.multipart/form-datarequests containing file uploads.libphonenumber-json the backend ensures recipient numbers are in a valid format before attempting to send.2. Implementing Core Functionality (Backend API)
Build the Express server and the endpoint to handle MMS sending. This implementation uses
axiosfor direct control over constructing themultipart/form-datarequest required by the Infobip binary MMS endpoint.1. Create the Server File (
server/server.js):// server/server.js require('dotenv').config(); // Load .env variables early const express = require('express'); const cors = require('cors'); const multer = require('multer'); const axios = require('axios'); const FormData = require('form-data'); // To construct multipart payload const { parsePhoneNumberFromString } = require('libphonenumber-js'); const fs = require('fs'); const path = require('path'); const app = express(); const port = process.env.PORT || 5001; // --- Middleware --- // CORS Configuration const corsOptions = { origin: process.env.CLIENT_URL, // Allow only the frontend origin optionsSuccessStatus: 200 // For legacy browser compatibility }; app.use(cors(corsOptions)); // Basic JSON parsing (though less relevant for our multipart endpoint) app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Multer Configuration (for handling file uploads) // **Warning:** `memoryStorage` loads the entire file into RAM. This is simple but // not suitable for production environments handling large files or high traffic, // as it can lead to excessive memory consumption. For production, strongly // consider using `multer.diskStorage` or streaming uploads if possible. const storage = multer.memoryStorage(); const upload = multer({ storage: storage, limits: { fileSize: 10 * 1024 * 1024 }, // Example: 10MB limit (Adjust based on Infobip limits) fileFilter: (req, file, cb) => { // Basic image type check (enhance as needed) const allowedTypes = /jpeg|jpg|png|gif/; const mimetype = allowedTypes.test(file.mimetype); const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase()); if (mimetype && extname) { return cb(null, true); } cb(new Error(`Error: File upload only supports the following filetypes - ${allowedTypes}`)); } }).single('mediaFile'); // Expects a single file field named 'mediaFile' in the form data // --- Infobip Configuration --- const INFOBIP_API_KEY = process.env.INFOBIP_API_KEY; const INFOBIP_BASE_URL = process.env.INFOBIP_BASE_URL; const INFOBIP_SENDER_NUMBER = process.env.INFOBIP_SENDER_NUMBER; // Your registered Infobip sender if (!INFOBIP_API_KEY || !INFOBIP_BASE_URL || !INFOBIP_SENDER_NUMBER) { console.error(""FATAL ERROR: Infobip API Key, Base URL, or Sender Number is missing in .env file.""); process.exit(1); // Exit if essential config is missing } // --- Routes --- app.get('/health', (req, res) => { res.status(200).json({ status: 'UP' }); }); app.post('/send-mms', (req, res) => { upload(req, res, async (err) => { // Handle Multer errors (file size, type, etc.) if (err instanceof multer.MulterError) { console.error('Multer Error:', err); return res.status(400).json({ success: false, error: 'File upload error: ' + err.message }); } else if (err) { console.error('File Filter Error:', err); return res.status(400).json({ success: false, error: err.message }); } // Check if file and other fields exist if (!req.file) { return res.status(400).json({ success: false, error: 'No media file uploaded.' }); } const { to, text } = req.body; if (!to || !text) { return res.status(400).json({ success: false, error: 'Recipient number (to) and text message are required.' }); } // Validate phone number const phoneNumber = parsePhoneNumberFromString(to); // Assumes E.164 if no default country specified if (!phoneNumber || !phoneNumber.isValid()) { console.warn(`Invalid phone number received: ${to}`); return res.status(400).json({ success: false, error: 'Invalid recipient phone number format. Please use international E.164 format (e.g., +447123456789).' }); } const formattedNumber = phoneNumber.number; // Use the validated E.164 number // --- Prepare Infobip API Request --- // MMS Binary endpoint (Verify against current Infobip documentation: https://www.infobip.com/docs/api) const mmsEndpoint = `${INFOBIP_BASE_URL}/mms/1/binary`; // **Crucially, always verify this endpoint and the required payload structure // against the current official Infobip API documentation before deployment.** // Construct the multipart/form-data payload using 'form-data' library const formData = new FormData(); // Append the JSON part (Infobip typically expects this as 'head') - CHECK DOCS const head = { from: INFOBIP_SENDER_NUMBER, to: formattedNumber, // Use validated number subject: 'MMS from App', // Optional subject // Add text here if required by the binary endpoint structure, or it might be separate // text: text, // Check Infobip docs if text goes in 'head' or as a separate part }; formData.append('head', JSON.stringify(head), { contentType: 'application/json' }); // Append the text part if needed as a separate field (CHECK DOCS) // formData.append('text', text); // Append the binary file part (Infobip typically expects this as 'media') - CHECK DOCS formData.append('media', req.file.buffer, { filename: req.file.originalname, contentType: req.file.mimetype, }); // --- Make API Call to Infobip --- try { console.log(`Attempting to send MMS via Infobip to: ${formattedNumber}`); const response = await axios.post(mmsEndpoint, formData, { headers: { 'Authorization': `App ${INFOBIP_API_KEY}`, ...formData.getHeaders() // IMPORTANT: Let form-data set the Content-Type and boundary }, timeout: 30000, // 30 seconds timeout }); console.log('Infobip API Response Status:', response.status); console.log('Infobip API Response Data:', response.data); res.status(response.status).json({ success: true, data: response.data }); } catch (error) { const timestamp = new Date().toISOString(); let errorMsg = 'Infobip API Error'; let errorDetails = error.message; let infobipResponse = null; let infobipStatus = 500; // Default to internal server error if (error.response) { // Error response received from Infobip errorMsg = 'Error response from Infobip API'; errorDetails = error.response.data; infobipStatus = error.response.status; infobipResponse = error.response.data; console.error(`[${timestamp}] ${errorMsg} - Status: ${infobipStatus}`, errorDetails); } else if (error.request) { // Request made but no response received (timeout, network issue) errorMsg = 'No response received from Infobip API'; console.error(`[${timestamp}] ${errorMsg}`, error.request); infobipStatus = 504; // Gateway Timeout might be appropriate } else { // Error setting up the request console.error(`[${timestamp}] ${errorMsg} - Setup Error:`, error.message); } // Log structured error const errorLog = { timestamp: timestamp, message: errorMsg, errorDetails: errorDetails, infobipStatus: infobipStatus, requestData: { to: formattedNumber } // Log non-sensitive parts }; console.error(""Detailed Error Log:"", JSON.stringify(errorLog)); res.status(infobipStatus).json({ success: false, error: infobipResponse || { message: errorMsg } }); } }); }); // --- Start Server --- // Only start listening if the script is run directly (not required by a test runner) if (require.main === module) { app.listen(port, () => { console.log(`Server listening at http://localhost:${port}`); console.log(`Accepting requests from: ${process.env.CLIENT_URL}`); }); } // Export the app instance for testing purposes module.exports = app;Code Explanation:
form-dataandlibphonenumber-js. Loads.env, creates Express app.cors(configured),express.json/urlencoded,multer(with memory storage warning and file filter)./healthRoute: Simple health check./send-mmsRoute (POST):uploadmiddleware.mediaFile,to,text.libphonenumber-jsto parse and validate thetonumber. Rejects if invalid, otherwise uses the formatted E.164 number.mmsEndpointwith a comment to verify it. Emphasizes checking official docs.form-datalibrary to build themultipart/form-datapayload.head(JSON part) andmedia(file part) according to typical Infobip structure. Crucially reinforces checking Infobip docs for the exact required field names ('head', 'media', 'text' placement).axios.post.Authorizationheader.formData.getHeaders()intoaxiosheaders config to correctly setContent-Type: multipart/form-data; boundary=....try...catchfor logging structured error details (distinguishing between Infobip response errors, network errors, and setup errors).appfor use in tests.3. Building the Frontend (Vite + React)
Create the React component for the form.
1. Create the Form Component (
client/src/App.jsx):Replace the contents of
client/src/App.jsxwith the following:// client/src/App.jsx import React, { useState } from 'react'; import axios from 'axios'; import './App.css'; // We'll create this for basic styling function App() { const [to, setTo] = useState(''); const [text, setText] = useState(''); const [mediaFile, setMediaFile] = useState(null); const [statusMessage, setStatusMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); // Get backend URL from environment variable, with fallback for local dev const backendUrl = import.meta.env.VITE_BACKEND_API_URL || 'http://localhost:5001'; const sendMmsEndpoint = `${backendUrl}/send-mms`; const handleFileChange = (event) => { const file = event.target.files[0]; // Optional: Add basic client-side size check const maxSize = 10 * 1024 * 1024; // 10MB (match backend or slightly lower) if (file && file.size > maxSize) { setStatusMessage(`Error: File size exceeds ${maxSize / 1024 / 1024}MB limit.`); setIsError(true); setMediaFile(null); event.target.value = null; // Clear the input return; } setMediaFile(file); setStatusMessage(''); // Clear status on new file selection setIsError(false); }; const handleSubmit = async (event) => { event.preventDefault(); if (!to || !text || !mediaFile) { setStatusMessage('Please fill in all fields and select a valid media file.'); setIsError(true); return; } // Basic client-side check for E.164 format (improves UX, but backend validation is key) // This regex is basic, libphonenumber-js on backend is more robust. if (!/^\+?[1-9]\d{1,14}$/.test(to)) { setStatusMessage('Please enter the recipient number in E.164 format (e.g., +447123456789).'); setIsError(true); return; } setIsLoading(true); setStatusMessage('Sending MMS...'); setIsError(false); // Use FormData to send multipart data const formData = new FormData(); formData.append('to', to); formData.append('text', text); formData.append('mediaFile', mediaFile); // Key must match Multer config on backend try { const response = await axios.post(sendMmsEndpoint, formData, { headers: { // 'Content-Type': 'multipart/form-data' // Axios usually sets this automatically for FormData }, timeout: 45000, // 45 seconds (allow for backend processing + Infobip call) }); console.log('Backend Response:', response.data); // Extract a meaningful status from the Infobip response if possible const infobipStatus = response.data?.data?.messages?.[0]?.status?.name || 'SUBMITTED'; setStatusMessage(`Successfully sent MMS! Infobip status: ${infobipStatus}`); setIsError(false); // Optionally clear the form // setTo(''); // setText(''); // setMediaFile(null); // event.target.reset(); // Reset file input if needed } catch (error) { console.error('Error sending MMS:', error); let errorMessage = 'Failed to send MMS.'; if (error.response) { // Error from backend API const backendError = error.response.data?.error; errorMessage += ` Server Error (${error.response.status}): ${backendError?.message || JSON.stringify(backendError || error.response.data)}`; } else if (error.request) { // Request made but no response received (network issue, backend down?) errorMessage += ' No response received from the server. Is it running or unreachable?'; } else { // Error setting up the request errorMessage += ` Request Setup Error: ${error.message}`; } setStatusMessage(errorMessage); setIsError(true); } finally { setIsLoading(false); } }; return ( <div className="app-container"> <h1>Send Infobip MMS</h1> <form onSubmit={handleSubmit} className="mms-form"> <div className="form-group"> <label htmlFor="to">Recipient Phone:</label> <input type="tel" id="to" value={to} onChange={(e) => setTo(e.target.value)} placeholder="e.g., +447123456789 (E.164 format)" required disabled={isLoading} /> </div> <div className="form-group"> <label htmlFor="text">Message Text:</label> <textarea id="text" value={text} onChange={(e) => setText(e.target.value)} rows="3" required disabled={isLoading} ></textarea> </div> <div className="form-group"> <label htmlFor="mediaFile">Media File (Image):</label> <input type="file" id="mediaFile" accept="image/jpeg, image/png, image/gif" // Match backend filter onChange={handleFileChange} required // File input required attribute might behave differently across browsers disabled={isLoading} /> {mediaFile && <p className="file-info">Selected: {mediaFile.name} ({(mediaFile.size / 1024 / 1024).toFixed(2)} MB)</p>} </div> <button type="submit" disabled={isLoading || !mediaFile}> {isLoading ? 'Sending...' : 'Send MMS'} </button> </form> {statusMessage && ( <div className={`status-message ${isError ? 'error' : 'success'}`}> {statusMessage} </div> )} <p className="api-url-note">Sending requests to: <code>{sendMmsEndpoint}</code></p> </div> ); } export default App;2. Add Basic Styling (
client/src/App.css):Create a file
client/src/App.css(or update the existing one):/* client/src/App.css */ body { font-family: sans-serif; margin: 0; padding: 20px; background-color: #f4f7f6; color: #333; } .app-container { max-width: 500px; margin: 40px auto; padding: 30px; background-color: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } h1 { text-align: center; color: #007bff; /* Infobip-like blue */ margin-bottom: 30px; } .mms-form .form-group { margin-bottom: 20px; } .mms-form label { display: block; margin-bottom: 8px; font-weight: bold; color: #555; } .mms-form input[type="tel"], .mms-form textarea, .mms-form input[type="file"] { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; /* Include padding in width */ font-size: 1rem; } .mms-form textarea { resize: vertical; /* Allow vertical resize */ min-height: 60px; } .mms-form input[type="file"] { padding: 5px; /* Slightly less padding for file input */ } .file-info { font-size: 0.9em; color: #666; margin-top: 5px; } .mms-form button { display: block; width: 100%; padding: 12px; background-color: #007bff; color: white; border: none; border-radius: 4px; font-size: 1.1rem; cursor: pointer; transition: background-color 0.2s ease; } .mms-form button:hover:not(:disabled) { background-color: #0056b3; } .mms-form button:disabled { background-color: #cccccc; cursor: not-allowed; } .status-message { margin-top: 25px; padding: 15px; border-radius: 4px; text-align: center; font-weight: bold; word-wrap: break-word; /* Prevent long error messages from overflowing */ } .status-message.success { background-color: #e6ffed; color: #28a745; border: 1px solid #b8e6c3; } .status-message.error { background-color: #f8d7da; color: #dc3545; border: 1px solid #f5c6cb; } .api-url-note { margin-top: 20px; font-size: 0.8em; color: #888; text-align: center; } .api-url-note code { background-color: #eee; padding: 2px 4px; border-radius: 3px; }3. Import CSS (
client/src/main.jsx):Ensure the
App.cssfile is imported in your main entry point:// client/src/main.jsx import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.jsx' import './index.css' // Keep default index.css if needed import './App.css' // Import your App specific styles ReactDOM.createRoot(document.getElementById('root')).render( <React.StrictMode> <App /> </React.StrictMode>, )Frontend Code Explanation:
to,text,mediaFile), loading/status messages.VITE_BACKEND_API_URLfrom environment variables (viaimport.meta.env) with a fallback tohttp://localhost:5001for easy local development. A note about setting this variable (inclient/.env) is added in Section 1.handleFileChange: UpdatesmediaFilestate, includes an optional client-side size check for better UX.handleSubmit:FormDataand appends fields.axios.postto send data to the backend endpoint derived from the environment variable.finally.tel,filewithaccept), provides feedback during loading, and displays status messages. Includes a note showing the API endpoint being used.4. Integrating with Infobip (Recap & Credentials)
This section focuses on ensuring the Infobip details are correctly obtained and used.
1. Obtain Credentials:
INFOBIP_API_KEY. Treat it securely.xxxxxx.api.infobip.com). This is yourINFOBIP_BASE_URL.INFOBIP_SENDER_NUMBER. Check Section 8 for more details on sender ID requirements.2. Secure Storage (
server/.env):As configured in Step 1, place these values securely in your
server/.envfile:3. Usage in Code (
server/server.js):The backend code (
server/server.js) loads these values usingrequire('dotenv').config()and accesses them viaprocess.env.Environment Variable Explanation:
INFOBIP_API_KEY: (String) Your secret API key from the Infobip Portal.INFOBIP_BASE_URL: (String) Your account-specific API domain from the Infobip Portal.INFOBIP_SENDER_NUMBER: (String) The E.164 formatted phone number or sender ID provisioned for MMS sending in your Infobip account.PORT: (Number) Network port for the backend server (default:5001).CLIENT_URL: (String) Full URL of the allowed frontend origin for CORS (e.g.,http://localhost:5173for dev, your production frontend URL for deployment).Security: Never hardcode API keys directly in your source code. Always use environment variables loaded from a
.envfile (which is excluded from version control via.gitignore) or secure configuration management systems in production.