code examples

Sent logo
Sent TeamMar 8, 2026 / code examples / Article

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.

Developer Guide: Sending Infobip MMS with Vite (React/Vue) and Node.js

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.

Technologies Used:

  • Frontend: Vite, React (or Vue), Axios (for HTTP requests)
  • 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:

  1. A functional Vite frontend allowing users to specify a recipient, text message, and image.
  2. A secure Node.js backend API endpoint (/send-mms) that handles file uploads, validates input, and communicates with Infobip.
  3. Working integration with the Infobip MMS API to send messages.
  4. 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.

1. Create Project Root Directory:

bash
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/" >> .gitignore

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 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 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
  • jest, supertest: For backend testing (Section 13)

3. Configure Backend Scripts (server/package.json):

Open server/package.json and modify the scripts section. Ensure main points to server.js or your entry file.

json
{
  "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 .env in the server directory. Never commit this file to version control.

dotenv
# 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 directory
cd ..

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 vue

cd 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:

dotenv
# 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.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:

  1. Dependencies & Setup: Includes form-data and libphonenumber-js. Loads .env, creates Express app.
  2. Middleware: cors (configured), express.json/urlencoded, multer (with memory storage warning and file filter).
  3. Infobip Config: Reads env vars, exits if missing.
  4. /health Route: Simple health check.
  5. /send-mms Route (POST):
    • Uses upload middleware.
    • Handles Multer errors.
    • Validates presence of mediaFile, to, text.
    • 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.
  6. 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.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):

css
/* 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.css file is imported in your main entry point:

jsx
// 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:

  1. State: Manages form inputs (to, text, mediaFile), loading/status messages.
  2. 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.
  3. handleFileChange: Updates mediaFile state, includes an optional client-side size check for better UX.
  4. 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.
  5. 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.

1. Obtain Credentials:

  • Log in to your Infobip account: https://portal.infobip.com/
  • 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:

dotenv
# server/.env

# Infobip Credentials
INFOBIP_API_KEY=PASTE_YOUR_API_KEY_HERE
INFOBIP_BASE_URL=PASTE_YOUR_BASE_URL_HERE # e.g., abcde.api.infobip.com
INFOBIP_SENDER_NUMBER=PASTE_YOUR_INFOBIP_SENDER_NUMBER_HERE # e.g., +447123456789

# Server Configuration (already defined)
# PORT=5001
# CLIENT_URL=http://localhost:5173

3. Usage in Code (server/server.js):

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.