Developer Guide: Sending SMS with Node.js, Express, and Vonage
A complete guide to building a Node.js/Express application for sending SMS messages using the Vonage Messages API, covering setup, implementation, error handling, and configuration.
This guide provides a complete walkthrough for building a Node.js application using the Express framework to send SMS messages via the Vonage Messages API. We will cover everything from project setup to deployment considerations, enabling you to integrate SMS functionality into your applications effectively.
This tutorial focuses on sending SMS messages. While Vonage also enables receiving messages via webhooks, that functionality requires a publicly accessible server (often set up using tools like ngrok during development) and is detailed in separate Vonage documentation.
Project Overview and Goals
Goal: To create a simple, robust Node.js API endpoint that accepts a recipient phone number and a message text, then uses the Vonage Messages API to send an SMS message.
Problem Solved: This provides a foundational service for applications needing to send programmatic SMS notifications, alerts, verification codes, or other messages to users.
Technologies Used:
Node.js: A JavaScript runtime environment ideal for building scalable network applications.
Express: A minimal and flexible Node.js web application framework providing features for web and mobile applications, perfect for creating our API endpoint.
Vonage Messages API: A powerful API from Vonage that enables sending messages across various channels, including SMS, MMS, WhatsApp, and more. We'll use it specifically for SMS.
@vonage/server-sdk: The official Vonage Server SDK for Node.js, simplifying interactions with the Vonage APIs.
dotenv: A zero-dependency module that loads environment variables from a .env file into process.env, crucial for managing sensitive credentials securely.
Vonage Virtual Phone Number: You need a Vonage number capable of sending SMS messages. You can rent one via the Vonage Dashboard. Note that trial accounts may have restrictions (see Troubleshooting section).
Vonage Application: A Vonage Application configured for the Messages API, with generated public/private keys.
Basic Command Line/Terminal Knowledge: Familiarity with navigating directories and running commands.
(Optional) API Testing Tool: Like Postman or curl for testing the API endpoint.
Final Outcome: A running Node.js server with a single API endpoint (/send-sms) that successfully sends an SMS message via Vonage when called with a valid phone number and message.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
Create Project Directory: Open your terminal or command prompt and create a new directory for your project, then navigate into it.
Bash
mkdir vonage-sms-sender
cd vonage-sms-sender
Initialize Node.js Project: Use npm init to create a package.json file. The -y flag accepts default settings.
Bash
npm init -y
Install Dependencies: Install Express (web framework), the Vonage Server SDK, and dotenv (for environment variables).
@vonage/server-sdk: Interacts with the Vonage API.
dotenv: Loads environment variables from a .env file.
--save: Adds these packages to the dependencies section in your package.json.
Create Project Structure: Create the main application file and the environment file.
Bash
touch index.js .env .gitignore
index.js: Will contain our Express server and API logic.
.env: Will store our sensitive credentials (API keys, secrets, phone numbers). Never commit this file to version control.
.gitignore: Specifies intentionally untracked files that Git should ignore.
Configure .gitignore: Add node_modules and .env to your .gitignore file to prevent committing them.
Text
# .gitignore
node_modules/
.env
*.log
private.key # If storing the key directly in the project
Configure .env: Open the .env file and add the following placeholders. We will fill these in later (Section 4).
.env
# .env
PORT=3000
# Vonage Credentials (Using Application ID & Private Key - Recommended for Messages API)
VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID
VONAGE_PRIVATE_KEY_PATH=./private.key # Or the full path to your key file
# Vonage Virtual Number
VONAGE_VIRTUAL_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER_E164
PORT: The port number our Express server will listen on.
VONAGE_APPLICATION_ID: Your Vonage Application ID (obtained from the Vonage Dashboard).
VONAGE_PRIVATE_KEY_PATH: The file path to the private.key file downloaded when creating your Vonage Application. We assume it will be placed in the project root for this example.
VONAGE_VIRTUAL_NUMBER: The Vonage phone number you rented, in E.164 format (e.g., +12015550123).
Note on Authentication: The Messages API primarily uses Application ID and Private Key for authentication via JWTs generated by the SDK. This method is generally preferred for the Messages API and provides better security partitioning. We will use Application ID/Private Key in this guide.
2. Implementing Core Functionality
Now, let's write the core logic in index.js to initialize the Vonage SDK and prepare the function to send SMS.
JavaScript
// index.js'use strict';
// 1. Load Environment Variablesrequire('dotenv').config(); // Load variables from .env file into process.env// 2. Import Dependenciesconst express = require('express');
const { Vonage } = require('@vonage/server-sdk'); // Import Vonage SDK// 3. Initialize Vonage SDK// Ensure required environment variables are presentif (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH || !process.env.VONAGE_VIRTUAL_NUMBER) {
console.error(""Error: RequiredVonage environment variables (VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH, VONAGE_VIRTUAL_NUMBER) are not set."");
console.error(""Please check your .env file."");
process.exit(1); // Exit if essential config is missing
}
const vonage = newVonage({
applicationId: process.env.VONAGE_APPLICATION_ID,
privateKey: process.env.VONAGE_PRIVATE_KEY_PATH,
});
// 4. Define the SMS Sending FunctionasyncfunctionsendSms(toNumber, messageText) {
// Validate input format (basic)if (!/^\+[1-9]\d{1,14}$/.test(toNumber)) {
// Use standard quotes within the error stringthrownewError('Invalid ""to"" number format. Must be E.164 format (e.g., +12125551234)');
}
if (!messageText || typeof messageText !== 'string' || messageText.trim().length === 0) {
thrownewError('Invalid ""text"" parameter. Message text cannot be empty.');
}
const fromNumber = process.env.VONAGE_VIRTUAL_NUMBER;
// Use template literal for cleaner loggingconsole.log(`Attempting to send SMS from ${fromNumber} to ${toNumber}`);
try {
// Use vonage.messages.send() for the Messages APIconst resp = await vonage.messages.send({
channel: 'sms',
message_type: 'text',
to: toNumber,
from: fromNumber,
text: messageText,
// Optional: client_ref for your internal tracking (max 40 chars)// client_ref: `my-internal-ref-${Date.now()}`
});
console.log('SMS submitted successfully:');
console.log(resp); // Log the full response from Vonage// The Messages API returns a message_uuid for trackingif (resp && resp.message_uuid) {
return resp.message_uuid;
} else {
// This case might indicate an issue before actual sending attemptconsole.error(""Unexpected response structure fromVonage:"", resp);
thrownewError('SMS submission failed or response format unexpected.');
}
} catch (err) {
console.error(""Error sending SMS via Vonage:"");
// Log detailed error if available (e.g., network issue, SDK error)if (err.response && err.response.data) {
console.error(""VonageAPIErrorDetails:"", JSON.stringify(err.response.data, null, 2));
// Rethrow a more specific error based on Vonage response if neededthrownewError(`Vonage API Error: ${err.response.data.title || 'Unknown error'} - ${err.response.data.detail || JSON.stringify(err.response.data)}`);
} else {
console.error(err); // Log the raw error objectthrownewError(`Failed to send SMS: ${err.message || 'Unknown error'}`);
}
}
}
// --- Express API Setup will go here in the next section ---// Placeholder for exports and starting the server (will be added later)
Explanation:
Load Environment Variables:require('dotenv').config() reads the .env file and makes variables like process.env.VONAGE_APPLICATION_ID available. This should be done at the very beginning.
Import Dependencies: We import express and the Vonage class from the SDK.
Initialize Vonage SDK: We create a Vonage instance, passing our Application ID and the path to the private key file retrieved from process.env. Basic validation ensures these crucial variables are set.
Define sendSms Function:
This async function takes the recipient number (toNumber) and the message (messageText) as arguments.
It performs basic validation on the toNumber (checking for E.164 format) and messageText.
It retrieves the fromNumber from the environment variable.
It calls vonage.messages.send(), specifying the channel ('sms'), message_type ('text'), to, from, and text.
The try...catch block handles potential errors during the API call (network issues, invalid credentials, Vonage API errors).
On success, it logs the response and returns the message_uuid, which is Vonage's identifier for the submitted message.
On failure, it logs detailed error information and throws an error to be handled by the calling code (our API endpoint).
3. Building a Complete API Layer
Now, let's use Express to create an API endpoint that utilizes our sendSms function.
Add the following code to the end of your index.js file:
JavaScript
// index.js (continued)// --- Express API Setup ---const app = express();
// Middleware to parse JSON request bodies
app.use(express.json());
// Middleware to parse URL-encoded request bodies
app.use(express.urlencoded({ extended: true }));
// Define the POST endpoint for sending SMS
app.post('/send-sms', async (req, res) => {
// Extract 'to' and 'text' from the request bodyconst { to, text } = req.body;
// Basic input validation for presenceif (!to || !text) {
// Corrected console error loggingconsole.error('Validation Error: Missing ""to"" or ""text"" in request body');
return res.status(400).json({
success: false,
// Corrected JSON string formatmessage: 'Missing required parameters: ""to"" (E.164 format) and ""text"".'
});
}
try {
// Call our core SMS sending functionconst messageId = awaitsendSms(to, text);
// Send success responseconsole.log(`API successfully processed SMS request. Vonage Message ID: ${messageId}`);
return res.status(200).json({
success: true,
message: 'SMS submitted successfully.',
messageId: messageId // Include the Vonage message_uuid
});
} catch (error) {
// Handle errors from the sendSms functionconsole.error(`API Error processing /send-sms: ${error.message}`);
// Determine appropriate status code (e.g., 400 for validation, 500 for server/Vonage errors)const statusCode = error.message.includes('Invalid') ? 400 : 500;
return res.status(statusCode).json({
success: false,
message: `Failed to send SMS: ${error.message}`
});
}
});
// Health check endpoint (Good practice)
app.get('/health', (req, res) => {
res.status(200).json({ status: 'OK', timestamp: newDate().toISOString() });
});
// Export app and sendSms for testing purposes// Note: Starting the server should typically be in a separate file (e.g., server.js)// for better testability, but we'll keep it here for simplicity in this guide.module.exports = { app, sendSms };
// Start the Express server (only if this file is run directly)if (require.main === module) {
constPORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server listening on http://localhost:${PORT}`);
console.log(`SMS Send Endpoint: POST http://localhost:${PORT}/send-sms`);
console.log(`Health Check: GET http://localhost:${PORT}/health`);
});
}
Explanation:
Initialize Express:const app = express(); creates an Express application.
Middleware:
app.use(express.json()); enables the server to parse incoming request bodies formatted as JSON.
app.use(express.urlencoded({ extended: true })); enables parsing of URL-encoded data (often used by HTML forms).
POST /send-sms Endpoint:
Defines a route that listens for POST requests at the /send-sms path.
It extracts the to phone number and text message from the request body (req.body).
It performs basic validation to ensure both parameters are present.
It calls the await sendSms(to, text) function within a try...catch block.
On success, it returns a 200 OK response with success: true and the messageId received from Vonage.
On failure (catching the error thrown by sendSms), it logs the error and returns an appropriate error status code (400 for bad input, 500 for internal/Vonage errors) and a JSON response with success: false and the error message.
GET /health Endpoint: A simple health check endpoint that returns a 200 OK status, useful for monitoring.
Exports:module.exports = { app, sendSms }; exports the Express app instance and the sendSms function, making them accessible for testing (See Section 13 - Note: Section 13 was mentioned but not provided in the original input, this reference is kept for context).
Start Server: The if (require.main === module) block ensures app.listen() is only called when index.js is run directly (e.g., node index.js), not when it's required by another module (like a test file). This starts the server on the specified PORT.
Testing the Endpoint:
Once your Vonage credentials are set up (Section 4), you can test this endpoint using curl or Postman.
Using curl:
Bash
curl -X POST http://localhost:3000/send-sms \
-H ""Content-Type: application/json"" \
-d '{
""to"": ""+12015550124"",
""text"": ""Hello from Node.js and Vonage!""
}'
{""success"":false,""message"":""Missing required parameters: \""to\"" (E.164 format) and \""text\"".""}
Expected Error Response (JSON - e.g., Vonage API failure):
JSON
{""success"":false,""message"":""Failed to send SMS: Vonage API Error: Unauthorized - Please check your Application ID/private key""}
4. Integrating with Necessary Third-Party Services (Vonage)
This section details how to get the required credentials from Vonage and configure them in your .env file.
Sign Up/Log In to Vonage:
Go to the Vonage API Dashboard and log in or create a new account.
Create a Vonage Application:
Applications act as containers for your Vonage settings and credentials.
Navigate to ""Applications"" in the left-hand menu.
Click ""Create a new application"".
Name: Give your application a descriptive name (e.g., ""Node SMS Sender App"").
Generate Public and Private Key: Click the button to generate key pair. Crucially, save the private.key file immediately. You cannot retrieve it later. Place this file in your project directory (or a secure location referenced by the full path in .env).
Capabilities: Enable the ""Messages"" capability.
Inbound URL / Status URL: For sending SMS, these are less critical but still required by the dashboard. You can enter placeholders like https://example.com/webhooks/inbound and https://example.com/webhooks/status. If you later implement receiving SMS or delivery receipts, you'll need valid, publicly accessible URLs here (often using ngrok during development).
Click ""Generate new application"".
Application ID: On the next screen, copy the generated Application ID.
Obtain a Vonage Virtual Number:
You need a phone number associated with your Vonage account to send SMS from.
Navigate to ""Numbers"" > ""Buy numbers"" in the left-hand menu.
Search for numbers by country and features (ensure ""SMS"" is selected).
Choose a number and click ""Buy"". Confirm the purchase.
Navigate to ""Numbers"" > ""Your numbers"". Find the number you just purchased. Copy the number in E.164 format (e.g., +12015550123).
Link Number to Application (Important):
Associate your virtual number with the application you created.
Go back to ""Applications"" and select the application you created (""Node SMS Sender App"").
Scroll down to the ""Linked numbers"" section.
Click ""Link"" next to the virtual number you purchased.
Configure Messages API Settings:
Ensure your account is set to use the Messages API as the default for SMS.
Ensure ""Default SMS Setting"" is set to Messages API. If it's set to ""SMS API"", toggle it and click ""Save changes"".
Update .env File:
Now, open your .env file and replace the placeholders with the actual values you obtained:
.env
# .env
PORT=3000
# Vonage Credentials (Using Application ID & Private Key)
VONAGE_APPLICATION_ID=YOUR_COPIED_APPLICATION_ID # Paste the Application ID here
VONAGE_PRIVATE_KEY_PATH=./private.key # Ensure this path matches where you saved the key file
# Vonage Virtual Number
VONAGE_VIRTUAL_NUMBER=+12015550123 # Paste your rented Vonage number in E.164 format
Environment Variable Explanation:
VONAGE_APPLICATION_ID: What: Unique identifier for your Vonage Application. How: Obtained after creating an application in the Vonage Dashboard. Purpose: Used by the SDK (along with the private key) to authenticate requests to the Messages API via JWT.
VONAGE_PRIVATE_KEY_PATH: What: File path to the private key file. How: Downloaded when creating the Vonage Application. Purpose: Used by the SDK to sign JWTs for authenticating requests. Keep this file secure and do not commit it to version control.
VONAGE_VIRTUAL_NUMBER: What: The Vonage phone number used as the sender ID ('from' number). How: Rented via the Vonage Dashboard (""Numbers"" section). Purpose: Specifies the origin number for the outgoing SMS. Must be in E.164 format.
Security: The private.key file and your .env file contain sensitive credentials. Ensure they are:
Never committed to Git (use .gitignore).
Stored securely in your development and production environments.
Have appropriate file permissions set (restrict read access).
5. Implementing Proper Error Handling, Logging, and Retry Mechanisms
Our current error handling is basic. Let's enhance it.
Error Handling Strategy:
Validation Errors (Client-side): Catch invalid input (missing params, bad number format) early in the API endpoint and return 400 Bad Request.
SDK/Network Errors (Server-side): Catch errors during the vonage.messages.send() call. Log detailed information for debugging. Return 500 Internal Server Error or a more specific 5xx code if appropriate.
Vonage API Errors (Server-side): Inspect the error object returned by the SDK (often in err.response.data) for specific Vonage error codes/messages (e.g., authentication failure, insufficient funds, invalid destination). Log these details and return 500 (or potentially 4xx if the error clearly indicates a client-side issue like an invalid 'to' number rejected by Vonage).
Logging:
console.log and console.error are suitable for development. For production, use a dedicated logging library like Winston or Pino for structured logging, different log levels (debug, info, warn, error), and configurable outputs (file, console, external services).
Example using Winston (Conceptual):
npm install winston
Configure Winston:
JavaScript
// logger.jsconst winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info', // Control log level via env varformat: winston.format.combine(
winston.format.timestamp(),
winston.format.json() // Log in JSON format
),
transports: [
// Log errors to a filenew winston.transports.File({ filename: 'error.log', level: 'error' }),
// Log everything to another filenew winston.transports.File({ filename: 'combined.log' }),
// Log to console (especially in development)new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple() // Simple format for console
)
})
],
});
module.exports = logger;
Replace console.log/console.error with logger.info/logger.error:
JavaScript
// index.js (modified example using logger)const logger = require('./logger'); // Assuming logger.js exists// ... inside /send-sms endpoint error handler
} catch (error) {
// Log more context like stack trace and request details
logger.error(`API Error processing /send-sms: ${error.message}`, { stack: error.stack, requestBody: req.body });
const statusCode = error.message.includes('Invalid') ? 400 : 500;
return res.status(statusCode).json({ /* ... response */ });
}
// ... inside sendSms function catch block
} catch (err) {
// Log error object and any Vonage-specific response data
logger.error(""Error sending SMS via Vonage"", { error: err, vonageResponse: err.response?.data });
// ... throw error
}
Retry Mechanisms:
Network glitches or temporary Vonage issues can cause failures. Implementing retries can improve reliability. Use libraries like async-retry for straightforward implementation with exponential backoff.
Example using async-retry (Conceptual):
npm install async-retry
Wrap the Vonage call in retry:
JavaScript
// index.js (modified sendSms function with retry logic)const retry = require('async-retry');
const logger = require('./logger'); // Assuming logger is setupasyncfunctionsendSms(toNumber, messageText) {
// ... input validation ...const fromNumber = process.env.VONAGE_VIRTUAL_NUMBER;
try {
const resp = awaitretry(async (bail, attempt) => {
// bail is a function to stop retrying (e.g., for non-retryable errors)// attempt is the current attempt number
logger.info(`Attempt ${attempt} to send SMS to ${toNumber}`);
try {
const vonageResponse = await vonage.messages.send({
channel: 'sms',
message_type: 'text',
to: toNumber,
from: fromNumber,
text: messageText,
});
logger.info('SMS submitted successfully:', vonageResponse);
return vonageResponse; // Return successful response
} catch (sdkError) {
// Check for non-retryable errors and call bail()// Examples: Authentication errors, invalid number formats, insufficient fundsif (sdkError.response) {
const status = sdkError.response.status;
const data = sdkError.response.data;
// 401 Unauthorized is definitely non-retryableif (status === 401) {
logger.error(`Non-retryable Vonage API error (Status: 401 - Unauthorized). Bailing.`);
bail(newError(`Vonage Authentication Error: ${data?.title || 'Unauthorized'}`));
return; // Important
}
// Some 400 Bad Requests might be non-retryable (e.g., invalid 'to' format)// Check specific error details if possibleif (status === 400 && data?.title?.includes('Invalid')) {
logger.warn(`Potentially non-retryable Vonage API error (Status: 400 - ${data.title}). Bailing.`);
bail(newError(`Vonage Bad Request Error: ${data?.title} - ${data?.detail || 'Invalid request'}`));
return; // Important
}
// Add checks for other known non-retryable errors (e.g., specific error codes for insufficient funds)// if (data?.code === 'specific-non-retryable-code') { bail(...); return; }
}
// If error seems temporary (network issue, 5xx server error), let retry handle it
logger.warn(`Attempt ${attempt} failed: ${sdkError.message}. Retrying...`);
throw sdkError; // Throw error to trigger retry
}
}, {
retries: 3, // Number of retriesfactor: 2, // Exponential backoff factorminTimeout: 1000, // Minimum time between retries (ms)maxTimeout: 5000, // Maximum time between retries (ms)onRetry: (error, attempt) => {
logger.warn(`Retrying SMS send (Attempt ${attempt}) due to error: ${error.message}`);
}
});
if (resp && resp.message_uuid) {
return resp.message_uuid;
} else {
logger.error(""Unexpected response structure after retries:"", resp);
thrownewError('SMS submission failed after retries or response format unexpected.');
}
} catch (err) {
// This catch block now catches the final error after all retries failed// or if bail() was called
logger.error(""Failed to send SMS after multiple retries:"", { finalError: err });
if (err.response && err.response.data) {
// Throw the specific error message from Vonage if availablethrownewError(`Vonage API Error: ${err.response.data.title || 'Unknown error'} - ${err.response.data.detail || JSON.stringify(err.response.data)}`);
} else {
// Throw the error message passed to bail() or the last retry errorthrownewError(`Failed to send SMS after retries: ${err.message || 'Unknown error'}`);
}
}
}
Testing Error Scenarios:
Provide invalid credentials in .env.
Provide an incorrectly formatted phone number (e.g., 12345, +1 555 123 4567).
Use a valid number but one not whitelisted on a trial account (see Section 11 - Note: Section 11 was mentioned but not provided in the original input, this reference is kept for context).
Temporarily disconnect network connectivity.
Send requests without required parameters (to, text).
Log Analysis: When issues occur, check your configured log output (e.g., error.log, console, or a log aggregation service) for detailed error messages, stack traces, and potentially the Vonage API response body, which often contains specific error codes and descriptions useful for debugging.
6. Creating a Database Schema and Data Layer
For this specific guide (only sending SMS), a database is not strictly required. However, in a real-world application, you would likely want to track sent messages, their status, cost, and potentially link them to users or events in your system.
If Tracking Were Needed:
Schema Design (Example using SQL):
SQL
CREATE TABLE sent_sms_log (
id SERIAL PRIMARY KEY, -- Or UUID
vonage_message_uuid VARCHAR(36) UNIQUE, -- Store the ID from Vonage
client_ref VARCHAR(40), -- Optional client reference sent to Vonage
to_number VARCHAR(20) NOT NULL,
from_number VARCHAR(20) NOT NULL,
message_body TEXT NOT NULL,
status VARCHAR(20) DEFAULT'submitted', -- e.g., submitted, delivered, failed, rejected
status_timestamp TIMESTAMPTZ, -- Timestamp of the last status update (requires webhook)
submission_timestamp TIMESTAMPTZ DEFAULTCURRENT_TIMESTAMP,
error_code VARCHAR(50), -- Store error code if failed
error_message TEXT, -- Store error description if failed
price DECIMAL(10, 5), -- Store cost (requires webhook)
currency VARCHAR(3), -- Store currency (requires webhook)-- Optional: Foreign key to your users table-- user_id INT REFERENCES users(id)
);
-- Index for common lookupsCREATE INDEX idx_sent_sms_log_uuid ON sent_sms_log(vonage_message_uuid);
CREATE INDEX idx_sent_sms_log_to_number ON sent_sms_log(to_number);
CREATE INDEX idx_sent_sms_log_submission_timestamp ON sent_sms_log(submission_timestamp);
Entity Relationship Diagram (Conceptual):
erDiagram
SENT_SMS_LOG {
INT id PK
VARCHAR vonage_message_uuid UK
VARCHAR client_ref
VARCHAR to_number
VARCHAR from_number
TEXT message_body
VARCHAR status
TIMESTAMPTZ status_timestamp
TIMESTAMPTZ submission_timestamp
VARCHAR error_code
TEXT error_message
DECIMAL price
VARCHAR currency
}
Data Access Layer: You would use an ORM (Object-Relational Mapper) like Prisma or Sequelize in Node.js to interact with the database.
Prisma Example (Conceptual schema.prisma):
Prisma
model SentSmsLog {
id Int @id @default(autoincrement())
vonageMessageUuid String? @unique @map(""vonage_message_uuid"")
clientRef String? @map(""client_ref"") @db.VarChar(40)
toNumber String @map(""to_number"") @db.VarChar(20)
fromNumber String @map(""from_number"") @db.VarChar(20)
messageBody String @map(""message_body"") @db.Text
status String @default(""submitted"") @db.VarChar(20)
statusTimestamp DateTime? @map(""status_timestamp"") @db.Timestamptz
submissionTimestamp DateTime @default(now()) @map(""submission_timestamp"") @db.Timestamptz
errorCode String? @map(""error_code"") @db.VarChar(50)
errorMessage String? @map(""error_message"") @db.Text
price Decimal? @db.Decimal(10, 5)
currency String? @db.VarChar(3)
// userId Int? @map(""user_id"")
// user User? @relation(fields: [userId], references: [id])
@@map(""sent_sms_log"")
@@index([vonageMessageUuid])
@@index([toNumber])
@@index([submissionTimestamp])
}
// Example User model if linking
// model User {
// id Int @id @default(autoincrement())
// email String @unique
// smsLogs SentSmsLog[]
// }
Frequently asked questions
Use the Vonage Messages API with the Express.js framework and the @vonage/server-sdk package. This setup allows you to create an API endpoint that accepts recipient numbers and message text for sending SMS messages programmatically within your Node.js application.
The Vonage Messages API enables sending messages via various channels like SMS, MMS, WhatsApp, and more. This tutorial uses it specifically for sending SMS text messages from your Node.js application.
Dotenv securely manages environment variables. It loads credentials like API keys and phone numbers from a .env file, which should never be committed to version control, ensuring sensitive data is protected.
Ngrok is beneficial during development when receiving SMS messages or delivery receipts via webhooks. It creates a publicly accessible tunnel to your local server, allowing Vonage to reach your webhook endpoints.
Create a Vonage Application in the Vonage Dashboard, enable the "Messages" capability, and link your virtual number. Generate public and private keys, saving the private key securely. Set the "Default SMS Setting" to "Messages API" in your Vonage Account Settings.
An E.164 formatted number is the internationally recognized standard for phone numbers and includes a plus sign (+) followed by the country code and national number. Example: +12015550123. Your Vonage virtual number, used for sending SMS, must be in this format.
Use npm install express @vonage/server-sdk dotenv --save. This command installs Express for creating the API, the Vonage Server SDK for interacting with Vonage, and dotenv for managing environment variables.
Receiving SMS messages involves setting up webhooks on a publicly accessible server, which is covered in separate Vonage documentation. This tutorial focuses solely on sending SMS messages.
The private.key file is crucial for authenticating with the Vonage Messages API. It is used by the SDK to digitally sign authentication tokens, and should be kept secure and never committed to version control.
The client_ref is an optional parameter, up to 40 characters, used for your internal tracking. It allows you to correlate messages sent through Vonage with your own records.
You can test the /send-sms endpoint using tools like curl or Postman. Send a POST request with the recipient's phone number ("to") and message text ("text") in the JSON request body.
Winston and Pino are logging libraries for Node.js. They provide structured logging, log levels, and output options, making it easier to manage and analyze log data, especially in production environments.
The async-retry library helps manage network hiccups and temporary API issues by implementing retry logic with features like exponential backoff, ensuring more reliable SMS message delivery.
The message_uuid is a unique identifier returned by the Vonage API for each successfully submitted message. This UUID is crucial for tracking message status and troubleshooting any delivery issues.
This guide provides a complete walkthrough for building a Node.js application using the Express framework to send SMS messages via the Vonage Messages API. We will cover everything from project setup to deployment considerations, enabling you to integrate SMS functionality into your applications effectively.
This tutorial focuses on sending SMS messages. While Vonage also enables receiving messages via webhooks, that functionality requires a publicly accessible server (often set up using tools like ngrok during development) and is detailed in separate Vonage documentation.
Project Overview and Goals
Goal: To create a simple, robust Node.js API endpoint that accepts a recipient phone number and a message text, then uses the Vonage Messages API to send an SMS message.
Problem Solved: This provides a foundational service for applications needing to send programmatic SMS notifications, alerts, verification codes, or other messages to users.
Technologies Used:
@vonage/server-sdk: The official Vonage Server SDK for Node.js, simplifying interactions with the Vonage APIs.dotenv: A zero-dependency module that loads environment variables from a.envfile intoprocess.env, crucial for managing sensitive credentials securely.System Architecture:
Prerequisites:
curlfor testing the API endpoint.Final Outcome: A running Node.js server with a single API endpoint (
/send-sms) that successfully sends an SMS message via Vonage when called with a valid phone number and message.1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
Create Project Directory: Open your terminal or command prompt and create a new directory for your project, then navigate into it.
mkdir vonage-sms-sender cd vonage-sms-senderInitialize Node.js Project: Use
npm initto create apackage.jsonfile. The-yflag accepts default settings.Install Dependencies: Install Express (web framework), the Vonage Server SDK, and
dotenv(for environment variables).express: Handles HTTP requests and routing.@vonage/server-sdk: Interacts with the Vonage API.dotenv: Loads environment variables from a.envfile.--save: Adds these packages to thedependenciessection in yourpackage.json.Create Project Structure: Create the main application file and the environment file.
touch index.js .env .gitignoreindex.js: Will contain our Express server and API logic..env: Will store our sensitive credentials (API keys, secrets, phone numbers). Never commit this file to version control..gitignore: Specifies intentionally untracked files that Git should ignore.Configure
.gitignore: Addnode_modulesand.envto your.gitignorefile to prevent committing them.Configure
.env: Open the.envfile and add the following placeholders. We will fill these in later (Section 4).PORT: The port number our Express server will listen on.VONAGE_APPLICATION_ID: Your Vonage Application ID (obtained from the Vonage Dashboard).VONAGE_PRIVATE_KEY_PATH: The file path to theprivate.keyfile downloaded when creating your Vonage Application. We assume it will be placed in the project root for this example.VONAGE_VIRTUAL_NUMBER: The Vonage phone number you rented, in E.164 format (e.g.,+12015550123).2. Implementing Core Functionality
Now, let's write the core logic in
index.jsto initialize the Vonage SDK and prepare the function to send SMS.// index.js 'use strict'; // 1. Load Environment Variables require('dotenv').config(); // Load variables from .env file into process.env // 2. Import Dependencies const express = require('express'); const { Vonage } = require('@vonage/server-sdk'); // Import Vonage SDK // 3. Initialize Vonage SDK // Ensure required environment variables are present if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH || !process.env.VONAGE_VIRTUAL_NUMBER) { console.error(""Error: Required Vonage environment variables (VONAGE_APPLICATION_ID, VONAGE_PRIVATE_KEY_PATH, VONAGE_VIRTUAL_NUMBER) are not set.""); console.error(""Please check your .env file.""); process.exit(1); // Exit if essential config is missing } const vonage = new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID, privateKey: process.env.VONAGE_PRIVATE_KEY_PATH, }); // 4. Define the SMS Sending Function async function sendSms(toNumber, messageText) { // Validate input format (basic) if (!/^\+[1-9]\d{1,14}$/.test(toNumber)) { // Use standard quotes within the error string throw new Error('Invalid ""to"" number format. Must be E.164 format (e.g., +12125551234)'); } if (!messageText || typeof messageText !== 'string' || messageText.trim().length === 0) { throw new Error('Invalid ""text"" parameter. Message text cannot be empty.'); } const fromNumber = process.env.VONAGE_VIRTUAL_NUMBER; // Use template literal for cleaner logging console.log(`Attempting to send SMS from ${fromNumber} to ${toNumber}`); try { // Use vonage.messages.send() for the Messages API const resp = await vonage.messages.send({ channel: 'sms', message_type: 'text', to: toNumber, from: fromNumber, text: messageText, // Optional: client_ref for your internal tracking (max 40 chars) // client_ref: `my-internal-ref-${Date.now()}` }); console.log('SMS submitted successfully:'); console.log(resp); // Log the full response from Vonage // The Messages API returns a message_uuid for tracking if (resp && resp.message_uuid) { return resp.message_uuid; } else { // This case might indicate an issue before actual sending attempt console.error(""Unexpected response structure from Vonage:"", resp); throw new Error('SMS submission failed or response format unexpected.'); } } catch (err) { console.error(""Error sending SMS via Vonage:""); // Log detailed error if available (e.g., network issue, SDK error) if (err.response && err.response.data) { console.error(""Vonage API Error Details:"", JSON.stringify(err.response.data, null, 2)); // Rethrow a more specific error based on Vonage response if needed throw new Error(`Vonage API Error: ${err.response.data.title || 'Unknown error'} - ${err.response.data.detail || JSON.stringify(err.response.data)}`); } else { console.error(err); // Log the raw error object throw new Error(`Failed to send SMS: ${err.message || 'Unknown error'}`); } } } // --- Express API Setup will go here in the next section --- // Placeholder for exports and starting the server (will be added later)Explanation:
require('dotenv').config()reads the.envfile and makes variables likeprocess.env.VONAGE_APPLICATION_IDavailable. This should be done at the very beginning.expressand theVonageclass from the SDK.Vonageinstance, passing our Application ID and the path to the private key file retrieved fromprocess.env. Basic validation ensures these crucial variables are set.sendSmsFunction:asyncfunction takes the recipient number (toNumber) and the message (messageText) as arguments.toNumber(checking for E.164 format) andmessageText.fromNumberfrom the environment variable.vonage.messages.send(), specifying thechannel('sms'),message_type('text'),to,from, andtext.try...catchblock handles potential errors during the API call (network issues, invalid credentials, Vonage API errors).message_uuid, which is Vonage's identifier for the submitted message.3. Building a Complete API Layer
Now, let's use Express to create an API endpoint that utilizes our
sendSmsfunction.Add the following code to the end of your
index.jsfile:// index.js (continued) // --- Express API Setup --- const app = express(); // Middleware to parse JSON request bodies app.use(express.json()); // Middleware to parse URL-encoded request bodies app.use(express.urlencoded({ extended: true })); // Define the POST endpoint for sending SMS app.post('/send-sms', async (req, res) => { // Extract 'to' and 'text' from the request body const { to, text } = req.body; // Basic input validation for presence if (!to || !text) { // Corrected console error logging console.error('Validation Error: Missing ""to"" or ""text"" in request body'); return res.status(400).json({ success: false, // Corrected JSON string format message: 'Missing required parameters: ""to"" (E.164 format) and ""text"".' }); } try { // Call our core SMS sending function const messageId = await sendSms(to, text); // Send success response console.log(`API successfully processed SMS request. Vonage Message ID: ${messageId}`); return res.status(200).json({ success: true, message: 'SMS submitted successfully.', messageId: messageId // Include the Vonage message_uuid }); } catch (error) { // Handle errors from the sendSms function console.error(`API Error processing /send-sms: ${error.message}`); // Determine appropriate status code (e.g., 400 for validation, 500 for server/Vonage errors) const statusCode = error.message.includes('Invalid') ? 400 : 500; return res.status(statusCode).json({ success: false, message: `Failed to send SMS: ${error.message}` }); } }); // Health check endpoint (Good practice) app.get('/health', (req, res) => { res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() }); }); // Export app and sendSms for testing purposes // Note: Starting the server should typically be in a separate file (e.g., server.js) // for better testability, but we'll keep it here for simplicity in this guide. module.exports = { app, sendSms }; // Start the Express server (only if this file is run directly) if (require.main === module) { const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server listening on http://localhost:${PORT}`); console.log(`SMS Send Endpoint: POST http://localhost:${PORT}/send-sms`); console.log(`Health Check: GET http://localhost:${PORT}/health`); }); }Explanation:
const app = express();creates an Express application.app.use(express.json());enables the server to parse incoming request bodies formatted as JSON.app.use(express.urlencoded({ extended: true }));enables parsing of URL-encoded data (often used by HTML forms)./send-smsEndpoint:/send-smspath.tophone number andtextmessage from the request body (req.body).await sendSms(to, text)function within atry...catchblock.200 OKresponse withsuccess: trueand themessageIdreceived from Vonage.sendSms), it logs the error and returns an appropriate error status code (400for bad input,500for internal/Vonage errors) and a JSON response withsuccess: falseand the error message./healthEndpoint: A simple health check endpoint that returns a200 OKstatus, useful for monitoring.module.exports = { app, sendSms };exports the Expressappinstance and thesendSmsfunction, making them accessible for testing (See Section 13 - Note: Section 13 was mentioned but not provided in the original input, this reference is kept for context).if (require.main === module)block ensuresapp.listen()is only called whenindex.jsis run directly (e.g.,node index.js), not when it's required by another module (like a test file). This starts the server on the specifiedPORT.Testing the Endpoint:
Once your Vonage credentials are set up (Section 4), you can test this endpoint using
curlor Postman.Using
curl:curl -X POST http://localhost:3000/send-sms \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""+12015550124"", ""text"": ""Hello from Node.js and Vonage!"" }'Expected Success Response (JSON):
{ ""success"": true, ""message"": ""SMS submitted successfully."", ""messageId"": ""aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"" }Expected Error Response (JSON - e.g., missing parameter):
{ ""success"": false, ""message"": ""Missing required parameters: \""to\"" (E.164 format) and \""text\""."" }Expected Error Response (JSON - e.g., Vonage API failure):
{ ""success"": false, ""message"": ""Failed to send SMS: Vonage API Error: Unauthorized - Please check your Application ID/private key"" }4. Integrating with Necessary Third-Party Services (Vonage)
This section details how to get the required credentials from Vonage and configure them in your
.envfile.Sign Up/Log In to Vonage: Go to the Vonage API Dashboard and log in or create a new account.
Create a Vonage Application: Applications act as containers for your Vonage settings and credentials.
private.keyfile immediately. You cannot retrieve it later. Place this file in your project directory (or a secure location referenced by the full path in.env).https://example.com/webhooks/inboundandhttps://example.com/webhooks/status. If you later implement receiving SMS or delivery receipts, you'll need valid, publicly accessible URLs here (often using ngrok during development).Obtain a Vonage Virtual Number: You need a phone number associated with your Vonage account to send SMS from.
+12015550123).Link Number to Application (Important): Associate your virtual number with the application you created.
Configure Messages API Settings: Ensure your account is set to use the Messages API as the default for SMS.
Update
.envFile: Now, open your.envfile and replace the placeholders with the actual values you obtained:Environment Variable Explanation:
VONAGE_APPLICATION_ID: What: Unique identifier for your Vonage Application. How: Obtained after creating an application in the Vonage Dashboard. Purpose: Used by the SDK (along with the private key) to authenticate requests to the Messages API via JWT.VONAGE_PRIVATE_KEY_PATH: What: File path to the private key file. How: Downloaded when creating the Vonage Application. Purpose: Used by the SDK to sign JWTs for authenticating requests. Keep this file secure and do not commit it to version control.VONAGE_VIRTUAL_NUMBER: What: The Vonage phone number used as the sender ID ('from' number). How: Rented via the Vonage Dashboard (""Numbers"" section). Purpose: Specifies the origin number for the outgoing SMS. Must be in E.164 format.Security: The
private.keyfile and your.envfile contain sensitive credentials. Ensure they are:.gitignore).5. Implementing Proper Error Handling, Logging, and Retry Mechanisms
Our current error handling is basic. Let's enhance it.
Error Handling Strategy:
400 Bad Request.vonage.messages.send()call. Log detailed information for debugging. Return500 Internal Server Erroror a more specific 5xx code if appropriate.err.response.data) for specific Vonage error codes/messages (e.g., authentication failure, insufficient funds, invalid destination). Log these details and return500(or potentially4xxif the error clearly indicates a client-side issue like an invalid 'to' number rejected by Vonage).Logging:
console.logandconsole.errorare suitable for development. For production, use a dedicated logging library like Winston or Pino for structured logging, different log levels (debug, info, warn, error), and configurable outputs (file, console, external services).Example using Winston (Conceptual):
npm install winston// logger.js const winston = require('winston'); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', // Control log level via env var format: winston.format.combine( winston.format.timestamp(), winston.format.json() // Log in JSON format ), transports: [ // Log errors to a file new winston.transports.File({ filename: 'error.log', level: 'error' }), // Log everything to another file new winston.transports.File({ filename: 'combined.log' }), // Log to console (especially in development) new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() // Simple format for console ) }) ], }); module.exports = logger;console.log/console.errorwithlogger.info/logger.error:// index.js (modified example using logger) const logger = require('./logger'); // Assuming logger.js exists // ... inside /send-sms endpoint error handler } catch (error) { // Log more context like stack trace and request details logger.error(`API Error processing /send-sms: ${error.message}`, { stack: error.stack, requestBody: req.body }); const statusCode = error.message.includes('Invalid') ? 400 : 500; return res.status(statusCode).json({ /* ... response */ }); } // ... inside sendSms function catch block } catch (err) { // Log error object and any Vonage-specific response data logger.error(""Error sending SMS via Vonage"", { error: err, vonageResponse: err.response?.data }); // ... throw error }Retry Mechanisms:
Network glitches or temporary Vonage issues can cause failures. Implementing retries can improve reliability. Use libraries like
async-retryfor straightforward implementation with exponential backoff.Example using
async-retry(Conceptual):npm install async-retryretry:// index.js (modified sendSms function with retry logic) const retry = require('async-retry'); const logger = require('./logger'); // Assuming logger is setup async function sendSms(toNumber, messageText) { // ... input validation ... const fromNumber = process.env.VONAGE_VIRTUAL_NUMBER; try { const resp = await retry(async (bail, attempt) => { // bail is a function to stop retrying (e.g., for non-retryable errors) // attempt is the current attempt number logger.info(`Attempt ${attempt} to send SMS to ${toNumber}`); try { const vonageResponse = await vonage.messages.send({ channel: 'sms', message_type: 'text', to: toNumber, from: fromNumber, text: messageText, }); logger.info('SMS submitted successfully:', vonageResponse); return vonageResponse; // Return successful response } catch (sdkError) { // Check for non-retryable errors and call bail() // Examples: Authentication errors, invalid number formats, insufficient funds if (sdkError.response) { const status = sdkError.response.status; const data = sdkError.response.data; // 401 Unauthorized is definitely non-retryable if (status === 401) { logger.error(`Non-retryable Vonage API error (Status: 401 - Unauthorized). Bailing.`); bail(new Error(`Vonage Authentication Error: ${data?.title || 'Unauthorized'}`)); return; // Important } // Some 400 Bad Requests might be non-retryable (e.g., invalid 'to' format) // Check specific error details if possible if (status === 400 && data?.title?.includes('Invalid')) { logger.warn(`Potentially non-retryable Vonage API error (Status: 400 - ${data.title}). Bailing.`); bail(new Error(`Vonage Bad Request Error: ${data?.title} - ${data?.detail || 'Invalid request'}`)); return; // Important } // Add checks for other known non-retryable errors (e.g., specific error codes for insufficient funds) // if (data?.code === 'specific-non-retryable-code') { bail(...); return; } } // If error seems temporary (network issue, 5xx server error), let retry handle it logger.warn(`Attempt ${attempt} failed: ${sdkError.message}. Retrying...`); throw sdkError; // Throw error to trigger retry } }, { retries: 3, // Number of retries factor: 2, // Exponential backoff factor minTimeout: 1000, // Minimum time between retries (ms) maxTimeout: 5000, // Maximum time between retries (ms) onRetry: (error, attempt) => { logger.warn(`Retrying SMS send (Attempt ${attempt}) due to error: ${error.message}`); } }); if (resp && resp.message_uuid) { return resp.message_uuid; } else { logger.error(""Unexpected response structure after retries:"", resp); throw new Error('SMS submission failed after retries or response format unexpected.'); } } catch (err) { // This catch block now catches the final error after all retries failed // or if bail() was called logger.error(""Failed to send SMS after multiple retries:"", { finalError: err }); if (err.response && err.response.data) { // Throw the specific error message from Vonage if available throw new Error(`Vonage API Error: ${err.response.data.title || 'Unknown error'} - ${err.response.data.detail || JSON.stringify(err.response.data)}`); } else { // Throw the error message passed to bail() or the last retry error throw new Error(`Failed to send SMS after retries: ${err.message || 'Unknown error'}`); } } }Testing Error Scenarios:
.env.12345,+1 555 123 4567).to,text).Log Analysis: When issues occur, check your configured log output (e.g.,
error.log, console, or a log aggregation service) for detailed error messages, stack traces, and potentially the Vonage API response body, which often contains specific error codes and descriptions useful for debugging.6. Creating a Database Schema and Data Layer
For this specific guide (only sending SMS), a database is not strictly required. However, in a real-world application, you would likely want to track sent messages, their status, cost, and potentially link them to users or events in your system.
If Tracking Were Needed:
Schema Design (Example using SQL):
CREATE TABLE sent_sms_log ( id SERIAL PRIMARY KEY, -- Or UUID vonage_message_uuid VARCHAR(36) UNIQUE, -- Store the ID from Vonage client_ref VARCHAR(40), -- Optional client reference sent to Vonage to_number VARCHAR(20) NOT NULL, from_number VARCHAR(20) NOT NULL, message_body TEXT NOT NULL, status VARCHAR(20) DEFAULT 'submitted', -- e.g., submitted, delivered, failed, rejected status_timestamp TIMESTAMPTZ, -- Timestamp of the last status update (requires webhook) submission_timestamp TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, error_code VARCHAR(50), -- Store error code if failed error_message TEXT, -- Store error description if failed price DECIMAL(10, 5), -- Store cost (requires webhook) currency VARCHAR(3), -- Store currency (requires webhook) -- Optional: Foreign key to your users table -- user_id INT REFERENCES users(id) ); -- Index for common lookups CREATE INDEX idx_sent_sms_log_uuid ON sent_sms_log(vonage_message_uuid); CREATE INDEX idx_sent_sms_log_to_number ON sent_sms_log(to_number); CREATE INDEX idx_sent_sms_log_submission_timestamp ON sent_sms_log(submission_timestamp);Entity Relationship Diagram (Conceptual):
Data Access Layer: You would use an ORM (Object-Relational Mapper) like Prisma or Sequelize in Node.js to interact with the database.
schema.prisma):