code examples
code examples
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.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:
+-----------------+ HTTP Request +-------------------+ Twilio API Call +--------+
| Vite/React UI | ---------------------> | Node.js/Express API | -------------------------> | Twilio |
| (User Input Form)| (Send SMS Data) | (/api/send-sms) | (AccountSID, AuthToken, | (SMS |
+-----------------+ +-------------------+ | To, From, Body) | Sent) |
^ | |
| HTTP Response (Success/Error) | |
+-------------------------------------------+ |
|
+---------------------+ +-------------------------+ |
| Environment Variables | | Environment Variables | |
| (backend/.env file) | | (frontend/.env file) | |
| - TWILIO_ACCOUNT_SID| | - VITE_API_URL | |
| - TWILIO_AUTH_TOKEN | +-------------------------+ |
| - TWILIO_PHONE_NUMBER| |
+---------------------+ |Prerequisites:
- Node.js and npm (or yarn): Ensure Node.js v18 LTS or later is installed (v20 LTS recommended as of 2025). Verify with
node -vandnpm -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:
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.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.
npm install express twilio dotenv cors1.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.dotenv# backend/.env TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token_xxxxxxxxxxxxxx TWILIO_PHONE_NUMBER=+15551234567Replace 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.text# backend/.gitignore node_modules .env -
server.js(Create this file in thebackenddirectory): This contains your backend server code. Add the logic in the next section.javascript// 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 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:
npm install1.5. Create Frontend Configuration Files:
-
.env(Create this file in thefrontenddirectory): This file stores frontend-specific environment variables, like the backend API URL.dotenv# frontend/.env VITE_API_URL=http://localhost:3001/api/send-smsVite 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.text# frontend/.gitignore node_modules dist .env* # Ignore .env, .env.development, .env.production etc. *.local # ... other default vite ignores(Note: Vite's default
.gitignoremight already cover.env*. Ensure it's present.)
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.
// 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:
- Environment Variables:
require('dotenv').config()loads variables frombackend/.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
asyncfunction because the Twilio API call is asynchronous. - It extracts
to(recipient number) andbody(message text) from the request's JSON payload (req.body). - Basic Validation: Checks if
toandbodyexist. Returns a400 Bad Requestif not. Note: Production apps require stricter validation (e.g., using a library likejoior 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 OKJSON response to the frontend. - Error Handling: A
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.
- It's an
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:
// 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.css for 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:
- State:
useStatehooks 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 infrontend/.env), with a fallback tohttp://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
fetchto make aPOSTrequest to the backend API. - Sends the
recipientandmessageBodyin the request body as a JSON string. - Sets the
Content-Typeheader toapplication/json. - Response Handling: Parses the JSON response from the backend. Updates the
statusMessagebased on whether the request was successful (response.okanddata.success) or resulted in an error. - Error Handling: A
catchblock 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
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:
- Account SID & Auth Token:
- Log in to your Twilio Console.
- On the main dashboard, find your Account SID and Auth Token. Click "Show" to reveal the Auth Token.
- Copy these values carefully.
- Twilio Phone Number:
- Navigate to Phone Numbers > Manage > Active Numbers in the Twilio Console.
- If you don't have one, go to Buy a Number, ensure the SMS capability is checked, search, and purchase a number.
- Copy the phone number exactly as shown, including the
+and country code (E.164 format).
4.2. Store Credentials Securely (.env):
-
Place your Twilio credentials only in the
backend/.envfile:dotenv# backend/.env TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token_xxxxxxxxxxxxxx TWILIO_PHONE_NUMBER=+1xxxxxxxxxx -
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 the backend directory, and run:
cd backend # Make sure you are in the twilio-sms-app/backend directory
node server.jsYou should see output like:
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:
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:
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
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:bashcurl -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):
- Missing Credentials: The initial check catches if
.envvariables aren't loaded. Ensure thebackend/.envfile exists in thebackenddirectory, 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_NUMBERinbackend/.envis 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/.envis 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).
- 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
- 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
fetchcall will fail (caught in thecatchblock). Ensure the backend is running and theVITE_API_URLinfrontend/.env(or the fallback inApp.jsx) is correct. - CORS Errors: If the
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). - 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
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/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 Requestserrors. 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
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:
- Phone Number Validation: Use
libphonenumber-jslibrary (npm packagelibphonenumber-js) to strictly validate E.164 format and check number validity for specific regions:javascriptconst { 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' }); } - 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
joiorexpress-validatorfor structured validation.
- Phone Number Validation: Use
-
Rate Limiting (Application Level): Beyond Twilio's limits, add rate limiting to your
/api/send-smsendpoint usingexpress-rate-limit:javascriptconst 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:javascript// 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() 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
How to send SMS messages with Node.js and Twilio
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.
What is the purpose of Vite in this project?
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.
Why use a separate backend for sending SMS?
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.
When should I register for A2P 10DLC?
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.
Can I use this setup for production?
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.
How to structure the project directories?
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).
What is the role of the .env file?
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.
Why does my Twilio trial account only send to my verified number?
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.
How to handle Twilio API errors?
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.
What is CORS and why is it necessary?
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.
How to validate the recipient's phone number?
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.
When testing locally, which port does the backend use?
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.
What is the architecture of this application?
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.