code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

Send SMS with Node.js, Express, and Vonage

Build a production-ready Node.js application using Express framework to send SMS messages via the Vonage Messages API. Learn project setup, implementation, error handling, security, and deployment.

Send SMS with Node.js, Express, and Vonage

This guide walks you through building a production-ready Node.js application using the Express framework to send SMS messages via the Vonage Messages API. You'll learn project setup, Vonage configuration, implementation, error handling, security, and deployment.

By the end, you'll have a robust API endpoint that accepts requests and sends SMS messages programmatically.

Project Overview and Goals

What You'll Build: A Node.js Express server with a single API endpoint (/send-sms) that accepts a recipient phone number and message text, then uses the Vonage Messages API to send an SMS.

Problem Solved: Enable your applications to send SMS notifications, alerts, verification codes, or other messages to users worldwide.

Technologies Used:

  • Node.js: JavaScript runtime for building server-side applications
  • Express: Minimal, flexible Node.js web framework for creating the API endpoint
  • Vonage Messages API: Powerful API for sending and receiving messages across SMS, MMS, WhatsApp, and other channels (we'll focus on SMS)
  • @vonage/server-sdk: Official Vonage Node.js SDK for interacting with Vonage APIs
  • dotenv: Module for loading environment variables from a .env file into process.env

System Architecture:

text
+-------------+       +---------------------+       +----------------+       +-----------+
| 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): Install on your system. Download Node.js
  • Vonage API Account: Sign up for a free account to start. Sign up for Vonage
  • Vonage Virtual Number: Get at least one Vonage phone number capable of sending SMS from the Vonage dashboard after signing up
  • Text Editor or IDE: Use VS Code or similar
  • Basic Command Line Knowledge: Familiarity with cd, npm, etc.

(Optional but Recommended)

  • Vonage CLI: Manage Vonage applications and numbers via the command line. Install with npm install -g @vonage/cli
  • ngrok: Expose your local server if you plan to extend this to receive SMS messages later. Download ngrok

Expected Outcome: A running Node.js server listening on a specified port (e.g., 3000) with a POST /send-sms endpoint. 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

Create the project structure and install the necessary dependencies.

  1. Create Project Directory: Open your terminal and create a new directory for your project, then navigate into it.

    bash
    mkdir vonage-sms-sender
    cd vonage-sms-sender
  2. Initialize Node.js Project: Create a package.json file to manage dependencies and project metadata.

    bash
    npm init -y
  3. Enable ES Modules: Our index.js example uses import/export syntax, so configure Node.js to treat .js files as ES Modules. Edit your package.json file and add the following top-level key-value pair:

    json
    // 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
    }

    Save the package.json file.

  4. Install Dependencies: Install express for the web server, @vonage/server-sdk to interact with the Vonage API, and dotenv to manage configuration securely.

    bash
    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 your dependencies in package.json
  5. Create Project Files: Create the main application file and a file for environment variables.

    bash
    touch index.js .env .gitignore
  6. Configure .gitignore: Protect sensitive information like API keys and your private key file from being committed. Add the following lines to your .gitignore file:

    text
    # .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
  7. Project Structure: Your project should now look like this:

    text
    vonage-sms-sender/
    ├── .env
    ├── .gitignore
    ├── index.js
    ├── package.json
    ├── package-lock.json
    └── node_modules/

2. Integrating with Vonage

Configure your Vonage account and obtain the necessary credentials. The Messages API requires an Application ID and a private key for authentication.

  1. Sign In to Vonage Dashboard: Access your Vonage API Dashboard.

  2. 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.

  3. 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're using the newer, more versatile Messages API for this guide.
  4. 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 added private.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're 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. The Status URL receives delivery status updates (e.g., delivered, failed).
    • Click Generate new application.
  5. 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).

  6. 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.
  7. Configure Environment Variables: Open the .env file you created earlier and add your Vonage credentials. Replace the placeholder values with your actual credentials.

    dotenv
    # .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 from index.js to your downloaded private.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: Never commit the .env file to version control (like Git). Ensure *.env and private.key are in your .gitignore file. Use platform-specific environment variable management for deployment.

3. Implementing Core Functionality & API Layer

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:

javascript
// 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:

  1. Imports: Import express, the Vonage class and Auth class from the SDK, and dotenv/config to load environment variables immediately.
  2. Initialization: Create an Express application instance and initialize the Vonage client using the Auth class, passing the Application ID and the path to the private key from environment variables.
  3. Middleware: express.json() and express.urlencoded() are essential middleware to parse incoming request bodies in JSON and URL-encoded formats, respectively.
  4. /send-sms Endpoint (POST):
    • This is an async function to allow using await for the asynchronous vonage.messages.send call.
    • It extracts the to (recipient number) and text (message body) from the req.body.
    • Basic Validation: It checks if to and text 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're sending plain text.
      • to: The recipient's phone number (E.164 format like 14155550101 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 a 200 OK JSON response back to the client, including the message_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.
  5. /health Endpoint (GET): A simple endpoint useful for monitoring systems to check if the server is running.
  6. app.listen: Starts the Express server, making it listen for incoming requests on the specified port.

Testing the Endpoint:

  1. Start the Server: In your terminal, run:

    bash
    node index.js

    You should see Server listening at http://localhost:3000.

  2. Send a Request (using curl): Open another terminal window. Replace YOUR_RECIPIENT_NUMBER with a valid phone number (including country code, e.g., 12125551234). Note: If you're using a Vonage trial account, this number must be added to your allowed list in the dashboard (Settings > Test Numbers).

    bash
    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!"
    }'
  3. 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, select raw, and choose JSON from the dropdown.
    • Enter the JSON payload:
      json
      {
        "to": "YOUR_RECIPIENT_NUMBER",
        "text": "Hello from Postman, Node.js, and Vonage!"
      }
    • Click Send.

Expected Responses:

  • Success (200 OK):

    json
    {
      "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):

    json
    {
      "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):

    json
    {
        "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 and console.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):

    javascript
    // 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):

    javascript
    // 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):

    javascript
    // 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 or private.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:

  1. Choose a database (e.g., PostgreSQL, MongoDB, MySQL).
  2. Design a schema (e.g., tables/collections for messages, users, schedules). A messages table might include columns like message_uuid (from Vonage), recipient_number, sender_number, message_text, status (e.g., submitted, delivered, failed), vonage_response, created_at, updated_at.
  3. 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.
  4. Implement data access logic (creating, reading, updating, deleting records) within your service layer or dedicated data access modules.
  5. 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 (like submitted, 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).
  • 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 both to and from 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 or async/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.
  • 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-check VONAGE_APPLICATION_ID and VONAGE_PRIVATE_KEY_PATH in your .env file or environment variables. Ensure the private.key file exists at the specified path relative to where you run node 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 the to number is verified and added to your Vonage dashboard under Settings > Test Numbers. Also, ensure the from number (VONAGE_NUMBER) is correctly linked to your Vonage Application in the dashboard.
  • Invalid Parameters / 400 Bad Request from Vonage: Check the format of the to and from numbers (E.164 recommended: +14155550101). Ensure text is not empty or excessively long. Consult the Vonage Messages API reference for required fields and valid values for message_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: Run npm install again to ensure all dependencies listed in package.json are installed in the node_modules directory. Check for typos in import 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 your package.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 or private.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 set VONAGE_API_KEY, VONAGE_API_SECRET, VONAGE_APPLICATION_ID, VONAGE_NUMBER, and PORT. 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:

    javascript
    // 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: Ensure node_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., a dist 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 Procfile web: node index.js, Docker CMD [""node"", ""index.js""], systemd service) to run node 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:

    1. Deploy the application to a staging or production environment.
    2. Configure all required environment variables securely on the host.
    3. Send a POST request to the deployed /send-sms endpoint using curl, Postman, or another HTTP client. Use a valid recipient number (whitelisted if needed for trial accounts).
    4. Verify you receive a 200 OK response with a success: true and a message_uuid.
    5. Check the recipient phone for the actual SMS message delivery. Note potential delays.
    6. Check the Vonage dashboard logs (API Logs or Messages API Logs) for the message status (submitted, delivered, etc.).
    7. Test error cases: send requests with missing to or text, invalid phone number formats, incorrect authentication (if implemented), etc., and verify you receive appropriate error responses (e.g., 400, 401, 403) with success: 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):

      javascript
      // 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.

Frequently Asked Questions

how to send sms with node.js and express

Use the Vonage Messages API with the @vonage/server-sdk and an Express.js server. Create a /send-sms endpoint that takes the recipient's number and message text, then uses the SDK to send the SMS via the Vonage API. This setup allows your Node.js application to programmatically send SMS messages.

what is the vonage messages api

The Vonage Messages API is a service that allows you to send and receive messages across multiple channels, including SMS, MMS, WhatsApp, and more. It provides a flexible and powerful way to integrate messaging into your applications using various programming languages, including Node.js.

why use dotenv with vonage and express

Dotenv is used for managing environment variables securely. It loads variables from a .env file into process.env, preventing sensitive information like API keys and secrets from being hardcoded in your application and accidentally exposed in version control.

when to use ngrok with vonage sms api

Ngrok is useful when developing locally and you need to expose your local server to the public internet. This is particularly important if you want to receive SMS messages or status updates via webhooks, as Vonage needs a publicly accessible URL to send these requests to.

how to create a vonage application for sms

Log into your Vonage Dashboard, navigate to Applications, and click "Create a new application." Give your application a name, generate public and private keys (store the private key securely), enable the Messages capability, set the Inbound and Status URLs for webhooks, and click "Generate new application." This creates the application and provides you with an Application ID.

what is vonage application id used for

The Vonage Application ID is a unique identifier for your Vonage application. Along with your private key, it is used to authenticate your application with the Vonage Messages API and other Vonage services. This ID is required when initializing the Vonage SDK.

how to link a vonage virtual number

After creating a Vonage Application, go to its configuration page in the dashboard. In the "Link virtual numbers" section, find the number you want to send SMS messages from and click "Link." This associates your Vonage number with your application so you can send messages from it using the Vonage API.

what is vonage private key path

The Vonage Private Key Path specifies the location of your downloaded private.key file on your server. It's used by the @vonage/server-sdk for authentication with the Vonage API and should be stored securely. Never expose this file in version control systems like Git.

how to handle vonage sms api errors

Use a try...catch block around the vonage.messages.send() call to catch potential errors. Provide specific error responses to the user, log detailed error information (including the Vonage API's error response if available) for debugging, and consider implementing retry mechanisms with exponential backoff for transient errors. Use structured JSON for errors where possible.

how to secure node.js vonage sms api

Implement robust input validation using libraries like Joi or express-validator, sanitize user input to prevent injection attacks, use rate limiting to avoid abuse, and consider API key authentication or JWT for internal or protected endpoints. Securely manage Vonage credentials using environment variables and avoid hardcoding them.

how to set up error logging for node.js vonage sms

Use a dedicated logging library like Winston or Pino. Configure different log levels, format logs in JSON for easier parsing and analysis, and output logs to files or external logging services. Also, log unhandled exceptions and promise rejections for complete error tracking.

what are vonage sms character limits

Standard SMS messages are limited to 160 characters when using GSM-7 encoding. Messages with non-GSM characters (emojis, some accented characters) use UCS-2 encoding, and each segment holds around 70 characters. Vonage charges per segment, so long messages are split into multiple segments.

how to check vonage sms delivery status

Set a valid Status URL in your Vonage Application configuration. This URL should point to an endpoint in your application (e.g., /webhooks/status) that can receive delivery receipts (DLRs) from Vonage. Implement logic to process these status updates, typically updating a database or logging the status. Secure the webhook endpoint.

why is my vonage sms not sending trial account

Trial accounts often have restrictions on sending to unverified numbers. Add the recipient's phone number to your allowed list in the Vonage Dashboard under Settings > Test Numbers. Also verify your 'from' number is linked and the default SMS API is set to Messages API, not SMS API.