Build SMS Marketing Campaigns with Twilio, Node.js, React & Vite [2025]
Step-by-step guide to building SMS marketing campaigns using Twilio API, Node.js Express backend, and React/Vite frontend. Includes A2P 10DLC compliance, E.164 validation, and security best practices.
Build a marketing campaign web application that sends SMS messages using Twilio. You'll create a React frontend with Vite that interacts with a secure Node.js backend using the Twilio API. Learn how to implement E.164 phone validation, handle A2P 10DLC compliance, and secure your SMS integration for production use.
Project Overview and Goals
What You're Building:
A two-part application:
A React frontend (built with Vite) providing a simple form to input a recipient phone number and message body.
A Node.js backend (using Express) with a single API endpoint that receives requests from the frontend and uses the Twilio Node.js helper library to send an SMS message.
Problem You'll Solve:
This setup provides a secure and basic way to integrate SMS functionality into a web application. It ensures that sensitive Twilio credentials (Account SID and Auth Token) never reach the client-side browser, mitigating security risks.
Technologies You'll Use:
Node.js: A JavaScript runtime for building the backend server.
Express: A minimal and flexible Node.js web application framework for creating the API endpoint.
Vite: A modern frontend build tool providing a fast development experience.
React: A popular JavaScript library for building user interfaces.
Twilio: A communication platform as a service (CPaaS) used here for its Programmable Messaging API to send SMS.
dotenv: A Node.js module to load environment variables from a .env file, keeping credentials secure.
cors: Node.js middleware to enable Cross-Origin Resource Sharing, necessary for the frontend (running on a different port) to communicate with the backend API during development.
Node.js and npm (or yarn): Ensure Node.js v18 LTS or later is installed (v20 LTS recommended as of 2025). Verify with node -v and npm -v. Earlier versions may work but are not actively supported.
Twilio Account: Sign up for a free Twilio account at twilio.com.
Twilio Phone Number: Purchase an SMS-enabled phone number from the Twilio Console (Phone Numbers > Buy a Number).
Twilio Credentials: Locate your Account SID (starts with "AC") and Auth Token in the Twilio Console dashboard.
Verified Personal Phone Number (for Trial Accounts): If using a Twilio trial account, verify your personal phone number in the Twilio Console (Phone Numbers > Verified Caller IDs). Trial accounts can only send messages to verified numbers – this security measure prevents abuse.
Final Outcome:
A functional web form where you can enter a phone number and message, click "Send," and have an SMS delivered via Twilio, with basic feedback provided in the UI.
1. Set Up Your Project Structure
Create a main project directory containing separate backend and frontend subdirectories.
1.1. Create the Project Structure:
Open your terminal and run:
Bash
mkdir twilio-sms-app
cd twilio-sms-app
mkdir backend
mkdir frontend
1.2. Set Up the Backend (Node.js/Express):
Navigate into the backend directory and initialize a new Node.js project.
Bash
cd backend
npm init -y
This creates a package.json file. Now install the necessary dependencies: Express for the server, the Twilio helper library, dotenv for environment variables, and cors for handling cross-origin requests during development.
Bash
npm install express twilio dotenv cors
1.3. Create Backend Configuration Files:
.env (Create this file in the backend directory): This file stores your sensitive Twilio credentials. Never commit this file to version control.
Replace the placeholder values with your actual Twilio Account SID, Auth Token, and Twilio Phone Number (in E.164 format).
.gitignore (Create this file in the backend directory): Prevent sensitive files and unnecessary directories from being tracked by Git.
Text
# backend/.gitignore
node_modules
.env
server.js (Create this file in the backend directory): This contains your backend server code. Add the logic in the next section.
JavaScript
// backend/server.js// Basic structure – expand in later stepsrequire('dotenv').config(); // Load environment variables from .env fileconst express = require('express');
const twilio = require('twilio');
const cors = require('cors');
const app = express();
const port = process.env.PORT || 3001; // Use port 3001 for the backend// Middleware
app.use(cors()); // Enable CORS for all origins (adjust for production)
app.use(express.json()); // Parse JSON request bodies// API Endpoint placeholder
app.post('/api/send-sms', (req, res) => {
// Logic will go here
res.send('SMS endpoint hit');
});
// Start the server
app.listen(port, () => {
console.log(`Backend server listening on port ${port}`);
});
1.4. Set Up the Frontend (Vite/React):
Navigate back to the root project directory (twilio-sms-app) and then into the frontend directory. Use Vite to scaffold a new React project.
Bash
cd .. # Ensure you are in the root twilio-sms-app directorycd frontend
npm create vite@latest . -- --template react
(Choose react and javascript or typescript if prompted, though this guide uses JavaScript)
This command scaffolds a React project in the current (frontend) directory. Now install the dependencies:
Bash
npm install
1.5. Create Frontend Configuration Files:
.env (Create this file in the frontend directory): This file stores frontend-specific environment variables, like the backend API URL.
This sets up your React application structure within the frontend directory.
2. Build the Backend API
Build the backend API endpoint in backend/server.js that sends SMS using Twilio.
2.1. Update server.js:
Modify the backend/server.js file to include the Twilio logic within the /api/send-sms route handler.
JavaScript
// backend/server.jsrequire('dotenv').config();
const express = require('express');
const twilio = require('twilio');
const cors = require('cors');
const app = express();
const port = process.env.PORT || 3001;
// --- Environment Variable Validation ---// Essential check: ensure credentials are loadedif (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN || !process.env.TWILIO_PHONE_NUMBER) {
console.error('Error: Missing Twilio credentials in backend/.env file.');
console.error('Ensure TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER are set.');
process.exit(1); // Exit if credentials aren't found
}
// --- Twilio Client Initialization ---// Creates the client instance needed to interact with the Twilio API.// Uses credentials securely loaded from environment variables.const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const client = twilio(accountSid, authToken);
const twilioPhoneNumber = process.env.TWILIO_PHONE_NUMBER;
// --- Middleware ---// cors: Allows the frontend (running on a different port/origin) to make requests to this backend API.// Important: For production, configure CORS more restrictively (e.g., allow only your frontend domain).
app.use(cors());
// express.json: Parses incoming requests with JSON payloads (like the one the frontend sends).
app.use(express.json());
// --- Logging Middleware (Optional but Recommended) ---// Provides visibility into incoming requests for debugging.
app.use((req, res, next) => {
console.log(`${newDate().toISOString()} – ${req.method}${req.path}`);
next(); // Pass control to the next middleware/route handler
});
// --- API Endpoint: /api/send-sms ---
app.post('/api/send-sms', async (req, res) => {
// --- Input Validation (Basic) ---// Prevents errors and potential misuse by ensuring required data is present.// Production apps need more robust validation (e.g., phone number format).const { to, body } = req.body;
if (!to || !body) {
console.error('Validation Error: Missing "to" or "body" in request.');
return res.status(400).json({ success: false, message: 'Missing required fields: "to" and "body".' });
}
// --- Twilio API Call ---try {
// async/await: Handles the asynchronous nature of the API call cleanly.console.log(`Attempting to send SMS to: ${to}`);
const message = await client.messages.create({
body: body,
from: twilioPhoneNumber, // Your Twilio phone number from .envto: to // Recipient's phone number from the request (must be E.164 format)
});
console.log(`SMS sent successfully! SID: ${message.sid}`);
// Return success: Sends a confirmation back to the frontend.
res.status(200).json({ success: true, message: `SMS sent successfully! SID: ${message.sid}` });
} catch (error) {
// --- Error Handling ---// Catches errors from the Twilio API (e.g., invalid number, network issue)// and provides informative feedback to both the console and the frontend.console.error('Twilio API Error:', error.message);
console.error('Error Code:', error.code);
// Determine appropriate status code based on Twilio error if possible// Example: Twilio error codes often map to HTTP status codes (e.g., 21211 invalid 'To' number → 400)const statusCode = error.status || 500; // Use Twilio status or default to 500
res.status(statusCode).json({
success: false,
message: `Failed to send SMS: ${error.message}`,
errorCode: error.code// Include Twilio error code in response
});
}
});
// --- Basic Root Route (Optional) ---
app.get('/', (req, res) => {
res.send('Twilio SMS Backend is running!');
});
// --- Start Server ---
app.listen(port, () => {
console.log(`Backend server listening on port ${port}`);
console.log('Ensure your backend/.env file is configured correctly.');
});
Explanation:
Environment Variables:require('dotenv').config() loads variables from backend/.env. A crucial check ensures the required Twilio variables exist before proceeding.
Twilio Client:twilio(accountSid, authToken) initializes the client needed to interact with the Twilio API.
Middleware:cors() enables requests from the frontend. express.json() parses the incoming request body. A simple logging middleware provides visibility.
API Endpoint (/api/send-sms):
It's an async function because the Twilio API call is asynchronous.
It extracts to (recipient number) and body (message text) from the request's JSON payload (req.body).
Basic Validation: Checks if to and body exist. Returns a 400 Bad Request if not. Note: Production apps require stricter validation (e.g., using a library like joi or checking E.164 format).
Twilio Call:client.messages.create() sends the SMS. It requires:
body: The message content.
from: Your Twilio phone number (loaded from .env).
to: The recipient's number (from the request). Must be in E.164 format (e.g., +1xxxxxxxxxx).
Success Response: If the API call succeeds, logs the message SID and sends a 200 OK JSON response to the frontend.
Error Handling: A try...catch block handles potential errors during the Twilio API call (invalid credentials, invalid 'to' number, network issues, etc.). It logs the error details and sends an appropriate error status code (using error.status if available, otherwise 500) and JSON message back to the frontend.
3. Build the Frontend UI
Create the React form in the frontend to interact with your backend API.
3.1. Update App.jsx:
Replace the contents of frontend/src/App.jsx with the following code:
JSX
// frontend/src/App.jsximportReact, { useState } from'react';
import'./App.css'; // You might want to add some basic stylingfunctionApp() {
// --- State Variables ---// Store the user's input for the phone number and message body,// and track the sending status/feedback.const [recipient, setRecipient] = useState('');
const [messageBody, setMessageBody] = useState('');
const [statusMessage, setStatusMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
// --- Backend API URL ---// Reads the backend URL from environment variables (Vite).// Provides a fallback for local development if .env is missing.constAPI_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api/send-sms';
// --- Handle Form Submission ---consthandleSubmit = async (event) => {
event.preventDefault(); // Prevent default form submission (page reload)setIsLoading(true); // Indicate loading statesetStatusMessage(''); // Clear previous statussetIsError(false);
// --- Basic Frontend Validation ---// Provide immediate feedback without waiting for the backend.if (!recipient || !messageBody) {
setStatusMessage('Enter both a recipient number and a message.');
setIsError(true);
setIsLoading(false);
return; // Stop submission
}
// Enhanced E.164 validation (ITU-T E.164 standard)// Validates: '+' followed by 1-15 digits (country code + national number)// Reference: https://www.itu.int/rec/T-REC-E.164/en// For production, use libphonenumber-js for complete validation including regional rulesif (!/^\+[1-9]\d{1,14}$/.test(recipient)) {
setStatusMessage('Recipient number must be in E.164 format: + followed by country code and number (1-15 digits total). Example: +15551234567');
setIsError(true);
setIsLoading(false);
return;
}
try {
// --- Make API Request ---// Sends the form data to the backend endpoint using fetch.const response = awaitfetch(API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json', // Indicate we're sending JSON
},
body: JSON.stringify({ to: recipient, body: messageBody }), // Send data as JSON string
});
// --- Handle API Response ---const data = await response.json(); // Parse the JSON response from the backendif (response.ok && data.success) {
// response.ok: Checks if the HTTP status code indicates success (200-299).// data.success: Checks the custom 'success' field in the backend's JSON response.setStatusMessage(`Success: ${data.message}`);
setIsError(false);
// Optionally clear form on success// setRecipient('');// setMessageBody('');
} else {
// Handle backend errors (e.g., validation, Twilio API errors)setStatusMessage(`Error: ${data.message || 'Failed to send SMS.'}`);
setIsError(true);
}
} catch (error) {
// Handle network errors or issues reaching the backendconsole.error('Frontend Error:', error);
setStatusMessage('Network error or backend is unreachable. Try again.');
setIsError(true);
} finally {
// finally: Ensures loading state turns off regardless of success or failure.setIsLoading(false);
}
};
// --- Render UI ---return (
<divclassName="App"><h1>Send SMS via Twilio</h1><formonSubmit={handleSubmit}><div><labelhtmlFor="recipient">Recipient Phone Number:</label><inputtype="tel" // Useteltypeforphonenumbersid="recipient"value={recipient}onChange={(e) => setRecipient(e.target.value)}
placeholder="+15551234567" // Example E.164 format
required // Basic HTML5 validation
disabled={isLoading} // Disable input during sending
/>
</div><div><labelhtmlFor="messageBody">Message:</label><textareaid="messageBody"value={messageBody}onChange={(e) => setMessageBody(e.target.value)}
required
disabled={isLoading} // Disable textarea during sending
/>
</div><buttontype="submit"disabled={isLoading}>
{isLoading ? 'Sending…' : 'Send SMS'}
</button></form>
{/* Display Status/Feedback Message */}
{statusMessage && (
<pclassName={`status-message ${isError ? 'error' : 'success'}`}>
{statusMessage}
</p>
)}
</div>
);
}
exportdefaultApp;
3.2. Basic Styling (Optional):
Add some basic styles to frontend/src/App.css for better presentation:
State:useState hooks manage the recipient number, message body, loading state, and status message displayed to the user.
API URL: Reads the backend endpoint URL from import.meta.env.VITE_API_URL (defined in frontend/.env), with a fallback to http://localhost:3001/api/send-sms.
handleSubmit:
Prevents the default form submission behavior (page reload).
Sets loading state and clears previous statuses.
Performs basic frontend validation (checks for empty fields and simple E.164 format). Note added about regex limitations.
Uses fetch to make a POST request to the backend API.
Sends the recipient and messageBody in the request body as a JSON string.
Sets the Content-Type header to application/json.
Response Handling: Parses the JSON response from the backend. Updates the statusMessage based on whether the request was successful (response.ok and data.success) or resulted in an error.
Error Handling: A catch block handles network errors (e.g., backend server not running).
finally: Resets the loading state regardless of the outcome.
JSX Structure: Renders a standard HTML form with input fields bound to the state variables and an onSubmit handler linked to handleSubmit. It disables inputs and the button during the loading state. A conditional paragraph (<p>) displays the statusMessage.
4. Configure Your Twilio Credentials
Obtain and securely manage your Twilio credentials.
Crucially: Add .env to your backend/.gitignore file. Never commit your .env file or hardcode credentials directly in your source code (server.js or frontend files).
Similarly, ensure frontend/.env (containing VITE_API_URL) is in your frontend/.gitignore.
4.3. Understand Environment Variables:
TWILIO_ACCOUNT_SID (Backend): Your unique account identifier.
TWILIO_AUTH_TOKEN (Backend): Your secret key for authenticating API requests. Treat it like a password.
TWILIO_PHONE_NUMBER (Backend): The E.164 formatted Twilio number used to send SMS messages.
VITE_API_URL (Frontend): The URL of your backend API endpoint.
5. Run and Test Your Application
Run both the backend and frontend servers to test the application. Ensure you run commands from the correct directories.
5.1. Start the Backend Server:
Open a terminal window, navigate to the project's root directory (twilio-sms-app), then into the backend directory, and run:
Bash
cd backend # Make sure you are in the twilio-sms-app/backend directory
node server.js
You should see output like:
Code
Backend server listening on port 3001
Ensure your backend/.env file is configured correctly.
If you installed nodemon (optional, npm install -D nodemon), run nodemon server.js for automatic restarts on file changes.
5.2. Start the Frontend Development Server:
Open a second terminal window, navigate to the project's root directory (twilio-sms-app), then into the frontend directory, and run:
Bash
cd frontend # Make sure you are in the twilio-sms-app/frontend directory
npm run dev
Vite starts the development server, typically on port 5173 (or the next available port), and provides a local URL:
Code
VITE vX.Y.Z ready in XXXms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
5.3. Verify Manually:
Open the local URL (e.g., http://localhost:5173/) provided by Vite in your web browser.
You should see the "Send SMS via Twilio" form.
Enter a valid recipient phone number in E.164 format (e.g., +15559876543).
Trial Account: Use the personal phone number you verified in the Twilio Console. Sending to any other number will fail.
Upgraded Account: Send to other numbers, but be mindful of regulations (like A2P 10DLC in the US – see Compliance section below).
Enter a message in the "Message" field.
Click the "Send SMS" button.
Observe the status message below the form. It should indicate "Sending…", then either "Success: SMS sent successfully! SID: SMxxxxxxxx…" or an error message.
Check the recipient's phone – the SMS should arrive shortly. (Trial messages include the prefix "Sent from a Twilio trial account.")
Check the terminal running the backend server – you should see logs for the incoming request and the Twilio API call attempt (success or error).
5.4. Test the API Directly (Optional – curl / Postman):
Test the backend API endpoint directly.
Using curl: Open a terminal and run:
Bash
curl -X POST http://localhost:3001/api/send-sms \
-H "Content-Type: application/json" \
-d '{"to": "+15559876543", "body": "Test message from curl"}'
Replace +15559876543 with a valid recipient number (your verified number if on a trial account). You should get a JSON response like:
{"success":true,"message":"SMS sent successfully! SID: SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} or an error JSON.
Using Postman: Create a new POST request to http://localhost:3001/api/send-sms. Set the Body type to raw and format to JSON. Enter the JSON payload: {"to": "+15559876543", "body": "Test from Postman"}. Send the request and observe the response.
6. Handle Errors and Understand Caveats
While the code includes basic error handling, here are common issues and considerations:
Backend (server.js):
Missing Credentials: The initial check catches if .env variables aren't loaded. Ensure the backend/.env file exists in the backend directory, has the correct name, and contains the right variables.
Twilio API Errors (Caught in try...catch):
Error Code 21211: Invalid 'To' Phone Number: The recipient number is likely missing, invalid, not E.164 formatted, or (if on a trial account) not your verified number. The backend returns a 400 status. Ensure the number starts with + and the country code. Reference: https://www.twilio.com/docs/api/errors/21211
Error Code 21408: Permission to send SMS has not been enabled: Your Twilio number doesn't have SMS capabilities. Purchase a different number with SMS enabled.
Error Code 21608: The 'To' phone number is not currently reachable: The number might be out of service, blocked, or on a carrier that doesn't accept your traffic.
Error Code 21212: Invalid 'From' Phone Number: The TWILIO_PHONE_NUMBER in backend/.env is incorrect, not SMS-enabled, or not owned by your account. Verify in Console > Phone Numbers > Manage > Active Numbers.
Error Code 20003: Authentication Error: Your Account SID or Auth Token in backend/.env is incorrect. Double-check them in the Twilio Console. Auth Token is case-sensitive.
Error Code 21610: Attempt to send to unsubscribed recipient: The recipient previously replied "STOP" to your Twilio number. They must reply "START" to re-subscribe.
Error Code 30007: Message filtered (carrier violation): The message content was filtered by the carrier. Ensure compliance with CTIA guidelines and avoid prohibited content (phishing, illegal content, etc.).
Trial Account Restriction: Sending to unverified numbers fails with Error 21211. You can only send SMS to phone numbers verified in your Twilio Console (Console > Phone Numbers > Manage > Verified Caller IDs).
Logging: The backend logs errors to the console. For production, use a dedicated logging library (like Winston or Pino) to structure logs and send them to a logging service.
Frontend (App.jsx):
Network Error: If the backend server isn't running or reachable, the fetch call will fail (caught in the catch block). Ensure the backend is running and the VITE_API_URL in frontend/.env (or the fallback in App.jsx) is correct.
CORS Errors: If the cors() middleware is missing or misconfigured on the backend, the browser will block the fetch request. Check the browser's developer console (Network tab) for CORS errors. The provided cors() setup is generally permissive for development; restrict it in production (see Security).
Incorrect E.164 Format: The basic frontend check catches some errors, but ensure the number format is correct before submitting. Use a more robust validation method for production apps.
Backend Error Display: The frontend displays the message field from the backend's JSON error response.
General Caveats:
E.164 Format: Always use + followed by the country code and number (e.g., +1 for North America, +44 for UK). The ITU-T E.164 standard specifies international telephone numbers must not exceed 15 digits (including country code). Reference: https://www.itu.int/rec/T-REC-E.164/en
Rate Limits: Twilio imposes rate limits on sending messages. Default limits: 1 message per second per phone number for new accounts, up to 100 concurrent requests per account. Sending too rapidly results in 429 Too Many Requests errors. For high volume, use Twilio Messaging Services with verified sender profiles. Reference: https://www.twilio.com/docs/usage/webhooks/webhooks-connection-overrides#connection-overrides
Compliance (A2P 10DLC – US & Canada): Sending Application-to-Person (A2P) SMS messages from standard 10-digit long code phone numbers to users in the US requires A2P 10DLC registration (effective September 1, 2022). Unregistered A2P traffic faces heavy carrier filtering and delivery failures. Registration involves:
Register your business/brand with The Campaign Registry (TCR)
Register your messaging use case (campaign)
Complete carrier vetting (typically 1-2 weeks)
US carriers (AT&T, T-Mobile, Verizon) enforce this requirement, not Twilio. Trial accounts are exempt from A2P 10DLC registration but have sending restrictions. For production US messaging, budget time for registration before launch. Reference: https://www.twilio.com/docs/sms/a2p-10dlc
Trial Account Limitations: Messages sent from trial accounts are prefixed with "Sent from a Twilio trial account" and can only be successfully sent to phone numbers verified in your Twilio Console. Trial accounts receive $15.50 USD in credit (as of 2025).
7. Secure Your Application
Security is paramount, especially when dealing with API keys.
Credential Security:Never expose TWILIO_ACCOUNT_SID or TWILIO_AUTH_TOKEN in frontend code or commit them to version control. Use environment variables on the backend (backend/.env locally, platform-specific environment variables in deployment). Treat Auth Token like a password – rotate it immediately if compromised.
Input Validation (Backend): The current validation is minimal. Implement robust validation on the backend:
Phone Number Validation: Use libphonenumber-js library (npm package libphonenumber-js) to strictly validate E.164 format and check number validity for specific regions:
Message Body Validation: Check length (SMS segments: 160 GSM-7 characters or 70 UCS-2 characters per segment), sanitize content if storing/displaying, and validate against prohibited content patterns.
Use validation libraries like joi or express-validator for structured validation.
Rate Limiting (Application Level): Beyond Twilio's limits, add rate limiting to your /api/send-sms endpoint using express-rate-limit:
JavaScript
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutesmax: 10, // Limit each IP to 10 requests per windowMsmessage: 'Too many SMS requests from this IP, try again later.'
});
app.post('/api/send-sms', limiter, async (req, res) => { /* ... */ });
This prevents abuse and controls costs.
CORS Configuration (Production): In server.js, app.use(cors()); allows any origin. For production, restrict this to only your deployed frontend's domain:
JavaScript
// Example: Restrictive CORS for production in backend/server.jsconst allowedOrigins = ['https://your-frontend-domain.com']; // Replace with your frontend URL(s)const corsOptions = {
origin: function (origin, callback) {
// Allow requests with no origin (like mobile apps or curl requests)if (!origin) returncallback(null, true);
if (allowedOrigins.indexOf(origin) === -1) {
const msg = 'The CORS policy for this site does not allow access from the specified Origin.';
returncallback(newError(msg), false);
}
returncallback(null, true);
}
};
app.use(cors(corsOptions));
Authentication/Authorization: This example has no user authentication. In a real application, ensure only logged-in/authorized users can trigger SMS messages, possibly with further restrictions based on roles or permissions.
Conclusion
You've built a basic application for sending SMS messages using Node.js, Vite, React, and Twilio. Key takeaways include separating frontend and backend concerns, securely managing API credentials using environment variables, handling asynchronous API calls, and implementing basic error handling. Address security, validation, and compliance requirements (like A2P 10DLC) before deploying to production.
Frequently Asked Questions
How do I send SMS messages with Twilio and React?
Send SMS messages by creating a Node.js Express backend that handles Twilio API calls, keeping credentials secure on the server. Build a React frontend form that sends recipient numbers and messages to your backend API endpoint. Use fetch() or axios to POST data to /api/send-sms, which calls client.messages.create() with Twilio credentials stored in environment variables. Never expose Account SID or Auth Token in frontend code.
What is A2P 10DLC and do I need it for SMS campaigns?
A2P 10DLC (Application-to-Person 10-Digit Long Code) is a US carrier requirement for businesses sending SMS from standard phone numbers, effective September 1, 2022. Register your business with The Campaign Registry (TCR), register your messaging campaign, and complete carrier vetting (1-2 weeks). Unregistered traffic faces heavy filtering and delivery failures. Trial Twilio accounts are exempt but have restricted sending capabilities.
How do I validate phone numbers in E.164 format?
Validate E.164 format using the regex /^\+[1-9]\d{1,14}$/ which checks for: + prefix, country code starting with 1-9, and 1-15 total digits (including country code). For production, use the libphonenumber-js library which validates region-specific rules, checks number types (mobile vs landline), and handles international formatting. E.164 is the ITU-T international telephone numbering standard required by Twilio API.
What Twilio error codes should I handle in my SMS app?
Handle these critical Twilio error codes: 21211 (invalid 'To' number or unverified on trial), 21212 (invalid 'From' number), 20003 (authentication failed), 21408 (SMS capability not enabled), 21610 (unsubscribed recipient who texted "STOP"), 30007 (carrier-filtered content), and 21608 (unreachable number). Each error returns specific HTTP status codes – catch them in try-catch blocks and provide user-friendly messages.
How do I secure Twilio credentials in Node.js?
Secure Twilio credentials by storing Account SID and Auth Token in a .env file (never commit to Git), loading them with dotenv on the backend only, and never exposing them in frontend code or API responses. Use environment variables in production deployment platforms. Rotate your Auth Token immediately if compromised. Add rate limiting with express-rate-limit to prevent abuse and implement CORS restrictions for production environments.
Can I send SMS to international numbers with Twilio?
Yes, send international SMS by purchasing a Twilio phone number with international SMS capabilities and formatting recipient numbers in E.164 format (e.g., +44 for UK, +61 for Australia). Check Twilio's geographic permissions in your account settings and enable specific countries. Pricing varies by destination country. Some countries have additional registration requirements – consult Twilio's country-specific SMS guidelines before launching international campaigns.
What are SMS character limits and message segmentation?
SMS messages are limited to 160 GSM-7 characters or 70 UCS-2 (Unicode) characters per segment. Messages exceeding these limits split into multiple segments, each charged separately. Emojis and special characters trigger UCS-2 encoding, reducing the limit to 70 characters. Twilio automatically handles segmentation and reassembly. Use message length validation in your code to warn users and prevent unexpected costs for multi-segment messages.
How do I handle STOP/START opt-out messages with Twilio?
Twilio automatically processes STOP, UNSTOP, START, and HELP keywords sent by recipients. When someone texts STOP to your number, Twilio blocks future messages (Error 21610 if you try sending). Recipients must text START to re-subscribe. Access opt-out status via Twilio Console or API. For compliance with TCPA and carrier guidelines, always respect opt-outs, include clear opt-out instructions in initial messages, and never override Twilio's automatic handling.
Frequently asked questions
Use the Twilio Node.js helper library in your backend API. After initializing the Twilio client with your credentials, call client.messages.create() with the message body, your Twilio phone number, and the recipient's number in E.164 format (+1XXXXXXXXXX). Ensure your credentials are stored securely in a .env file on the server-side, never exposed to the frontend.
Vite is a modern frontend build tool used to improve the development experience. It offers faster startup times, hot module reloading, and optimized builds, making the process of building and testing the React frontend significantly quicker and more efficient.
A separate backend is essential for security. It prevents your Twilio Account SID and Auth Token from being exposed to the client-side browser. The backend handles the Twilio integration and keeps your sensitive credentials protected.
Register for A2P 10DLC before sending application-to-person (A2P) SMS messages from a standard number to users in the US or Canada. It's a regulatory requirement. Trial accounts have limitations and are not subject to the same rules, but messages are marked as from a trial account.
While this project provides a basic foundation, enhance it before production deployment. Strengthen input validation, implement robust error handling, and configure more restrictive CORS settings. Consider application-level rate limiting and add authentication/authorization if needed.
Create a main project directory (e.g., twilio-sms-app), with 'backend' and 'frontend' subdirectories. The backend will house the Node.js server (server.js, .env, .gitignore), while the frontend contains the React app (App.jsx, .env, .gitignore).
The .env file stores environment variables, such as your Twilio credentials (Account SID, Auth Token, Twilio Phone Number) in the backend, and the backend API URL in the frontend. These values are loaded into the application at runtime but should never be committed to version control.
Twilio trial accounts are restricted to sending SMS only to the phone number(s) you've verified in your Twilio Console. Sending to any other numbers will fail. This is a security measure and part of the trial account limitations.
The provided backend code includes a try...catch block to handle Twilio API errors. It logs error details to the console and returns a JSON response with 'success: false' and an appropriate error message and code to the frontend for display.
CORS (Cross-Origin Resource Sharing) is a security mechanism that allows or restricts web pages from one origin (domain, protocol, and port) to access resources from a different origin. It's enabled by the 'cors' middleware in the backend to allow communication between the frontend (running on a different port during development) and the backend API.
The frontend performs a basic E.164 check (starts with '+', followed by digits). However, this basic check allows invalid numbers. A more robust method for production is using a dedicated phone number validation library, such as libphonenumber-js on both frontend and backend, and stricter E.164 checks in the backend.
The backend server uses port 3001 by default, as specified in server.js. You can change it if necessary, and also update the corresponding VITE_API_URL in your frontend/.env file.
The application consists of a React frontend (using Vite) that sends data to a Node.js/Express backend API. The backend interacts with the Twilio API to send SMS messages. Sensitive credentials are stored in .env files and loaded via environment variables.
Build a marketing campaign web application that sends SMS messages using Twilio. You'll create a React frontend with Vite that interacts with a secure Node.js backend using the Twilio API. Learn how to implement E.164 phone validation, handle A2P 10DLC compliance, and secure your SMS integration for production use.
Project Overview and Goals
What You're Building:
A two-part application:
Problem You'll Solve:
This setup provides a secure and basic way to integrate SMS functionality into a web application. It ensures that sensitive Twilio credentials (Account SID and Auth Token) never reach the client-side browser, mitigating security risks.
Technologies You'll Use:
dotenv: A Node.js module to load environment variables from a.envfile, keeping credentials secure.cors: Node.js middleware to enable Cross-Origin Resource Sharing, necessary for the frontend (running on a different port) to communicate with the backend API during development.System Architecture:
Prerequisites:
node -vandnpm -v. Earlier versions may work but are not actively supported.Final Outcome:
A functional web form where you can enter a phone number and message, click "Send," and have an SMS delivered via Twilio, with basic feedback provided in the UI.
1. Set Up Your Project Structure
Create a main project directory containing separate
backendandfrontendsubdirectories.1.1. Create the Project Structure:
Open your terminal and run:
mkdir twilio-sms-app cd twilio-sms-app mkdir backend mkdir frontend1.2. Set Up the Backend (Node.js/Express):
Navigate into the backend directory and initialize a new Node.js project.
cd backend npm init -yThis creates a
package.jsonfile. Now install the necessary dependencies: Express for the server, the Twilio helper library,dotenvfor environment variables, andcorsfor handling cross-origin requests during development.1.3. Create Backend Configuration Files:
.env(Create this file in thebackenddirectory): This file stores your sensitive Twilio credentials. Never commit this file to version control.Replace the placeholder values with your actual Twilio Account SID, Auth Token, and Twilio Phone Number (in E.164 format).
.gitignore(Create this file in thebackenddirectory): Prevent sensitive files and unnecessary directories from being tracked by Git.server.js(Create this file in thebackenddirectory): This contains your backend server code. Add the logic in the next section.// backend/server.js // Basic structure – expand in later steps require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const twilio = require('twilio'); const cors = require('cors'); const app = express(); const port = process.env.PORT || 3001; // Use port 3001 for the backend // Middleware app.use(cors()); // Enable CORS for all origins (adjust for production) app.use(express.json()); // Parse JSON request bodies // API Endpoint placeholder app.post('/api/send-sms', (req, res) => { // Logic will go here res.send('SMS endpoint hit'); }); // Start the server app.listen(port, () => { console.log(`Backend server listening on port ${port}`); });1.4. Set Up the Frontend (Vite/React):
Navigate back to the root project directory (
twilio-sms-app) and then into the frontend directory. Use Vite to scaffold a new React project.cd .. # Ensure you are in the root twilio-sms-app directory cd frontend npm create vite@latest . -- --template react(Choose
reactandjavascriptortypescriptif prompted, though this guide uses JavaScript)This command scaffolds a React project in the current (
frontend) directory. Now install the dependencies:1.5. Create Frontend Configuration Files:
.env(Create this file in thefrontenddirectory): This file stores frontend-specific environment variables, like the backend API URL.Vite automatically loads variables prefixed with
VITE_from this file..gitignore(Create or update this file in thefrontenddirectory): Ensure the frontend.envfile is also ignored by Git.(Note: Vite's default
.gitignoremight already cover.env*. Ensure it's present.)This sets up your React application structure within the
frontenddirectory.2. Build the Backend API
Build the backend API endpoint in
backend/server.jsthat sends SMS using Twilio.2.1. Update
server.js:Modify the
backend/server.jsfile to include the Twilio logic within the/api/send-smsroute handler.// backend/server.js require('dotenv').config(); const express = require('express'); const twilio = require('twilio'); const cors = require('cors'); const app = express(); const port = process.env.PORT || 3001; // --- Environment Variable Validation --- // Essential check: ensure credentials are loaded if (!process.env.TWILIO_ACCOUNT_SID || !process.env.TWILIO_AUTH_TOKEN || !process.env.TWILIO_PHONE_NUMBER) { console.error('Error: Missing Twilio credentials in backend/.env file.'); console.error('Ensure TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER are set.'); process.exit(1); // Exit if credentials aren't found } // --- Twilio Client Initialization --- // Creates the client instance needed to interact with the Twilio API. // Uses credentials securely loaded from environment variables. const accountSid = process.env.TWILIO_ACCOUNT_SID; const authToken = process.env.TWILIO_AUTH_TOKEN; const client = twilio(accountSid, authToken); const twilioPhoneNumber = process.env.TWILIO_PHONE_NUMBER; // --- Middleware --- // cors: Allows the frontend (running on a different port/origin) to make requests to this backend API. // Important: For production, configure CORS more restrictively (e.g., allow only your frontend domain). app.use(cors()); // express.json: Parses incoming requests with JSON payloads (like the one the frontend sends). app.use(express.json()); // --- Logging Middleware (Optional but Recommended) --- // Provides visibility into incoming requests for debugging. app.use((req, res, next) => { console.log(`${new Date().toISOString()} – ${req.method} ${req.path}`); next(); // Pass control to the next middleware/route handler }); // --- API Endpoint: /api/send-sms --- app.post('/api/send-sms', async (req, res) => { // --- Input Validation (Basic) --- // Prevents errors and potential misuse by ensuring required data is present. // Production apps need more robust validation (e.g., phone number format). const { to, body } = req.body; if (!to || !body) { console.error('Validation Error: Missing "to" or "body" in request.'); return res.status(400).json({ success: false, message: 'Missing required fields: "to" and "body".' }); } // --- Twilio API Call --- try { // async/await: Handles the asynchronous nature of the API call cleanly. console.log(`Attempting to send SMS to: ${to}`); const message = await client.messages.create({ body: body, from: twilioPhoneNumber, // Your Twilio phone number from .env to: to // Recipient's phone number from the request (must be E.164 format) }); console.log(`SMS sent successfully! SID: ${message.sid}`); // Return success: Sends a confirmation back to the frontend. res.status(200).json({ success: true, message: `SMS sent successfully! SID: ${message.sid}` }); } catch (error) { // --- Error Handling --- // Catches errors from the Twilio API (e.g., invalid number, network issue) // and provides informative feedback to both the console and the frontend. console.error('Twilio API Error:', error.message); console.error('Error Code:', error.code); // Determine appropriate status code based on Twilio error if possible // Example: Twilio error codes often map to HTTP status codes (e.g., 21211 invalid 'To' number → 400) const statusCode = error.status || 500; // Use Twilio status or default to 500 res.status(statusCode).json({ success: false, message: `Failed to send SMS: ${error.message}`, errorCode: error.code // Include Twilio error code in response }); } }); // --- Basic Root Route (Optional) --- app.get('/', (req, res) => { res.send('Twilio SMS Backend is running!'); }); // --- Start Server --- app.listen(port, () => { console.log(`Backend server listening on port ${port}`); console.log('Ensure your backend/.env file is configured correctly.'); });Explanation:
require('dotenv').config()loads variables frombackend/.env. A crucial check ensures the required Twilio variables exist before proceeding.twilio(accountSid, authToken)initializes the client needed to interact with the Twilio API.cors()enables requests from the frontend.express.json()parses the incoming request body. A simple logging middleware provides visibility./api/send-sms):asyncfunction because the Twilio API call is asynchronous.to(recipient number) andbody(message text) from the request's JSON payload (req.body).toandbodyexist. Returns a400 Bad Requestif not. Note: Production apps require stricter validation (e.g., using a library likejoior checking E.164 format).client.messages.create()sends the SMS. It requires:body: The message content.from: Your Twilio phone number (loaded from.env).to: The recipient's number (from the request). Must be in E.164 format (e.g.,+1xxxxxxxxxx).200 OKJSON response to the frontend.try...catchblock handles potential errors during the Twilio API call (invalid credentials, invalid 'to' number, network issues, etc.). It logs the error details and sends an appropriate error status code (usingerror.statusif available, otherwise500) and JSON message back to the frontend.3. Build the Frontend UI
Create the React form in the frontend to interact with your backend API.
3.1. Update
App.jsx:Replace the contents of
frontend/src/App.jsxwith the following code:// frontend/src/App.jsx import React, { useState } from 'react'; import './App.css'; // You might want to add some basic styling function App() { // --- State Variables --- // Store the user's input for the phone number and message body, // and track the sending status/feedback. const [recipient, setRecipient] = useState(''); const [messageBody, setMessageBody] = useState(''); const [statusMessage, setStatusMessage] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isError, setIsError] = useState(false); // --- Backend API URL --- // Reads the backend URL from environment variables (Vite). // Provides a fallback for local development if .env is missing. const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api/send-sms'; // --- Handle Form Submission --- const handleSubmit = async (event) => { event.preventDefault(); // Prevent default form submission (page reload) setIsLoading(true); // Indicate loading state setStatusMessage(''); // Clear previous status setIsError(false); // --- Basic Frontend Validation --- // Provide immediate feedback without waiting for the backend. if (!recipient || !messageBody) { setStatusMessage('Enter both a recipient number and a message.'); setIsError(true); setIsLoading(false); return; // Stop submission } // Enhanced E.164 validation (ITU-T E.164 standard) // Validates: '+' followed by 1-15 digits (country code + national number) // Reference: https://www.itu.int/rec/T-REC-E.164/en // For production, use libphonenumber-js for complete validation including regional rules if (!/^\+[1-9]\d{1,14}$/.test(recipient)) { setStatusMessage('Recipient number must be in E.164 format: + followed by country code and number (1-15 digits total). Example: +15551234567'); setIsError(true); setIsLoading(false); return; } try { // --- Make API Request --- // Sends the form data to the backend endpoint using fetch. const response = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', // Indicate we're sending JSON }, body: JSON.stringify({ to: recipient, body: messageBody }), // Send data as JSON string }); // --- Handle API Response --- const data = await response.json(); // Parse the JSON response from the backend if (response.ok && data.success) { // response.ok: Checks if the HTTP status code indicates success (200-299). // data.success: Checks the custom 'success' field in the backend's JSON response. setStatusMessage(`Success: ${data.message}`); setIsError(false); // Optionally clear form on success // setRecipient(''); // setMessageBody(''); } else { // Handle backend errors (e.g., validation, Twilio API errors) setStatusMessage(`Error: ${data.message || 'Failed to send SMS.'}`); setIsError(true); } } catch (error) { // Handle network errors or issues reaching the backend console.error('Frontend Error:', error); setStatusMessage('Network error or backend is unreachable. Try again.'); setIsError(true); } finally { // finally: Ensures loading state turns off regardless of success or failure. setIsLoading(false); } }; // --- Render UI --- return ( <div className="App"> <h1>Send SMS via Twilio</h1> <form onSubmit={handleSubmit}> <div> <label htmlFor="recipient">Recipient Phone Number:</label> <input type="tel" // Use tel type for phone numbers id="recipient" value={recipient} onChange={(e) => setRecipient(e.target.value)} placeholder="+15551234567" // Example E.164 format required // Basic HTML5 validation disabled={isLoading} // Disable input during sending /> </div> <div> <label htmlFor="messageBody">Message:</label> <textarea id="messageBody" value={messageBody} onChange={(e) => setMessageBody(e.target.value)} required disabled={isLoading} // Disable textarea during sending /> </div> <button type="submit" disabled={isLoading}> {isLoading ? 'Sending…' : 'Send SMS'} </button> </form> {/* Display Status/Feedback Message */} {statusMessage && ( <p className={`status-message ${isError ? 'error' : 'success'}`}> {statusMessage} </p> )} </div> ); } export default App;3.2. Basic Styling (Optional):
Add some basic styles to
frontend/src/App.cssfor better presentation:/* frontend/src/App.css */ .App { font-family: sans-serif; max-width: 500px; margin: 40px auto; padding: 20px; border: 1px solid #ccc; border-radius: 8px; box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.1); } h1 { text-align: center; color: #333; } form div { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; font-weight: bold; color: #555; } input[type="tel"], textarea { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; /* Include padding in width */ font-size: 1rem; } textarea { height: 100px; resize: vertical; } button { background-color: #007bff; /* Blue */ color: white; padding: 12px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; width: 100%; transition: background-color 0.2s ease-in-out; } button:hover { background-color: #0056b3; /* Darker blue */ } button:disabled { background-color: #cccccc; cursor: not-allowed; } .status-message { margin-top: 20px; padding: 10px; border-radius: 4px; text-align: center; } .status-message.success { background-color: #d4edda; /* Light green */ color: #155724; /* Dark green */ border: 1px solid #c3e6cb; } .status-message.error { background-color: #f8d7da; /* Light red */ color: #721c24; /* Dark red */ border: 1px solid #f5c6cb; }Explanation:
useStatehooks manage the recipient number, message body, loading state, and status message displayed to the user.import.meta.env.VITE_API_URL(defined infrontend/.env), with a fallback tohttp://localhost:3001/api/send-sms.handleSubmit:fetchto make aPOSTrequest to the backend API.recipientandmessageBodyin the request body as a JSON string.Content-Typeheader toapplication/json.statusMessagebased on whether the request was successful (response.okanddata.success) or resulted in an error.catchblock handles network errors (e.g., backend server not running).finally: Resets the loading state regardless of the outcome.onSubmithandler linked tohandleSubmit. It disables inputs and the button during the loading state. A conditional paragraph (<p>) displays thestatusMessage.4. Configure Your Twilio Credentials
Obtain and securely manage your Twilio credentials.
4.1. Obtain Your Credentials:
+and country code (E.164 format).4.2. Store Credentials Securely (
.env):Place your Twilio credentials only in the
backend/.envfile:Crucially: Add
.envto yourbackend/.gitignorefile. Never commit your.envfile or hardcode credentials directly in your source code (server.jsor frontend files).Similarly, ensure
frontend/.env(containingVITE_API_URL) is in yourfrontend/.gitignore.4.3. Understand Environment Variables:
TWILIO_ACCOUNT_SID(Backend): Your unique account identifier.TWILIO_AUTH_TOKEN(Backend): Your secret key for authenticating API requests. Treat it like a password.TWILIO_PHONE_NUMBER(Backend): The E.164 formatted Twilio number used to send SMS messages.VITE_API_URL(Frontend): The URL of your backend API endpoint.5. Run and Test Your Application
Run both the backend and frontend servers to test the application. Ensure you run commands from the correct directories.
5.1. Start the Backend Server:
Open a terminal window, navigate to the project's root directory (
twilio-sms-app), then into thebackenddirectory, and run:cd backend # Make sure you are in the twilio-sms-app/backend directory node server.jsYou should see output like:
If you installed
nodemon(optional,npm install -D nodemon), runnodemon server.jsfor automatic restarts on file changes.5.2. Start the Frontend Development Server:
Open a second terminal window, navigate to the project's root directory (
twilio-sms-app), then into thefrontenddirectory, and run:cd frontend # Make sure you are in the twilio-sms-app/frontend directory npm run devVite starts the development server, typically on port 5173 (or the next available port), and provides a local URL:
5.3. Verify Manually:
http://localhost:5173/) provided by Vite in your web browser.+15559876543).backendserver – you should see logs for the incoming request and the Twilio API call attempt (success or error).5.4. Test the API Directly (Optional –
curl/ Postman):Test the backend API endpoint directly.
Using
curl: Open a terminal and run:curl -X POST http://localhost:3001/api/send-sms \ -H "Content-Type: application/json" \ -d '{"to": "+15559876543", "body": "Test message from curl"}'Replace
+15559876543with a valid recipient number (your verified number if on a trial account). You should get a JSON response like:{"success":true,"message":"SMS sent successfully! SID: SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}or an error JSON.Using Postman: Create a new
POSTrequest tohttp://localhost:3001/api/send-sms. Set the Body type torawand format toJSON. Enter the JSON payload:{"to": "+15559876543", "body": "Test from Postman"}. Send the request and observe the response.6. Handle Errors and Understand Caveats
While the code includes basic error handling, here are common issues and considerations:
Backend (
server.js):.envvariables aren't loaded. Ensure thebackend/.envfile exists in thebackenddirectory, has the correct name, and contains the right variables.try...catch):+and the country code. Reference: https://www.twilio.com/docs/api/errors/21211TWILIO_PHONE_NUMBERinbackend/.envis incorrect, not SMS-enabled, or not owned by your account. Verify in Console > Phone Numbers > Manage > Active Numbers.backend/.envis incorrect. Double-check them in the Twilio Console. Auth Token is case-sensitive.Frontend (
App.jsx):fetchcall will fail (caught in thecatchblock). Ensure the backend is running and theVITE_API_URLinfrontend/.env(or the fallback inApp.jsx) is correct.cors()middleware is missing or misconfigured on the backend, the browser will block thefetchrequest. Check the browser's developer console (Network tab) for CORS errors. The providedcors()setup is generally permissive for development; restrict it in production (see Security).messagefield from the backend's JSON error response.General Caveats:
E.164 Format: Always use
+followed by the country code and number (e.g.,+1for North America,+44for UK). The ITU-T E.164 standard specifies international telephone numbers must not exceed 15 digits (including country code). Reference: https://www.itu.int/rec/T-REC-E.164/enRate Limits: Twilio imposes rate limits on sending messages. Default limits: 1 message per second per phone number for new accounts, up to 100 concurrent requests per account. Sending too rapidly results in
429 Too Many Requestserrors. For high volume, use Twilio Messaging Services with verified sender profiles. Reference: https://www.twilio.com/docs/usage/webhooks/webhooks-connection-overrides#connection-overridesCompliance (A2P 10DLC – US & Canada): Sending Application-to-Person (A2P) SMS messages from standard 10-digit long code phone numbers to users in the US requires A2P 10DLC registration (effective September 1, 2022). Unregistered A2P traffic faces heavy carrier filtering and delivery failures. Registration involves:
US carriers (AT&T, T-Mobile, Verizon) enforce this requirement, not Twilio. Trial accounts are exempt from A2P 10DLC registration but have sending restrictions. For production US messaging, budget time for registration before launch. Reference: https://www.twilio.com/docs/sms/a2p-10dlc
Canada has similar requirements through the Canadian SMS Sender Registration (effective June 1, 2023). Reference: https://www.twilio.com/docs/sms/a2p/a2p-canada
Trial Account Limitations: Messages sent from trial accounts are prefixed with "Sent from a Twilio trial account" and can only be successfully sent to phone numbers verified in your Twilio Console. Trial accounts receive $15.50 USD in credit (as of 2025).
7. Secure Your Application
Security is paramount, especially when dealing with API keys.
Credential Security: Never expose
TWILIO_ACCOUNT_SIDorTWILIO_AUTH_TOKENin frontend code or commit them to version control. Use environment variables on the backend (backend/.envlocally, platform-specific environment variables in deployment). Treat Auth Token like a password – rotate it immediately if compromised.Input Validation (Backend): The current validation is minimal. Implement robust validation on the backend:
libphonenumber-jslibrary (npm packagelibphonenumber-js) to strictly validate E.164 format and check number validity for specific regions:const { parsePhoneNumber } = require('libphonenumber-js'); try { const phoneNumber = parsePhoneNumber(to); if (!phoneNumber.isValid()) { return res.status(400).json({ success: false, message: 'Invalid phone number' }); } } catch (error) { return res.status(400).json({ success: false, message: 'Phone number parsing failed' }); }joiorexpress-validatorfor structured validation.Rate Limiting (Application Level): Beyond Twilio's limits, add rate limiting to your
/api/send-smsendpoint usingexpress-rate-limit:const rateLimit = require('express-rate-limit'); const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 10, // Limit each IP to 10 requests per windowMs message: 'Too many SMS requests from this IP, try again later.' }); app.post('/api/send-sms', limiter, async (req, res) => { /* ... */ });This prevents abuse and controls costs.
CORS Configuration (Production): In
server.js,app.use(cors());allows any origin. For production, restrict this to only your deployed frontend's domain:// Example: Restrictive CORS for production in backend/server.js const allowedOrigins = ['https://your-frontend-domain.com']; // Replace with your frontend URL(s) const corsOptions = { origin: function (origin, callback) { // Allow requests with no origin (like mobile apps or curl requests) if (!origin) return callback(null, true); if (allowedOrigins.indexOf(origin) === -1) { const msg = 'The CORS policy for this site does not allow access from the specified Origin.'; return callback(new Error(msg), false); } return callback(null, true); } }; app.use(cors(corsOptions));Authentication/Authorization: This example has no user authentication. In a real application, ensure only logged-in/authorized users can trigger SMS messages, possibly with further restrictions based on roles or permissions.
Conclusion
You've built a basic application for sending SMS messages using Node.js, Vite, React, and Twilio. Key takeaways include separating frontend and backend concerns, securely managing API credentials using environment variables, handling asynchronous API calls, and implementing basic error handling. Address security, validation, and compliance requirements (like A2P 10DLC) before deploying to production.
Frequently Asked Questions
How do I send SMS messages with Twilio and React?
Send SMS messages by creating a Node.js Express backend that handles Twilio API calls, keeping credentials secure on the server. Build a React frontend form that sends recipient numbers and messages to your backend API endpoint. Use
fetch()oraxiosto POST data to/api/send-sms, which callsclient.messages.create()with Twilio credentials stored in environment variables. Never expose Account SID or Auth Token in frontend code.What is A2P 10DLC and do I need it for SMS campaigns?
A2P 10DLC (Application-to-Person 10-Digit Long Code) is a US carrier requirement for businesses sending SMS from standard phone numbers, effective September 1, 2022. Register your business with The Campaign Registry (TCR), register your messaging campaign, and complete carrier vetting (1-2 weeks). Unregistered traffic faces heavy filtering and delivery failures. Trial Twilio accounts are exempt but have restricted sending capabilities.
How do I validate phone numbers in E.164 format?
Validate E.164 format using the regex
/^\+[1-9]\d{1,14}$/which checks for:+prefix, country code starting with 1-9, and 1-15 total digits (including country code). For production, use thelibphonenumber-jslibrary which validates region-specific rules, checks number types (mobile vs landline), and handles international formatting. E.164 is the ITU-T international telephone numbering standard required by Twilio API.What Twilio error codes should I handle in my SMS app?
Handle these critical Twilio error codes: 21211 (invalid 'To' number or unverified on trial), 21212 (invalid 'From' number), 20003 (authentication failed), 21408 (SMS capability not enabled), 21610 (unsubscribed recipient who texted "STOP"), 30007 (carrier-filtered content), and 21608 (unreachable number). Each error returns specific HTTP status codes – catch them in try-catch blocks and provide user-friendly messages.
How do I secure Twilio credentials in Node.js?
Secure Twilio credentials by storing Account SID and Auth Token in a
.envfile (never commit to Git), loading them withdotenvon the backend only, and never exposing them in frontend code or API responses. Use environment variables in production deployment platforms. Rotate your Auth Token immediately if compromised. Add rate limiting withexpress-rate-limitto prevent abuse and implement CORS restrictions for production environments.Can I send SMS to international numbers with Twilio?
Yes, send international SMS by purchasing a Twilio phone number with international SMS capabilities and formatting recipient numbers in E.164 format (e.g.,
+44for UK,+61for Australia). Check Twilio's geographic permissions in your account settings and enable specific countries. Pricing varies by destination country. Some countries have additional registration requirements – consult Twilio's country-specific SMS guidelines before launching international campaigns.What are SMS character limits and message segmentation?
SMS messages are limited to 160 GSM-7 characters or 70 UCS-2 (Unicode) characters per segment. Messages exceeding these limits split into multiple segments, each charged separately. Emojis and special characters trigger UCS-2 encoding, reducing the limit to 70 characters. Twilio automatically handles segmentation and reassembly. Use message length validation in your code to warn users and prevent unexpected costs for multi-segment messages.
How do I handle STOP/START opt-out messages with Twilio?
Twilio automatically processes STOP, UNSTOP, START, and HELP keywords sent by recipients. When someone texts STOP to your number, Twilio blocks future messages (Error 21610 if you try sending). Recipients must text START to re-subscribe. Access opt-out status via Twilio Console or API. For compliance with TCPA and carrier guidelines, always respect opt-outs, include clear opt-out instructions in initial messages, and never override Twilio's automatic handling.