Developer guide: Implementing two-way SMS messaging with Node.js, Express, and Vonage
This guide provides a complete walkthrough for building a production-ready Node.js application using the Express framework to handle two-way SMS messaging via the Vonage Messages API. You'll learn how to send outbound SMS messages and set up webhooks to receive inbound messages and delivery statuses, enabling interactive communication flows.
By the end of this guide, you will have a functional Node.js application capable of:
- Sending SMS messages programmatically using the Vonage Messages API.
- Receiving incoming SMS messages via a webhook endpoint.
- Receiving message delivery status updates via a webhook endpoint.
- Handling basic two-way interaction (e.g., auto-replying to incoming messages).
We assume you have a basic understanding of Node.js, asynchronous programming (Promises, async/await), and REST APIs.
Project overview and goals
Goal: To create a robust Node.js service that can both send and receive SMS messages using Vonage's communication capabilities.
Problem Solved: This implementation enables applications to engage users via SMS for notifications, alerts, two-factor authentication (2FA), customer support, marketing campaigns, or any scenario requiring direct mobile communication. It provides the foundation for building interactive SMS-based services.
Technologies Used:
- Node.js: A JavaScript runtime environment ideal for building scalable network applications, especially I/O-intensive ones like webhook handlers.
- Express: A minimal and flexible Node.js web application framework that simplifies creating API endpoints and handling HTTP requests, perfect for our webhook server.
- Vonage Messages API: A unified API from Vonage enabling communication across multiple channels (SMS, MMS, WhatsApp, etc.). We focus on SMS for this guide. It provides reliable message delivery and robust webhook features.
@vonage/server-sdk
: The official Vonage Node.js SDK simplifies interaction with Vonage APIs.ngrok
: A tool to expose local development servers to the internet, essential for testing Vonage webhooks without deploying (for local development testing only).dotenv
: A module to load environment variables from a.env
file intoprocess.env
, keeping sensitive credentials out of source code.
System Architecture:
+-------------+ +-----------------+ +---------------------+ +---------------------+
| User's Phone| <---->| Vonage Platform | <---->| Your Node.js/Express| ----> | (Optional) Database |
| (SMS) | | (Messages API) | | App (Webhook Server)| | / Other Services |
+-------------+ +-----------------+ +---------------------+ +---------------------+
| ^ | ^
| Send SMS | | Receive SMS | Store/Process Data
|______________________| |______________________|
| | |
| Delivery Status | | Outbound SMS Request
v | v
+---------------------+ | +---------------------+
| Your Node.js/Express| <---- | Vonage Platform |
| App (Sending Logic) | | (Messages API) |
+---------------------+ +---------------------+
Expected Outcome: A running Node.js application with two main parts:
- A script (
send-sms.js
) to send an outbound SMS on demand. - An Express server (
server.js
) listening for incoming HTTP POST requests from Vonage webhooks (inbound messages and status updates).
Prerequisites:
- Node.js and npm (or yarn): Installed on your development machine. (LTS version recommended).
- Vonage API Account: Sign up if you don't have one. You'll get free credits to start.
- Vonage API Key and Secret: Found on your Vonage API Dashboard.
- Vonage Virtual Phone Number: Purchase an SMS-capable number from the Vonage dashboard (e.g._ via Numbers -> Buy Numbers - Note: Exact dashboard navigation may change over time).
ngrok
: Installed and authenticated (a free account is sufficient). Download from ngrok.com.- Vonage CLI (Optional but Recommended): Install via
npm install -g @vonage/cli
. Useful for managing applications and numbers.
1. Setting up the project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
mkdir vonage-sms-app cd vonage-sms-app
-
Initialize Node.js Project: This creates a
package.json
file to manage project details and dependencies.npm init -y
-
Install Dependencies: We need
express
for the web server,@vonage/server-sdk
to interact with the Vonage API, anddotenv
for managing environment variables.npm install express @vonage/server-sdk dotenv
-
Project Structure: Create the following basic structure:
vonage-sms-app/ ├── node_modules/ ├── .env # Stores environment variables (DO NOT COMMIT) ├── .gitignore # Specifies intentionally untracked files ├── private.key # Vonage application private key (DO NOT COMMIT) ├── package.json ├── package-lock.json ├── send-sms.js # Script to send outbound SMS └── server.js # Express server for receiving webhooks
-
Configure
.gitignore
: Create a.gitignore
file in the root directory to prevent committing sensitive information and unnecessary files.# .gitignore node_modules .env private.key *.log
Why .gitignore
? This ensures your API secrets, private keys, and local environment configurations are not accidentally pushed to version control systems like Git, which is crucial for security.
2. Vonage configuration
Before writing code, we need to configure our Vonage account and application settings. Note: Specific UI labels and navigation paths within the Vonage Dashboard may change over time.
-
API Key and Secret: Navigate to your Vonage API Dashboard. Your API Key and API Secret are displayed prominently at the top. You'll need these shortly.
-
Purchase a Vonage Number:
- Go to
Numbers
->Buy Numbers
(or equivalent section) in the dashboard. - Search for a number with SMS capability in your desired country.
- Purchase the number. Note down this number. It will be your
VONAGE_NUMBER
.
- Go to
-
Select Messages API for SMS:
- Crucially, Vonage has both an older SMS API and the newer Messages API. They use different webhook formats and authentication. This guide uses the Messages API.
- Go to your API Settings page.
- Under
SMS Settings
, ensureDefault SMS Setting
is set toUse the Messages API
. - Click
Save changes
.
Why this setting? It ensures that webhooks related to your Vonage number use the format expected by the Messages API and the
@vonage/server-sdk
methods we'll use. -
Create a Vonage Application: The Messages API primarily uses Applications for authentication via public/private key pairs, which is more secure for API access compared to only using the API Key/Secret (though Key/Secret might still be used by the SDK for other functionalities).
- Go to
Applications
->Create a new application
(or equivalent). - Give your application a name (e.g.,
Nodejs SMS App
). - Click
Generate public and private key
. Immediately save theprivate.key
file that downloads. Move this file into your project's root directory (where.gitignore
will prevent committing it). - Enable the
Messages
capability. - You'll need to provide Webhook URLs:
- Inbound URL:
YOUR_NGROK_URL/webhooks/inbound
(We'll getYOUR_NGROK_URL
later using ngrok). - Status URL:
YOUR_NGROK_URL/webhooks/status
- Inbound URL:
- Leave these blank for now or enter temporary placeholders like
http://example.com/inbound
. We will update them after startingngrok
. - Click
Generate new application
. - Note down the Application ID provided.
- Go to
-
Link Your Number to the Application:
- Go to
Numbers
->Your numbers
. - Find the Vonage number you purchased.
- Click the
Edit
(pencil) icon orManage
button next to the number. - In the
Forwarding
orApplication
section, select the Vonage Application you just created (Nodejs SMS App
) from the dropdown underMessages
. - Save the changes.
Why link the number? This tells Vonage to route incoming messages and status updates for this specific number to the webhooks defined in the linked Application.
- Go to
.env
)
3. Environment setup (Create the .env
file in your project root and add your credentials. Replace the placeholder values with your actual details.
# .env
# Vonage API Credentials (Found on Dashboard)
# Used by the SDK for some operations or legacy API access.
VONAGE_API_KEY=YOUR_API_KEY
VONAGE_API_SECRET=YOUR_API_SECRET
# Vonage Application Details (From Application creation)
# Primary authentication method for the Messages API client.
VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID
# Path relative to project root
VONAGE_PRIVATE_KEY_PATH=./private.key
# Vonage Number (Purchased Number) - Use E.164 format (e.g., +12015550123)
VONAGE_NUMBER=YOUR_VONAGE_NUMBER
# Test Recipient Number (Your mobile number) - Use E.164 format
MY_TEST_NUMBER=YOUR_MOBILE_NUMBER
VONAGE_API_KEY
,VONAGE_API_SECRET
: Used by the SDK for certain operations (e.g., some account management functions) or potentially as fallback authentication if Application ID/Key isn't configured for a specific client.VONAGE_APPLICATION_ID
,VONAGE_PRIVATE_KEY_PATH
: The primary credentials used by the Messages API client in this guide for sending messages via JWT generation using your private key.VONAGE_NUMBER
: The Vonage virtual number you purchased and linked to the application. This will be the sender (from
) number. Use E.164 format.MY_TEST_NUMBER
: The destination number (to
) for sending test messages. Use your own mobile number in E.164 format (country code + number, no spaces or symbols, e.g.,+14155552671
).
4. Sending SMS messages
Let's create the script (send-sms.js
) to send an outbound SMS.
// send-sms.js
'use strict';
// Load environment variables from .env file
require('dotenv').config();
// Import the Vonage Server SDK
const { Vonage } = require('@vonage/server-sdk');
const { MessengerText } = require('@vonage/messages');
// Initialize Vonage client with application credentials
// This uses Application ID and Private Key for Messages API authentication
const vonage = new Vonage({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH,
});
// Function to send an SMS message
async function sendSms(to, text) {
const from = process.env.VONAGE_NUMBER;
if (!from || !to || !text) {
console.error('Missing required environment variables or parameters.');
console.error(`FROM: ${from}, TO: ${to}, TEXT: ${text ? 'Provided' : 'Missing'}`);
return; // Exit if essential info is missing
}
console.log(`Attempting to send SMS from ${from} to ${to}...`);
try {
const resp = await vonage.messages.send(
new MessengerText({
text: text,
to: to,
from: from,
channel: 'sms', // Specify the channel as SMS
})
);
console.log('Message sent successfully!');
console.log('Message UUID:', resp.messageUuid);
} catch (err) {
console.error('Error sending SMS:');
// Log the detailed error response if available
if (err.response && err.response.data) {
console.error(JSON.stringify(err.response.data, null, 2));
} else {
console.error(err);
}
}
}
// --- Script Execution ---
// Get recipient number and message text
// For simplicity, using environment variable for recipient
// In a real app, 'to' and 'text' might come from user input, DB, etc.
const recipientNumber = process.env.MY_TEST_NUMBER;
const messageText = 'Hello from Vonage and Node.js!';
// Call the function to send the SMS
sendSms(recipientNumber, messageText);
Explanation:
require('dotenv').config();
: Loads variables from your.env
file intoprocess.env
.@vonage/server-sdk
: Imports the necessary Vonage SDK components. We useVonage
for the client andMessengerText
as a helper class for constructing the SMS payload correctly for the Messages API.new Vonage(...)
: Initializes the client. Crucially, for the Messages API, we provideapplicationId
andprivateKey
. The SDK handles JWT generation for authentication behind the scenes.sendSms
function:- Takes the recipient number (
to
) and message content (text
) as arguments. - Retrieves the
from
number (your Vonage number) from environment variables. - Includes basic validation to ensure required parameters are present.
- Uses
vonage.messages.send()
which is the method for the Messages API. - Constructs the payload using
new MessengerText({...})
, specifyingchannel: 'sms'
. - Uses
async/await
for cleaner asynchronous code. - Logs the
messageUuid
on success, which is useful for tracking. - Includes
try...catch
for robust error handling, logging the detailed error response from Vonage if available.
- Takes the recipient number (
- Script Execution: Sets the recipient and text, then calls
sendSms
.
Run the Sending Script:
Execute the script from your terminal:
node send-sms.js
You should see output indicating the attempt and success (with a message UUID) or failure (with error details). Check your mobile phone for the incoming SMS!
5. Receiving SMS messages (Webhook Server)
Now, let's build the Express server (server.js
) to handle incoming webhooks from Vonage.
1. Start ngrok
:
Vonage needs a publicly accessible URL to send webhooks to. ngrok
creates a secure tunnel from the internet to your local machine. Remember, ngrok
(especially the free tier) is suitable for development and testing only, not for production environments.
Open a new terminal window (keep the first one for running the Node server later) and run:
ngrok http 3000
Why port 3000? This matches the port our Express server will listen on.
ngrok
will display output similar to this:
ngrok (Ctrl+C to quit)
Session Status online
Account Your Name (Plan: Free)
Version x.x.x
Region United States (us-cal-1)
Latency xx.xxms
Web Interface http://127.0.0.1:4040
Forwarding https://<RANDOM_SUBDOMAIN>.ngrok-free.app -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Copy the https://<RANDOM_SUBDOMAIN>.ngrok-free.app
URL. This is YOUR_NGROK_URL
.
2. Update Vonage Application Webhooks:
- Go back to your Vonage Application settings in the dashboard (
Applications
-> Your Application Name). - Paste your
ngrok
Forwarding URL into the webhook fields:- Inbound URL:
https://<RANDOM_SUBDOMAIN>.ngrok-free.app/webhooks/inbound
- Status URL:
https://<RANDOM_SUBDOMAIN>.ngrok-free.app/webhooks/status
- Inbound URL:
- Ensure the HTTP Method for both is set to POST.
- Click
Save changes
.
3. Create the Express Server (server.js
):
// server.js
'use strict';
// Load environment variables
require('dotenv').config();
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000; // Use environment port or default to 3000
// --- Middleware ---
// Enable Express to parse JSON request bodies
app.use(express.json());
// Enable Express to parse URL-encoded request bodies
app.use(express.urlencoded({ extended: true }));
// --- Webhook Endpoints ---
// Inbound SMS Webhook Endpoint
app.post('/webhooks/inbound', (req, res) => {
console.log('--- Inbound SMS Received ---');
console.log('Request Body:', JSON.stringify(req.body, null, 2));
const { from, text, timestamp, message_uuid } = req.body;
// Basic validation
if (!from || !from.number || !text) { // Check from.number specifically
console.warn('Received incomplete inbound message data.');
// Still send 200 OK so Vonage doesn't retry
return res.status(200).send('Incomplete data received.');
}
console.log(`From: ${from.number}`); // Access sender number
console.log(`Text: ${text}`);
console.log(`Timestamp: ${timestamp}`);
console.log(`Message UUID: ${message_uuid}`);
// --- Add your logic here ---
// Example: Log to a file, store in a database, trigger another action,
// or implement an auto-reply (see section 6).
// --- IMPORTANT: Respond to Vonage ---
// Vonage expects a 200 OK response to acknowledge receipt of the webhook.
// Failure to respond (or responding with non-200) will cause Vonage to retry.
res.status(200).send('Webhook received successfully.');
// Use res.sendStatus(200); for an empty body response.
});
// Delivery Receipt (Status) Webhook Endpoint
app.post('/webhooks/status', (req, res) => {
console.log('--- Message Status Update Received ---');
console.log('Request Body:', JSON.stringify(req.body, null, 2));
const { message_uuid, status, timestamp, to } = req.body;
// Basic validation
if (!message_uuid || !status || !timestamp || !to || !to.number) {
console.warn('Received incomplete status update data.');
return res.status(200).send('Incomplete status data received.');
}
console.log(`Message UUID: ${message_uuid}`);
console.log(`Status: ${status}`); // e.g., 'delivered', 'accepted', 'failed', 'rejected'
console.log(`Timestamp: ${timestamp}`);
console.log(`Recipient: ${to.number}`);
// --- Add your logic here ---
// Example: Update message status in your database based on message_uuid.
// Handle 'failed' or 'rejected' statuses appropriately (e.g., logging, alerts).
// --- IMPORTANT: Respond to Vonage ---
res.status(200).send('Status webhook received successfully.');
});
// --- Root Endpoint (Optional: for health checks/testing) ---
app.get('/', (req, res) => {
res.send('SMS Webhook Server is running!');
});
// --- Start Server ---
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
console.log(`ngrok should be forwarding to http://localhost:${PORT}`);
console.log('Waiting for Vonage webhooks...');
});
// Basic Error Handling Middleware (Optional but Recommended)
app.use((err, req, res, next) => {
console.error('An unexpected error occurred:', err.stack);
res.status(500).send('Something broke!');
});
Explanation:
- Middleware (
express.json()
,express.urlencoded()
): Essential for parsing the incoming JSON and form-encoded data that Vonage sends in webhook requests. /webhooks/inbound
Endpoint (POST):- This route matches the Inbound URL configured in your Vonage Application.
- It logs the entire request body (
req.body
) received from Vonage. This is crucial for debugging and understanding the payload structure. - It extracts key fields like
from
(an object containing the sender'snumber
),text
(the message content),timestamp
, andmessage_uuid
. Includes basic validation. - Crucially sends
res.status(200).send(...)
. This acknowledges receipt to Vonage. Without this, Vonage will assume failure and retry sending the webhook, leading to duplicate processing.
/webhooks/status
Endpoint (POST):- Matches the Status URL configured in the Vonage Application.
- Receives updates about the delivery status of outbound messages you sent.
- Logs the payload, extracting fields like
message_uuid
,status
('delivered', 'failed', etc.),timestamp
, andto
(recipient). Includes basic validation. - Also sends a
200 OK
response.
- Server Start (
app.listen
): Starts the Express server on the specifiedPORT
(defaulting to 3000 to matchngrok
). - Basic Error Handling: A simple middleware catches unhandled errors.
4. Run the Webhook Server:
Go back to your first terminal window (where you ran npm install
) and start the server:
node server.js
You should see the ""Server listening..."" message.
5. Test Receiving:
- Send an SMS message from your mobile phone to your Vonage number.
- Watch the terminal where
node server.js
is running. You should see the ""--- Inbound SMS Received ---"" log, followed by the JSON payload of the incoming message. - Check the terminal where
ngrok
is running. You should seePOST /webhooks/inbound 200 OK
indicating the request was received and forwarded. - You can also inspect the request details in the
ngrok
web interface (usuallyhttp://127.0.0.1:4040
).
6. Test Status Updates:
- Run the sending script again:
node send-sms.js
. - Watch the
node server.js
terminal. After a short delay, you should see the ""--- Message Status Update Received ---"" log with the delivery status (likely 'delivered' or 'accepted' initially). - Check
ngrok
logs/interface forPOST /webhooks/status 200 OK
.
6. Putting it together: Basic two-way messaging (Auto-Reply)
Let's modify the server.js
to automatically reply to incoming messages.
-
Refactor Sending Logic: Move the
sendSms
function fromsend-sms.js
intoserver.js
(or a separate utility file) so the webhook handler can call it. Ensure@vonage/server-sdk
,dotenv
, and theVonage
client initialization are also present inserver.js
. -
Modify
/webhooks/inbound
Handler:
// server.js (Additions/Modifications)
'use strict';
require('dotenv').config();
const express = require('express');
const { Vonage } = require('@vonage/server-sdk');
const { MessengerText } = require('@vonage/messages');
const app = express();
const PORT = process.env.PORT || 3000;
// Initialize Vonage client (as in send-sms.js)
const vonage = new Vonage({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH,
});
// --- Middleware ---
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// --- Reusable Send SMS Function ---
async function sendSms(to, text) {
const from = process.env.VONAGE_NUMBER;
if (!from || !to || !text) {
console.error('Auto-reply failed: Missing required parameters.');
console.error(`FROM: ${from}, TO: ${to}, TEXT: ${text ? 'Provided' : 'Missing'}`);
return;
}
console.log(`Attempting auto-reply from ${from} to ${to}...`);
try {
const resp = await vonage.messages.send(
new MessengerText({ text, to, from, channel: 'sms' })
);
console.log('Auto-reply sent successfully! UUID:', resp.messageUuid);
} catch (err) {
console.error('Error sending auto-reply:');
if (err.response && err.response.data) {
console.error(JSON.stringify(err.response.data, null, 2));
} else {
console.error(err);
}
}
}
// --- Webhook Endpoints ---
app.post('/webhooks/inbound', async (req, res) => { // Note: async handler now
console.log('--- Inbound SMS Received ---');
console.log('Request Body:', JSON.stringify(req.body, null, 2));
// --- Acknowledge receipt IMMEDIATELY ---
// It's best practice to acknowledge the webhook quickly
// before doing potentially long-running tasks like sending another SMS.
res.status(200).send('Webhook received. Processing...');
// --- Process the message ---
const { from, text, timestamp, message_uuid } = req.body;
if (!from || !from.number || !text) {
console.warn('Received incomplete inbound message data. Cannot auto-reply.');
return; // Already sent 200 OK
}
console.log(`From: ${from.number}`);
console.log(`Text: ${text}`);
// --- Auto-Reply Logic ---
const replyText = `Thanks for your message: ""${text}"". We received it!`;
// Send the reply asynchronously AFTER acknowledging the webhook
await sendSms(from.number, replyText); // Use from.number as the recipient
});
// --- Status Webhook Endpoint (Keep as before) ---
app.post('/webhooks/status', (req, res) => {
console.log('--- Message Status Update Received ---');
console.log('Request Body:', JSON.stringify(req.body, null, 2));
const { message_uuid, status, timestamp, to } = req.body;
if (!message_uuid || !status || !timestamp || !to || !to.number) {
console.warn('Received incomplete status update data.');
return res.status(200).send('Incomplete status data received.');
}
console.log(`Message UUID: ${message_uuid}`);
console.log(`Status: ${status}`);
console.log(`Timestamp: ${timestamp}`);
console.log(`Recipient: ${to.number}`);
// Add logic to handle status updates (e.g., update DB)
res.status(200).send('Status webhook received successfully.');
});
// --- Root Endpoint ---
app.get('/', (req, res) => {
res.send('SMS Webhook Server is running!');
});
// --- Start Server ---
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
// --- Error Handling Middleware ---
app.use((err, req, res, next) => {
console.error('An unexpected error occurred:', err.stack);
res.status(500).send('Something broke!');
});
Explanation of Changes:
- The
sendSms
function and Vonage client initialization are now part ofserver.js
. - The
/webhooks/inbound
handler is markedasync
. - Important: We now send
res.status(200).send(...)
before processing the message and sending the reply. This prevents Vonage webhook timeouts if the reply takes time. - We extract the sender's number from
req.body.from.number
. - We define
replyText
. - We call
await sendSms(from.number, replyText)
to send the reply back to the original sender.
Retest:
- Restart the server:
Ctrl+C
thennode server.js
. - Send another SMS from your mobile to your Vonage number.
- Check the server logs – you should see the inbound message log.
- Check your mobile phone – you should receive the auto-reply message shortly after.
- Check the server logs again – you should see the status update for the auto-reply message.
7. Error handling and logging
Our current setup uses basic console.log
and console.error
. For production:
- Use a Structured Logger: Implement a more robust logging library like Winston or Pino. This enables different log levels (debug, info, warn, error), formatting (like JSON), and sending logs to files or external services.
// Example with Pino (Conceptual) const pino = require('pino')(); // Replace console.log/error with pino.info, pino.error, etc. pino.info({ body: req.body }, 'Inbound SMS Received');
- Centralized Error Handling: The basic Express error middleware is a start. Expand it to format error responses consistently and potentially send alerts for critical errors.
- Webhook Retries: Remember Vonage retries webhooks on non-200 responses or timeouts. Ensure your handlers are idempotent (safe to run multiple times with the same input) or have mechanisms to detect duplicate
message_uuid
s if necessary, especially if writing to a database. Always respond200 OK
quickly. - API Error Handling: The
try...catch
block aroundvonage.messages.send
is crucial. Parse theerr.response.data
for specific Vonage error codes and messages to understand failures (e.g., invalid number format, insufficient funds, carrier restrictions). Implement retry logic with exponential backoff for transient network errors when sending, if needed.
8. Security considerations
- Webhook Security: Our current setup relies on the obscurity of the
ngrok
URL. For production:- Signature Validation (Recommended Practice): While standard Vonage Messages API webhooks might not use JWT signatures by default, implementing some form of signature validation (JWT, shared secret HMAC, etc.) is a highly recommended security practice if available/configurable for your setup. This ensures requests genuinely originate from Vonage. Always consult the latest Vonage documentation for current webhook security options and best practices.
- Basic Authentication: Configure Basic Auth on your webhook endpoint and provide the credentials in the Vonage dashboard URL (e.g.,
https://user:pass@yourdomain.com/webhooks/inbound
). - IP Whitelisting: Configure your firewall/server to only accept requests from Vonage's documented IP ranges for webhooks (see Vonage documentation for current IPs). This is less flexible if IPs change.
- Credential Management: Never commit
.env
orprivate.key
. Use environment variables provided by your hosting platform or secrets management tools (like HashiCorp Vault, AWS Secrets Manager) in production. - Input Validation/Sanitization: If you process the
text
from incoming SMS messages (e.g., storing in DB, displaying in UI), sanitize it thoroughly to prevent Cross-Site Scripting (XSS) or other injection attacks. Libraries likeDOMPurify
(for HTML contexts) or robust validation/escaping are essential. - Rate Limiting: Protect your webhook endpoints from abuse or accidental loops by implementing rate limiting using Express middleware like
express-rate-limit
.
9. Database schema and data layer (Conceptual)
This guide doesn't implement a database, but this section provides a conceptual overview. The schema and code snippets below are illustrative examples, not a complete, production-ready implementation.
- Schema: You'd likely need tables for:
messages
: To store details of both inbound and outbound messages (UUID, direction, sender, recipient, text, timestamp, status, related message UUID for replies).conversations
(Optional): To group messages belonging to the same interaction thread.
- Entity Relationship Diagram (ERD) - Conceptual:
+--------------+ +--------------+ | conversations|--|--( messages | +--------------+ +--------------+ | conversation_id (PK)| message_id (PK)| | participant_a | | conversation_id (FK)| | participant_b | | direction | | last_update | | sender | +--------------+ | recipient | | text | | timestamp | | status | | vonage_uuid | +--------------+
- Data Access: Use an ORM like Prisma or Sequelize to interact with your database (PostgreSQL, MySQL, etc.).
- In
/webhooks/inbound
: Create a newmessages
record with direction 'inbound'. - In
sendSms
: Create a newmessages
record with direction 'outbound' and status 'submitted'. - In
/webhooks/status
: Find the message byvonage_uuid
(message_uuid from webhook) and update itsstatus
.
- In
- Migrations: Use the ORM's migration tools (e.g.,
prisma migrate dev
) to manage schema changes.
10. Testing and verification
Testing is crucial for a reliable messaging application. The examples here are conceptual starting points.
- Manual Testing:
- Send SMS to your Vonage number -> Verify server logs inbound webhook -> Verify auto-reply is received on mobile.
- Run
send-sms.js
(or trigger sending via your app logic) -> Verify SMS received on mobile -> Verify server logs status webhook. - Test edge cases: long messages, messages with special characters/emojis, invalid recipient numbers.
ngrok
Inspector: Usehttp://127.0.0.1:4040
during development to inspect exact headers and payloads of incoming webhooks, which is invaluable for debugging.- Unit/Integration Tests: Use frameworks like Jest or Mocha.
- Unit test individual functions (like input validation, payload formatting).
- Integration test SDK interactions (
vonage.messages.send
) by mocking the Vonage API client. - Integration test webhook handlers by sending mock HTTP requests and asserting responses/side effects (like database writes or calls to
sendSms
).
- End-to-End (E2E) Tests:
- These are more complex, often requiring a dedicated testing environment and potentially real Vonage numbers/credits.
- Simulate the full flow: send an SMS via the API -> verify receipt on a test device/simulator -> have the test device reply -> verify the inbound webhook is processed correctly -> verify the auto-reply is sent and received.
- Be mindful of costs and potential rate limits when running E2E tests against live services.