Send SMS with Node.js, Express, and Vonage
This guide provides a step-by-step walkthrough for building a production-ready Node.js application using the Express framework to send SMS messages via the Vonage Messages API. We'll cover everything from project setup and Vonage configuration to implementation, error handling, security, and deployment considerations.
By the end of this tutorial, you will have a simple but robust API endpoint capable of accepting requests and sending SMS messages programmatically.
Project Overview and Goals
What We're Building: A Node.js Express server with a single API endpoint (/send-sms
) that accepts a recipient phone number and a message text, then uses the Vonage Messages API to send an SMS.
Problem Solved: This enables applications to programmatically send SMS notifications, alerts, verification codes, or other messages to users worldwide.
Technologies Used:
- Node.js: A JavaScript runtime environment for building server-side applications.
- Express: A minimal and flexible Node.js web application framework used to create the API endpoint.
- Vonage Messages API: A powerful API from Vonage for sending and receiving messages across various channels (SMS, MMS, WhatsApp, etc.). We'll focus on SMS.
- @vonage/server-sdk: The official Vonage Node.js SDK for interacting with Vonage APIs.
- dotenv: A module to load environment variables from a
.env
file intoprocess.env
.
System Architecture:
+-------------+ +---------------------+ +----------------+ +-----------+
| User/Client | ----> | Node.js/Express API | ----> | Vonage SDK | ----> | Vonage API| ----> SMS Recipient
| (e.g. curl, | | (POST /send-sms) | | (@vonage/ | | (Messages)|
| Postman) | <---- | (Sends Response) | <---- | server-sdk) | <---- | |
+-------------+ +---------------------+ +----------------+ +-----------+
Prerequisites:
- Node.js and npm (or yarn): Installed on your system. Download Node.js
- Vonage API Account: A free account is sufficient to start. Sign up for Vonage
- Vonage Virtual Number: You need at least one Vonage phone number capable of sending SMS. You can get one from the Vonage dashboard after signing up.
- Text Editor or IDE: Such as VS Code.
- Basic Command Line Knowledge: Familiarity with
cd
_npm
_ etc.
(Optional but Recommended)
- Vonage CLI: Useful for managing Vonage applications and numbers via the command line. Install with
npm install -g @vonage/cli
. - ngrok: If you plan to extend this to receive SMS messages later_ ngrok is essential for exposing your local server. Download ngrok
Expected Outcome: A running Node.js server listening on a specified port (e.g._ 3000) with an endpoint POST /send-sms
. Sending a request to this endpoint with a valid to
phone number and text
message will trigger an SMS delivery via Vonage.
1. Setting up the Project
Let's create the project structure and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for your project_ then navigate into it.
mkdir vonage-sms-sender cd vonage-sms-sender
-
Initialize Node.js Project: This creates a
package.json
file to manage dependencies and project metadata.npm init -y
-
Enable ES Modules: Since our
index.js
example usesimport
/export
syntax_ we need to tell Node.js to treat.js
files as ES Modules. Edit yourpackage.json
file and add the following top-level key-value pair:// package.json { ""name"": ""vonage-sms-sender""_ ""version"": ""1.0.0""_ ""description"": """"_ ""main"": ""index.js""_ ""type"": ""module""_ // <-- Add this line ""scripts"": { ""test"": ""echo \""Error: no test specified\"" && exit 1"" }_ ""keywords"": []_ ""author"": """"_ ""license"": ""ISC""_ ""dependencies"": {} // Dependencies will be added next }
Make sure to save the
package.json
file. -
Install Dependencies: We need
express
for the web server_@vonage/server-sdk
to interact with the Vonage API_ anddotenv
to manage configuration securely.npm install express @vonage/server-sdk dotenv --save
express
: Web framework.@vonage/server-sdk
: Vonage's official Node.js library.dotenv
: Loads environment variables from.env
file.--save
: Adds these packages to yourdependencies
inpackage.json
.
-
Create Project Files: Create the main application file and a file for environment variables.
touch index.js .env .gitignore
-
Configure
.gitignore
: It's crucial not to commit sensitive information like API keys or your private key file. Add the following lines to your.gitignore
file:# .gitignore # Dependencies node_modules # Environment variables .env *.env.* !.env.example # Vonage private key private.key # Log files *.log logs *.log.*.[0-9]*.gz # Operating system specific files .DS_Store Thumbs.db
-
Project Structure: Your project should now look like this:
vonage-sms-sender/ ├── .env ├── .gitignore ├── index.js ├── package.json ├── package-lock.json └── node_modules/
2. Integrating with Vonage
Now_ let's configure your Vonage account and obtain the necessary credentials. The Messages API requires an Application ID and a private key for authentication.
-
Sign In to Vonage Dashboard: Access your Vonage API Dashboard.
-
Get API Key and Secret: Your API Key and API Secret are visible at the top of the dashboard home page. You'll need these for setting up the SDK initially and potentially for the Vonage CLI.
-
Set Default SMS API to ""Messages API"":
- Navigate to your account Settings in the left-hand menu.
- Scroll down to the API Settings section.
- Under Default SMS Setting_ ensure Messages API is selected. If it's set to ""SMS API""_ change it to ""Messages API"".
- Click Save changes.
- Why? Vonage has two SMS APIs. The SDK calls and webhook formats differ. We are using the newer_ more versatile Messages API for this guide.
-
Create a Vonage Application: Applications act as containers for your communication settings and credentials.
- In the dashboard menu_ go to Applications > Create a new application.
- Give your application a descriptive Name (e.g.,
My Node SMS App
). - Click Generate public and private key. This will automatically download a file named
private.key
. Save this file securely inside your project directory (e.g.,vonage-sms-sender/private.key
). Remember, we addedprivate.key
to.gitignore
so it won't be committed. - Enable the Messages capability. Toggle the switch ON.
- You'll see fields for Inbound URL and Status URL. Even though we are only sending SMS in this guide, the Messages API Application requires these URLs to potentially send message status updates back to your application. For now, you can use dummy URLs or point them to your local server if you plan to add receiving capabilities later.
- Inbound URL:
http://localhost:3000/webhooks/inbound
- Status URL:
http://localhost:3000/webhooks/status
- Set the HTTP method for both to
POST
. - Explanation: If you were receiving messages, the
Inbound URL
would receive the message content. TheStatus URL
receives delivery status updates (e.g.,delivered
,failed
).
- Inbound URL:
- Click Generate new application.
-
Get Application ID: After creating the application, you'll be taken to its configuration page. Copy the Application ID. It's a UUID (e.g.,
a1b2c3d4-e5f6-7890-abcd-ef1234567890
). -
Link Your Vonage Number:
- On the same application configuration page, scroll down to the Link virtual numbers section.
- Find the Vonage number you want to send SMS from and click the Link button next to it.
-
Configure Environment Variables: Open the
.env
file you created earlier and add your Vonage credentials. Replace the placeholder values with your actual credentials.# .env # Vonage API Credentials VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID VONAGE_PRIVATE_KEY_PATH=./private.key # Vonage Number to send SMS FROM (in E.164 format, e.g., 14155550100) VONAGE_NUMBER=YOUR_VONAGE_NUMBER # Server Configuration PORT=3000
VONAGE_API_KEY
,VONAGE_API_SECRET
: Found on the dashboard homepage. Used by the SDK internally sometimes.VONAGE_APPLICATION_ID
: Copied after creating the Vonage Application.VONAGE_PRIVATE_KEY_PATH
: The relative path fromindex.js
to your downloadedprivate.key
file../private.key
assumes it's in the same directory.VONAGE_NUMBER
: The Vonage virtual number you linked to the application, which will appear as the sender ID. Use E.164 format (country code + number, no spaces or symbols).PORT
: The port your Express server will listen on.
Security Note: The
.env
file should never be committed to version control (like Git). Ensure*.env
andprivate.key
are in your.gitignore
file. Use platform-specific environment variable management for deployment.
3. Implementing Core Functionality & API Layer
Now, let's write the Node.js code to initialize the Vonage SDK, create the Express server, and define the /send-sms
endpoint.
Edit your index.js
file:
// index.js
import express from 'express';
import { Vonage } from '@vonage/server-sdk';
import { Auth } from '@vonage/auth'; // Correct import for Auth
import 'dotenv/config'; // Load environment variables from .env file
// --- Initialization ---
const app = express();
const port = process.env.PORT || 3000; // Use port from .env or default to 3000
// Vonage Client Initialization
// Use Application ID and Private Key for Messages API authentication
const credentials = new Auth({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH,
// apiKey and apiSecret are not directly needed for Messages API JWT auth
// but can be useful for other SDK functions or fallback.
// apiKey: process.env.VONAGE_API_KEY,
// apiSecret: process.env.VONAGE_API_SECRET
});
const vonage = new Vonage(credentials);
// --- Middleware ---
// Enable parsing of JSON request bodies
app.use(express.json());
// Enable parsing of URL-encoded request bodies
app.use(express.urlencoded({ extended: true }));
// --- API Endpoint ---
app.post('/send-sms', async (req, res) => {
console.log('Received request body:', req.body); // Log incoming request
const { to, text } = req.body;
// Basic Input Validation (More robust validation recommended - see Security section)
if (!to || !text) {
console.error('Validation Error: Missing `to` or `text` in request body');
return res.status(400).json({
success: false,
error: 'Missing required fields: `to` (recipient phone number) and `text` (message content).',
});
}
// Ensure 'from' number is correctly loaded from environment variables
const fromNumber = process.env.VONAGE_NUMBER;
if (!fromNumber) {
console.error('Configuration Error: VONAGE_NUMBER is not set in environment variables.');
return res.status(500).json({ success: false, error: 'Server configuration error: Sender number not set.' });
}
console.log(`Attempting to send SMS from ${fromNumber} to ${to}`);
try {
const resp = await vonage.messages.send({
message_type: 'text',
to: to, // Recipient phone number (E.164 format recommended)
from: fromNumber, // Your Vonage virtual number
channel: 'sms',
text: text, // The message content
});
console.log('Vonage API Response:', resp);
res.status(200).json({
success: true,
message_uuid: resp.message_uuid,
message: `SMS submitted successfully to ${to}.`,
});
} catch (error) {
console.error('Vonage API Error:', error); // Log the detailed error
// Provide more specific feedback if possible
let errorMessage = 'Failed to send SMS.';
let errorDetails = error.message; // Default details
if (error.response && error.response.data) {
errorMessage = `Vonage API Error: ${error.response.data.title || error.response.data.detail || 'Unknown error'}`;
errorDetails = error.response.data;
console.error('Vonage Error Details:', error.response.data);
}
res.status(error.response?.status || 500).json({
success: false,
error: errorMessage,
details: errorDetails, // Include details for debugging
});
}
});
// --- Basic Health Check Endpoint ---
app.get('/health', (req, res) => {
res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
});
// --- Start Server ---
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
Code Explanation:
- Imports: We import
express
, theVonage
class andAuth
class from the SDK, anddotenv/config
to load environment variables immediately. - Initialization: We create an Express application instance and initialize the
Vonage
client using theAuth
class, passing the Application ID and the path to the private key from environment variables. - Middleware:
express.json()
andexpress.urlencoded()
are essential middleware to parse incoming request bodies in JSON and URL-encoded formats, respectively. /send-sms
Endpoint (POST):- This is an
async
function to allow usingawait
for the asynchronousvonage.messages.send
call. - It extracts the
to
(recipient number) andtext
(message body) from thereq.body
. - Basic Validation: It checks if
to
andtext
exist in the request. See the Security section for more robust validation. - It retrieves the
from
number from the environment variables. vonage.messages.send({...})
: This is the core SDK call.message_type: 'text'
: Specifies we are sending plain text.to
: The recipient's phone number (E.164 format like14155550101
is recommended).from
: Your Vonage virtual number (loaded from.env
).channel: 'sms'
: Specifies the communication channel.text
: The content of the SMS message.
- Response Handling:
- On success (
try
block), it logs the Vonage response and sends a200 OK
JSON response back to the client, including themessage_uuid
provided by Vonage. - On failure (
catch
block), it logs the detailed error object from the Vonage SDK and sends an appropriate error status code (extracted from the error object if possible, otherwise 500) and a JSON error message.
- On success (
- This is an
/health
Endpoint (GET): A simple endpoint useful for monitoring systems to check if the server is running.app.listen
: Starts the Express server, making it listen for incoming requests on the specifiedport
.
Testing the Endpoint:
-
Start the Server: In your terminal, run:
node index.js
You should see
Server listening at http://localhost:3000
. -
Send a Request (using
curl
): Open another terminal window. ReplaceYOUR_RECIPIENT_NUMBER
with a valid phone number (including country code, e.g.,12125551234
). Note: If you are using a Vonage trial account, this number must be added to your allowed list in the dashboard (Settings > Test Numbers).curl -X POST http://localhost:3000/send-sms \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""YOUR_RECIPIENT_NUMBER"", ""text"": ""Hello from Node.js and Vonage!"" }'
-
Send a Request (using Postman):
- Open Postman.
- Set the request type to
POST
. - Enter the URL:
http://localhost:3000/send-sms
- Go to the
Body
tab, selectraw
, and chooseJSON
from the dropdown. - Enter the JSON payload:
{ ""to"": ""YOUR_RECIPIENT_NUMBER"", ""text"": ""Hello from Postman, Node.js, and Vonage!"" }
- Click
Send
.
Expected Responses:
-
Success (200 OK):
{ ""success"": true, ""message_uuid"": ""a1b2c3d4-e5f6-7890-abcd-ef1234567890"", // Example UUID ""message"": ""SMS submitted successfully to YOUR_RECIPIENT_NUMBER."" }
You should receive the SMS on the recipient phone shortly after.
-
Validation Error (400 Bad Request):
{ ""success"": false, ""error"": ""Missing required fields: `to` (recipient phone number) and `text` (message content)."" }
-
Vonage API Error (e.g., 401 Unauthorized if credentials are wrong):
{ ""success"": false, ""error"": ""Vonage API Error: Unauthorized"", ""details"": { ""type"": ""https://developer.nexmo.com/api-errors/messages-olympus#unauthorized"", ""title"": ""Unauthorized"", ""detail"": ""You did not provide valid credentials."", ""instance"": ""bf0ca710-55e8-48a6-b0e6-a9fbd5b2f22a"" // Example instance ID } }
4. Implementing Proper Error Handling and Logging
While the basic try...catch
block handles errors, production applications need more robust strategies.
-
Consistent Error Format: We already implemented sending back a consistent JSON error object (
{ success: false, error: '...', details: '...' }
). Stick to this format. -
Specific Error Handling: You could add checks within the
catch
block for specific Vonage error codes (e.g.,error.response?.data?.title === 'Insufficient Balance'
) to provide more tailored user feedback or trigger alerts. -
Logging: The current
console.log
andconsole.error
are basic. For production, use a dedicated logging library like Winston or Pino. These enable:- Different log levels (debug, info, warn, error).
- Structured logging (JSON format is common for easier parsing by log analysis tools).
- Outputting logs to files, databases, or external logging services (like Datadog, Logstash, Splunk).
Example (Conceptual Winston Setup):
// Conceptual - requires npm install winston import winston from 'winston'; const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', // Log level from env or default to info format: winston.format.combine( winston.format.timestamp(), winston.format.json() // Log in JSON format ), defaultMeta: { service: 'sms-sender-service' }, transports: [ // Write all logs with level `error` and below to `error.log` new winston.transports.File({ filename: 'error.log', level: 'error' }), // Write all logs with level `info` and below to `combined.log` new winston.transports.File({ filename: 'combined.log' }), ], exceptionHandlers: [ // Optional: Log unhandled exceptions new winston.transports.File({ filename: 'exceptions.log' }) ], rejectionHandlers: [ // Optional: Log unhandled promise rejections new winston.transports.File({ filename: 'rejections.log' }) ] }); // If we're not in production then log to the `console` with colors if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() ), })); } // Replace console.log/error with logger calls: // logger.info(`Server listening at http://localhost:${port}`); // logger.error('Vonage API Error', { // message: error.message, // status: error.response?.status, // details: error.response?.data // });
-
Retry Mechanisms: For transient network errors or temporary Vonage issues, you might implement a retry strategy (e.g., using libraries like
async-retry
). Use exponential backoff to avoid overwhelming the API. However, be cautious retrying SMS sends, as you could accidentally send duplicate messages if the initial request did succeed but the response failed. Retries are often better suited for status checks or configuration tasks.
5. Adding Security Features
Protecting your API endpoint is crucial.
-
Input Validation and Sanitization: Never trust user input. The basic check in
index.js
is insufficient. Use a dedicated validation library like Joi or express-validator to enforce:to
: Must be a string, potentially matching a phone number pattern (E.164).text
: Must be a non-empty string, perhaps with a maximum length limit (to control costs and prevent abuse).
Example (Conceptual
express-validator
):// Conceptual - requires npm install express-validator import { body, validationResult } from 'express-validator'; // Add this middleware array before your route handler const validateSmsRequest = [ body('to') .trim() .notEmpty().withMessage('Recipient `to` number is required.') .isMobilePhone('any', { strictMode: false }).withMessage('Invalid phone number format for `to`. E.164 format recommended.'), // Basic phone check body('text') .trim() .notEmpty().withMessage('Message `text` is required.') .isLength({ max: 1600 }).withMessage('Message text exceeds maximum length (1600 characters).'), // SMS limit (generous) ]; // In your route definition: app.post('/send-sms', validateSmsRequest, async (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { // Log validation errors for debugging console.error('Validation Errors:', errors.array()); return res.status(400).json({ success: false, errors: errors.array() }); } // ... rest of your existing route logic ... });
-
Rate Limiting: Prevent abuse (accidental or malicious) by limiting how many requests a client can make in a given time window. Use a library like express-rate-limit.
Example (Basic Rate Limiting):
// Conceptual - requires npm install express-rate-limit import rateLimit from 'express-rate-limit'; const smsLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per `windowMs` message: { success: false, error: 'Too many SMS requests from this IP, please try again after 15 minutes.' }, standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers // store: // RedisStore, MemcachedStore, etc. for distributed environments }); // Apply the rate limiting middleware specifically to the SMS endpoint app.use('/send-sms', smsLimiter); // Define your endpoint *after* applying the limiter app.post('/send-sms', /* validation middleware, */ async (req, res) => { /* ... */ });
-
API Key / Authentication: For internal or protected APIs, you would typically require an API key or other authentication mechanism (like JWT tokens) on the
/send-sms
endpoint itself, not just rely on Vonage credentials. This prevents unauthorized parties from using your endpoint to send SMS messages via your Vonage account. This is beyond the scope of this basic guide but essential for production. Common approaches include checking for a specific header (X-API-Key
) or using middleware like Passport.js for more complex strategies. -
Secure Credential Management: Reiterate: Use environment variables (
.env
locally, platform-specific variables in deployment). Never hardcode credentials or commit.env
orprivate.key
. Consider using secrets management tools for production environments (e.g., AWS Secrets Manager, HashiCorp Vault, Google Secret Manager).
6. Database Schema and Data Layer
This specific example does not require a database as it only sends outgoing messages based on immediate API requests.
If you were building features like scheduled messages, storing message history, or managing user preferences, you would need to:
- Choose a database (e.g., PostgreSQL, MongoDB, MySQL).
- Design a schema (e.g., tables/collections for
messages
,users
,schedules
). Amessages
table might include columns likemessage_uuid
(from Vonage),recipient_number
,sender_number
,message_text
,status
(e.g., submitted, delivered, failed),vonage_response
,created_at
,updated_at
. - Use an ORM (like Prisma, Sequelize, TypeORM) or a database driver (like
pg
for PostgreSQL,mysql2
for MySQL,mongodb
for MongoDB) to interact with the database. - Implement data access logic (creating, reading, updating, deleting records) within your service layer or dedicated data access modules.
- Manage database migrations to handle schema changes over time (tools like Prisma Migrate, Sequelize CLI, TypeORM migrations).
This is out of scope for the current guide.
7. Handling Special Cases
- Trial Account Limitations: Vonage trial accounts can typically only send SMS messages to phone numbers that have been verified and added to the ""Test Numbers"" list in the Vonage dashboard (Settings > Test Numbers). Ensure your
to
number is on this list during development if using a trial account. Attempting to send to other numbers will result in an error (often related to whitelisting or permissions). - Sender ID: In some countries, the
from
number might be replaced by a generic ID (like ""InfoSMS"") or an Alphanumeric Sender ID if you have one registered and approved with Vonage. Behavior varies significantly by country and carrier regulations. Check Vonage documentation for country-specific sender ID rules. Using an unregistered Alphanumeric Sender ID where required might cause messages to fail. - Character Limits and Encoding: A standard SMS segment has 160 characters (using GSM-7 encoding). Longer messages, or messages with non-GSM characters (like emojis, certain accented characters, requiring Unicode/UCS-2 encoding), will be split into multiple segments. Each UCS-2 segment holds fewer characters (typically 70). Vonage charges per segment. The Messages API handles this segmentation automatically, but be mindful of the
text
length and character set to predict and manage costs. - Delivery Status: This guide doesn't handle status updates (DLRs - Delivery Receipts). To track if a message was actually delivered, you would need to:
- Ensure your Vonage Application's
Status URL
points to a valid, publicly accessible endpoint on your server (using ngrok locally or your deployed URL). - Create an endpoint (e.g.,
POST /webhooks/status
) to receive status updates (likesubmitted
,delivered
,failed
,rejected
,accepted
) from Vonage via HTTP POST requests. - Implement logic in this endpoint to parse the incoming JSON payload from Vonage, identify the corresponding message (using the
message_uuid
), and update its status (e.g., log it, update a database record). - Secure this webhook endpoint (e.g., by verifying Vonage signatures using JWT).
- Ensure your Vonage Application's
- International Formatting: Always aim to use the E.164 format for phone numbers (
+
followed by country code and number, without spaces or symbols, e.g.,+447700900000
,+14155550101
) for bothto
andfrom
to ensure reliable international delivery and proper routing. The SDK and API might tolerate other formats, but E.164 is the most robust standard.
8. Performance Optimizations
For this simple endpoint, performance is unlikely to be a major issue unless under very high load. Key considerations:
- SDK Initialization: Initialize the Vonage SDK (
new Vonage(credentials)
) once when your application starts, not inside the request handler. Our current code already does this correctly. Recreating the client on every request adds unnecessary overhead (including reading the private key file repeatedly). - Asynchronous Operations: The Vonage SDK methods are asynchronous (
async/await
). Node.js handles this efficiently without blocking the event loop. Ensure all I/O operations (like potential database calls if added later, or complex logging transports) are also handled asynchronously using Promises orasync/await
. - Payload Size: Keep request and response JSON payloads reasonably small. Avoid sending excessively large amounts of data back and forth if not necessary.
- Connection Pooling: If interacting with a database, ensure you are using connection pooling to reuse database connections efficiently, rather than opening/closing a connection for every request. ORMs typically handle this automatically.
- Caching: If certain data is frequently requested and doesn't change often (e.g., configuration settings, user permissions), consider caching it in memory (e.g., using a simple object or
Map
) or using an external cache like Redis to reduce load on downstream services or databases.
9. Monitoring, Observability, and Analytics
For production systems:
- Health Checks: The
/health
endpoint is a basic start. More advanced checks could verify connectivity to Vonage (e.g., by making a low-impact API call like fetching account balance) or other critical dependencies (like a database). Kubernetes and other orchestrators use health checks for managing container lifecycles. - Metrics: Track key application and business metrics using libraries like
prom-client
(for Prometheus) or platform-specific agents:- Request rate and latency for
/send-sms
and/health
. - Error rates (HTTP 4xx, 5xx) per endpoint.
- Node.js process metrics (CPU usage, memory usage, event loop lag).
- Vonage API success/error counts (by parsing responses or processing status webhooks).
- Number of SMS messages sent/failed per time period.
- Request rate and latency for
- Logging: As mentioned in Error Handling, use structured logging (JSON) and centralize logs using services like Elasticsearch/Logstash/Kibana (ELK stack), Splunk, Datadog Logs, Grafana Loki, or cloud provider logging services (AWS CloudWatch Logs, Google Cloud Logging). This allows for searching, filtering, and analyzing logs effectively.
- Distributed Tracing: For more complex systems involving multiple microservices, implement distributed tracing (e.g., using OpenTelemetry with Jaeger or Zipkin) to track requests as they flow through different services, helping pinpoint bottlenecks and errors.
- Error Tracking: Use services like Sentry, Bugsnag, or Rollbar to capture, aggregate, and alert on application errors (unhandled exceptions, promise rejections, logged errors) in real-time, providing stack traces and context.
- Monitoring Tools: Integrate with monitoring platforms (Datadog, Prometheus/Grafana, New Relic, Dynatrace) to visualize metrics, set up dashboards, and create alerts based on thresholds or anomalies (e.g., alert if the 5xx error rate exceeds 1%, or if Vonage API latency spikes above 500ms).
10. Troubleshooting and Caveats
401 Unauthorized
: Double-checkVONAGE_APPLICATION_ID
andVONAGE_PRIVATE_KEY_PATH
in your.env
file or environment variables. Ensure theprivate.key
file exists at the specified path relative to where you runnode index.js
and is readable by the Node.js process. Verify the Application ID matches the one in the Vonage dashboard exactly. Ensure the private key content is correct if reading from an environment variable.Non-Whitelisted Destination
/Illegal Sender Address
(Trial Account): If using a trial account, ensure theto
number is verified and added to your Vonage dashboard under Settings > Test Numbers. Also, ensure thefrom
number (VONAGE_NUMBER
) is correctly linked to your Vonage Application in the dashboard.Invalid Parameters
/400 Bad Request
from Vonage: Check the format of theto
andfrom
numbers (E.164 recommended:+14155550101
). Ensuretext
is not empty or excessively long. Consult the Vonage Messages API reference for required fields and valid values formessage_type
,channel
, etc. Check your server logs for the detailed error response from Vonage.- Server Error
500
: Check your server logs (console.error
output or dedicated log files/services) for detailed stack traces. Common causes include:- Incorrect SDK initialization (e.g., missing credentials).
- Missing or incorrect environment variables (like
VONAGE_NUMBER
not being set). - File system errors (e.g., cannot read
private.key
). - Unhandled exceptions in your code logic.
- Network connectivity issues between your server and Vonage.
Cannot find module '@vonage/server-sdk'
or other modules: Runnpm install
again to ensure all dependencies listed inpackage.json
are installed in thenode_modules
directory. Check for typos inimport
statements.- Syntax Errors (e.g.,
SyntaxError: Cannot use import statement outside a module
): Ensure your Node.js version supports ES Modules (v14+ recommended). Verify that""type"": ""module""
is correctly added to the top level of yourpackage.json
. If using CommonJS (require
), ensure you are using the correct import syntax (const { Vonage } = require('@vonage/server-sdk');
) and remove""type"": ""module""
. - Private Key Permissions: On Linux/macOS systems, ensure the
private.key
file has the correct read permissions for the user running the Node.js process (e.g.,chmod 600 private.key
). - Check Vonage Dashboard: The Vonage Dashboard (under Applications > Your Application > Logs, or the global API Logs section) often provides valuable insight into API requests and reasons for failures directly from the Vonage platform.
11. Deployment and CI/CD
-
Environment Variables: Crucially, do not deploy your
.env
file orprivate.key
file directly. Use your hosting provider's mechanism for setting environment variables (e.g., Heroku Config Vars, AWS Systems Manager Parameter Store / Secrets Manager, Google Secret Manager, Vercel Environment Variables, Docker environment variables). You will need to securely setVONAGE_API_KEY
,VONAGE_API_SECRET
,VONAGE_APPLICATION_ID
,VONAGE_NUMBER
, andPORT
. For the private key, the best practice is to store the content of the private key in a secure environment variable (e.g.,VONAGE_PRIVATE_KEY_CONTENT
) and modify the SDK initialization to read it from there, rather than relying on a file path.Example reading key from environment variable:
// In index.js, modify the credentials setup: const privateKeyContent = process.env.VONAGE_PRIVATE_KEY_CONTENT; if (!privateKeyContent) { throw new Error('VONAGE_PRIVATE_KEY_CONTENT environment variable not set.'); } const credentials = new Auth({ applicationId: process.env.VONAGE_APPLICATION_ID, // Read key content directly from an environment variable // Replace escaped newlines if necessary (common issue with multi-line env vars) privateKey: privateKeyContent.replace(/\\n/g, '\n'), // apiKey: process.env.VONAGE_API_KEY, // Keep if needed // apiSecret: process.env.VONAGE_API_SECRET // Keep if needed }); // Ensure the VONAGE_PRIVATE_KEY_CONTENT environment variable is set // in your deployment environment, containing the full multi-line text // of the private key (-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----).
-
.gitignore
: Ensurenode_modules
,.env
,*.env.*
(except maybe.env.example
),private.key
, log files (*.log
,logs/
), and OS-specific files (.DS_Store
) are listed in your.gitignore
file before your first commit. -
Build Step: If using TypeScript or a bundler (like Webpack, esbuild), ensure you have a build script in your
package.json
(""build"": ""tsc""
or similar) and run this step as part of your deployment process to generate the JavaScript code that will be executed. Deploy the built artifacts (e.g., adist
folder), not the source code directly. -
Starting the Server: Use a process manager like PM2 (
pm2 start index.js --name sms-app
) or rely on your hosting platform's mechanism (e.g., Heroku Procfileweb: node index.js
, DockerCMD [""node"", ""index.js""]
, systemd service) to runnode index.js
. Process managers handle restarting the application if it crashes and can manage clustering for better performance. -
CI/CD Pipeline: Set up a pipeline using tools like GitHub Actions, GitLab CI, Jenkins, CircleCI, or Bitbucket Pipelines to automate:
- Linting/Formatting: Run ESLint, Prettier.
- Testing: Run unit tests, integration tests.
- Building: Compile TypeScript, bundle assets (if applicable).
- Security Scanning: Scan dependencies for vulnerabilities (e.g.,
npm audit
), check code for security issues. - Deploying: Push the built application to your hosting environment (e.g., Heroku, AWS Elastic Beanstalk, Vercel, Docker registry).
12. Verification and Testing
-
Manual Verification:
- Deploy the application to a staging or production environment.
- Configure all required environment variables securely on the host.
- Send a
POST
request to the deployed/send-sms
endpoint usingcurl
, Postman, or another HTTP client. Use a valid recipient number (whitelisted if needed for trial accounts). - Verify you receive a
200 OK
response with asuccess: true
and amessage_uuid
. - Check the recipient phone for the actual SMS message delivery. Note potential delays.
- Check the Vonage dashboard logs (API Logs or Messages API Logs) for the message status (
submitted
,delivered
, etc.). - Test error cases: send requests with missing
to
ortext
, invalid phone number formats, incorrect authentication (if implemented), etc., and verify you receive appropriate error responses (e.g., 400, 401, 403) withsuccess: false
.
-
Automated Testing:
-
Unit Tests: Use a framework like Jest or Mocha with Chai/Sinon to test individual functions or modules in isolation. You would mock the
@vonage/server-sdk
to avoid making real API calls during tests and to assert that the SDK methods are called with the correct parameters.Example (Conceptual Jest Unit Test for the route handler):
// Conceptual - requires npm install --save-dev jest @types/jest // __tests__/smsRoute.test.js (assuming route logic is in a testable function) // Mock the Vonage SDK const mockSend = jest.fn(); jest.mock('@vonage/server-sdk', () => ({ Vonage: jest.fn().mockImplementation(() => ({ messages: { send: mockSend, }, })), })); jest.mock('@vonage/auth', () => ({ // Mock Auth as well Auth: jest.fn().mockImplementation(() => ({})), })); // Import your app or route handler function after mocks // const { handleSendSms } = require('../src/smsHandler'); // Example structure describe('POST /send-sms handler', () => { beforeEach(() => { // Reset mocks before each test mockSend.mockClear(); process.env.VONAGE_NUMBER = '15551234567'; // Set necessary env var }); test('should send SMS successfully with valid input', async () => { const mockReq = { body: { to: '15559876543', text: 'Test message' }, }; const mockRes = { // Mock Express response object status: jest.fn().mockReturnThis(), json: jest.fn(), }; mockSend.mockResolvedValue({ message_uuid: 'test-uuid' }); // Mock successful API call // await handleSendSms(mockReq, mockRes); // Call the handler // expect(mockSend).toHaveBeenCalledWith({ // message_type: 'text', // to: '15559876543', // from: '15551234567', // channel: 'sms', // text: 'Test message', // }); // expect(mockRes.status).toHaveBeenCalledWith(200); // expect(mockRes.json).toHaveBeenCalledWith({ // success: true, // message_uuid: 'test-uuid', // message: expect.stringContaining('SMS submitted successfully'), // }); }); test('should return 400 if `to` is missing', async () => { // ... test case for missing 'to' ... // expect(mockSend).not.toHaveBeenCalled(); // expect(mockRes.status).toHaveBeenCalledWith(400); // expect(mockRes.json).toHaveBeenCalledWith(expect.objectContaining({ success: false })); }); // ... more unit tests for other scenarios (missing text, API error) ... });
-
Integration Tests: Test the interaction between your API endpoint and the (mocked or real, in specific environments) Vonage service. You might use libraries like
supertest
to make HTTP requests to your running Express application during tests and assert the responses. -
End-to-End (E2E) Tests: These tests simulate real user scenarios, making actual API calls to your deployed application (potentially in a staging environment) and verifying the outcome, including checking for the received SMS (which is harder to automate fully). Use tools like Cypress or Playwright if you have a frontend interacting with this API. For API-only E2E, tools like Postman (with Newman CLI) or custom scripts using HTTP clients can be used. Be mindful of costs and rate limits when running E2E tests that make real API calls.
-