code examples

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

Send SMS with Plivo, Node.js, and Fastify: Complete Production-Ready Guide

Build a production-ready SMS API with Plivo, Node.js, and Fastify. Learn setup, authentication, error handling, rate limiting, security best practices, and deployment with complete working code examples.

Send SMS with Plivo, Node.js, and Fastify: Complete Production-Ready Guide

Build a Node.js application using the Fastify framework to send SMS messages via the Plivo API. This guide covers project setup, core implementation, error handling, security considerations, and deployment basics.

By the end of this guide, you'll have a functional Fastify API endpoint capable of accepting requests and triggering SMS messages through Plivo, along with the foundational knowledge to expand upon it.

<!-- DEPTH: Introduction lacks concrete use cases showing when/why developers would choose this specific stack over alternatives (Priority: Medium) --> <!-- GAP: Missing estimated time to complete and skill level prerequisites (Type: Substantive) -->

What Will You Build with This Guide?

Goal: Create a simple, robust, and scalable API endpoint using Fastify that sends SMS messages via the Plivo communication platform.

Problem Solved: Build a backend service that abstracts the Plivo SMS sending functionality, enabling other services or frontend applications to send SMS messages easily through a standard REST API call without needing direct access to Plivo credentials or SDKs.

<!-- EXPAND: Could benefit from a comparison table showing why Fastify + Plivo vs. alternatives like Express + Twilio (Type: Enhancement) -->

Technologies:

  • Node.js: A JavaScript runtime environment enabling server-side execution. Required: Node.js v20+ for Fastify v5 (or Node.js v14+ for Fastify v4, supported until June 30, 2025).
  • Fastify: A high-performance, low-overhead web framework for Node.js, chosen for its speed, extensibility, and developer-friendly features like built-in validation. Current versions: Fastify v5 (Node.js v20+) or Fastify v4 (Node.js v14-v22, EOL June 2025).
  • Plivo: A cloud communications platform providing APIs for SMS, voice, and more. You'll use their Node.js SDK (actively maintained on npm, latest stable version available at npmjs.com/package/plivo).
  • dotenv: A zero-dependency module to load environment variables from a .env file into process.env, crucial for managing sensitive credentials securely.
<!-- DEPTH: Technology section lacks quantitative comparisons (e.g., performance benchmarks, pricing considerations) (Priority: Medium) -->

System Architecture:

text
+-------------+        +---------------------+        +--------------+        +-----------------+
| Client (e.g.,| ---->  | Fastify API Server  | ---->  | Plivo Node.js| ---->  | Plivo API       |
| Postman/App)|        | (Node.js)           |        | SDK          |        | (Sends SMS)     |
+-------------+        +---------------------+        +--------------+        +-----------------+
     |                     | Send Request (POST)             | Uses Credentials      | Delivers Message
     |                     | Validate Request                | Constructs API Call   |
     |                     | Call Plivo SDK                  | Handles Response      |
     |<--------------------| Return Response                 |<----------------------| Returns Status
     |                     | (Success/Error)                 | (e.g., Message UUID)  |
     +                     +---------------------------------+-----------------------+

Prerequisites:

  • Node.js and npm (or yarn): Ensure you have Node.js installed. For Fastify v5, install Node.js v20 or later. For Fastify v4, Node.js v14+ works. You can download it from nodejs.org. npm is included with Node.js.
  • Plivo Account: Sign up for a Plivo account at plivo.com.
  • Plivo Credentials: Obtain your Auth ID and Auth Token from the Plivo console dashboard at console.plivo.com/dashboard.
  • Plivo Phone Number: Acquire an SMS-enabled Plivo phone number from the Phone Numbers section of the Plivo console. This will be your sender number (src). Note specific regulations for sender IDs in different countries; a Plivo number is generally required for sending to the US and Canada.
  • Verified Destination Number (Trial Account): If using a Plivo trial account, you must verify any destination phone numbers in the Plivo console under Phone Numbers > Sandbox Numbers before you can send messages to them.
<!-- GAP: Missing information about Plivo trial account limitations (message count, credits, trial duration) (Type: Critical) --> <!-- DEPTH: Prerequisites section lacks troubleshooting steps for common setup issues (Priority: Medium) -->

Final Outcome: A running Fastify server with a single API endpoint (POST /api/sms/send) that accepts a destination number and message text, sends the SMS via Plivo, and returns a confirmation or error.


1. How Do You Set Up Your Project?

Initialize your Node.js project 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 fastify-plivo-sms
    cd fastify-plivo-sms
  2. Initialize Node.js Project: Initialize the project using npm, creating a package.json file. The -y flag accepts default settings.

    bash
    npm init -y
  3. Install Dependencies: Install Fastify, the Plivo Node.js SDK, and dotenv.

    bash
    npm install fastify plivo dotenv

    Version Note: This installs the latest stable versions. As of January 2025, Fastify v5 requires Node.js v20+. If you're using an older Node.js version (v14-v19), install Fastify v4 specifically:

    bash
    npm install fastify@4 plivo dotenv
<!-- GAP: Missing specific version numbers for package installations and compatibility notes (Type: Substantive) -->
  1. Set Up Project Structure: Create a basic structure for organization.

    bash
    mkdir src
    mkdir src/routes
    mkdir src/config
    touch src/server.js
    touch src/routes/sms.js
    touch src/config/plivo.js
    touch .env
    touch .gitignore
    • src/server.js: Main application entry point, Fastify server setup.
    • src/routes/sms.js: Defines the routes related to SMS functionality.
    • src/config/plivo.js: Initializes and configures the Plivo client.
    • .env: Stores sensitive credentials (Plivo Auth ID, Token, Number). Never commit this file to version control.
    • .gitignore: Specifies intentionally untracked files that Git should ignore.
<!-- EXPAND: Could add a visual directory tree diagram for better clarity (Type: Enhancement) -->
  1. Configure .gitignore: Add node_modules and .env to your .gitignore file to prevent them from being checked into Git.

    .gitignore

    text
    node_modules/
    .env
    *.log
  2. Configure Environment Variables (.env): Open the .env file and add your Plivo credentials and phone number. Replace the placeholder values with your actual credentials found in the Plivo console.

    .env

    env
    # Plivo Credentials - Found on your Plivo Console Dashboard (console.plivo.com/dashboard/)
    PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID
    PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN
    
    # Plivo Phone Number - An SMS-enabled number from your Plivo account
    PLIVO_SENDER_NUMBER=+1XXXXXXXXXX # Use E.164 format (e.g., +12025551234)
    • PLIVO_AUTH_ID: Your Plivo account identifier.
    • PLIVO_AUTH_TOKEN: Your Plivo account secret token.
    • PLIVO_SENDER_NUMBER: The SMS-enabled Plivo phone number you purchased, in E.164 format (+ followed by country code and number, max 15 digits total per ITU-T E.164 standard).
<!-- DEPTH: Environment variable section lacks validation steps to verify credentials are correct before proceeding (Priority: High) -->

2. How Do You Implement SMS Sending?

Write the code to initialize the Plivo client and set up the Fastify server.

  1. Configure Plivo Client: Set up the Plivo client instance using the environment variables.

    src/config/plivo.js

    javascript
    'use strict';
    
    const plivo = require('plivo');
    require('dotenv').config(); // Load .env variables
    
    // Ensure essential environment variables are set
    if (!process.env.PLIVO_AUTH_ID || !process.env.PLIVO_AUTH_TOKEN) {
        console.error('Error: Plivo Auth ID or Auth Token not found in environment variables.');
        console.error('Please check your .env file or environment configuration.');
        process.exit(1); // Exit if critical configuration is missing
    }
    
    // Initialize Plivo client
    const client = new plivo.Client(
        process.env.PLIVO_AUTH_ID,
        process.env.PLIVO_AUTH_TOKEN
    );
    
    module.exports = client; // Export the configured client instance
    • Why dotenv here? Load environment variables early to ensure the Plivo client can be initialized correctly.
    • Why the check? Explicitly checking for credentials prevents the application from starting in an invalid state and provides clear error messages.
<!-- GAP: Missing validation to test if credentials are valid by making a test API call (Type: Substantive) -->
  1. Create the SMS Sending Route: Define the Fastify route that will handle the SMS sending request.

    src/routes/sms.js

    javascript
    'use strict';
    
    const plivoClient = require('../config/plivo'); // Import the configured Plivo client
    require('dotenv').config(); // Ensure env vars are loaded
    
    // Define the schema for request validation
    // Ensures 'to' and 'text' are provided and are strings
    const sendSmsSchema = {
      body: {
        type: 'object',
        required: ['to', 'text'],
        properties: {
          to: {
            type: 'string',
            description: 'Destination phone number in E.164 format (e.g., +15551234567). Max 15 digits per ITU-T E.164 standard.',
            pattern: '^\\+[1-9]\\d{1,14}$' // E.164 format: + followed by 1-14 digits (country code 1-3 digits + subscriber number max 12 digits)
          },
          text: {
            type: 'string',
            description: 'The content of the SMS message. Max 160 chars (GSM-7) or 70 chars (Unicode) per segment.',
            minLength: 1,
            maxLength: 1600 // Reasonable limit for concatenated messages (10 segments)
          },
        },
      },
      response: {
        200: {
          type: 'object',
          properties: {
            message: { type: 'string' },
            message_uuid: { type: 'array', items: { type: 'string' } }, // Plivo API returns message_uuid as an array
            api_id: { type: 'string' }, // Plivo also returns api_id
          },
        },
        // Define other response schemas (e.g., 400, 500) as needed
      },
    };
    
    async function smsRoutes(fastify, options) {
      // Ensure the sender number is available
      const senderNumber = process.env.PLIVO_SENDER_NUMBER;
      if (!senderNumber) {
          fastify.log.error('PLIVO_SENDER_NUMBER is not set in environment variables.');
          // Optionally, throw an error during setup if this is critical
          // throw new Error('PLIVO_SENDER_NUMBER is required.');
      }
    
      fastify.post('/sms/send', { schema: sendSmsSchema }, async (request, reply) => {
        // Check again in handler in case it wasn't critical at setup
        if (!senderNumber) {
            reply.code(500).send({ error: 'Server configuration error: Sender number not set.' });
            return;
        }
    
        const { to, text } = request.body;
    
        try {
          fastify.log.info(`Attempting to send SMS to ${to} from ${senderNumber}`);
    
          const response = await plivoClient.messages.create(
            senderNumber, // Source number (from .env)
            to,           // Destination number (from request body)
            text          // Message text (from request body)
          );
    
          fastify.log.info(`SMS sent successfully. Message UUID: ${response.messageUuid}, API ID: ${response.apiId}`);
    
          // Send success response back to the client
          reply.code(200).send({
            message: 'SMS sent successfully!',
            message_uuid: response.messageUuid, // Plivo returns this as an array
            api_id: response.apiId, // Include API ID for tracking
          });
    
        } catch (error) {
          fastify.log.error(`Error sending SMS via Plivo: ${error.message}`);
          fastify.log.error(error.stack); // Log the full stack trace for debugging
    
          // Determine appropriate error code based on Plivo error or internal issue
          // Map Plivo-specific errors to appropriate HTTP status codes
          let statusCode = 500;
          let errorMessage = 'Failed to send SMS due to an internal server error.';
    
          if (error.statusCode) {
            // Use Plivo's status code if available
            statusCode = error.statusCode >= 500 ? 502 : error.statusCode; // Treat Plivo 5xx as 502 Bad Gateway
            errorMessage = `Failed to send SMS. Plivo API error: ${error.message}`;
          }
    
          // Handle specific Plivo error cases
          if (error.message && error.message.includes('rate limit')) {
            statusCode = 429; // Too Many Requests
            errorMessage = 'Rate limit exceeded. Please retry after a delay.';
          }
    
          reply.code(statusCode).send({
            error: errorMessage,
            details: error.message,
            // Include error code if available from Plivo
            ...(error.errorCode && { error_code: error.errorCode })
          });
        }
      });
    }
    
    module.exports = smsRoutes;
    • Why Fastify Schema? Fastify's built-in schema validation automatically validates incoming request bodies and serializes responses, reducing boilerplate code and improving security/reliability. The E.164 regex pattern validates phone number format according to ITU-T E.164 standard.
    • Why async/await? Simplifies handling asynchronous operations like the Plivo API call.
    • Why E.164 Pattern? The regex ^\\+[1-9]\\d{1,14}$ enforces ITU-T E.164 format: + sign, followed by 1-14 digits (country code 1-3 digits + subscriber number max 12 digits), total max 15 digits.
    • Why try...catch? Essential for handling potential errors during the API call to Plivo (network issues, invalid credentials, API errors, rate limits, etc.).
    • Rate Limit Handling: Plivo enforces rate limits (default 5 messages/second per account, 100 concurrent requests for MMS). The error handler now specifically catches rate limit errors and returns HTTP 429.
<!-- DEPTH: Code lacks inline comments explaining Plivo SDK response structure and fields (Priority: Medium) --> <!-- GAP: Missing explanation of what message_uuid can be used for (tracking, webhooks, status checks) (Type: Substantive) -->
  1. Set Up the Fastify Server: Configure and start the Fastify server, registering the SMS routes.

    src/server.js

    javascript
    'use strict';
    
    require('dotenv').config(); // Load environment variables early
    
    // Instantiate Fastify with basic logging enabled
    // For Fastify v5, ensure Node.js v20+ is installed
    const fastify = require('fastify')({
      logger: true, // Enable Fastify's built-in Pino logger
    });
    
    // Register the SMS routes
    // The prefix option namespaces all routes defined in smsRoutes under '/api'
    fastify.register(require('./routes/sms'), { prefix: '/api' });
    
    // Basic health check route
    fastify.get('/health', async (request, reply) => {
      return { status: 'ok', timestamp: new Date().toISOString() };
    });
    
    // Define the start function
    const start = async () => {
      try {
        const port = process.env.PORT || 3000;
        await fastify.listen({ port: port, host: '0.0.0.0' }); // Listen on all available network interfaces
        fastify.log.info(`Server listening on port ${port}`);
      } catch (err) {
        fastify.log.error(err);
        process.exit(1);
      }
    };
    
    // Start the server
    start();
    • Why logger: true? Enables request/response logging and allows using fastify.log for custom logs.
    • Why register with prefix? Organizes API routes under a common base path (e.g., /api/sms/send).
    • Why 0.0.0.0? Makes the server accessible from outside its container/machine in development or production (localhost/127.0.0.1 only allows local connections).
    • Why Health Check? A simple /health endpoint is standard practice for monitoring services and load balancers.
    • Fastify v5 Note: If you're using Fastify v5, ensure you're running Node.js v20 or later. For Fastify v4 (supported until June 30, 2025), Node.js v14+ is sufficient.
<!-- EXPAND: Could add graceful shutdown handling example with SIGTERM/SIGINT (Type: Enhancement) --> <!-- GAP: Missing configuration for production logger settings (log levels, redaction) (Type: Substantive) -->
  1. Add Start Script to package.json: Add a script to easily start your server.

    package.json (additions)

    json
    {
      // ... other package.json content ...
      "scripts": {
        "start": "node src/server.js",
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      // ... other package.json content ...
    }
<!-- EXPAND: Could add development script with nodemon for auto-restart (Type: Enhancement) -->

3. How Do You Build a Complete API Layer?

Your core API endpoint (POST /api/sms/send) is already set up. Here's how to document and test it.

API Endpoint Documentation:

  • Method: POST
  • Path: /api/sms/send
  • Body (JSON):
    • to (string, required): The destination phone number in E.164 format (e.g., +12025551234). Must match ITU-T E.164 standard: + followed by country code (1-3 digits) and subscriber number (max 12 digits), total max 15 digits.
    • text (string, required): The content of the SMS message. Max 160 characters for GSM-7 encoding (standard characters) or 70 characters for UCS-2/Unicode encoding (special characters, emoji). Messages exceeding these limits are automatically split into multiple segments by Plivo.
  • Success Response (200 OK):
    • message (string): Confirmation message (e.g., "SMS sent successfully!").
    • message_uuid (array of strings): An array containing the unique identifier(s) assigned by Plivo to the message request.
    • api_id (string): Plivo's API request identifier for tracking.
    • Example:
      json
      {
        "message": "SMS sent successfully!",
        "message_uuid": ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"],
        "api_id": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
      }
  • Error Responses:
    • 400 Bad Request: Invalid input (missing fields, invalid phone number format, message too long).
      json
      {
        "statusCode": 400,
        "code": "FST_ERR_VALIDATION",
        "error": "Bad Request",
        "message": "body/to must match pattern \"^\\+[1-9]\\d{1,14}$\""
      }
    • 429 Too Many Requests: Plivo rate limit exceeded (default 5 messages/second per account).
      json
      {
        "error": "Rate limit exceeded. Please retry after a delay.",
        "details": "Plivo API rate limit error message"
      }
    • 500 Internal Server Error / 502 Bad Gateway: Server configuration issue or error communicating with Plivo.
      json
      {
        "error": "Failed to send SMS due to an internal server error.",
        "details": "Specific error message from Plivo or internal logic"
      }
<!-- EXPAND: Could add OpenAPI/Swagger specification for API documentation (Type: Enhancement) --> <!-- GAP: Missing authentication/authorization headers documentation for production (Type: Critical) -->

Testing with curl:

  1. Start the Server:

    bash
    npm start

    You should see output indicating the server is listening on port 3000 (or your configured PORT).

  2. Send a Request: Open another terminal window and run the following curl command. Replace +1DESTINATIONNUMBER with a verified phone number (if using a trial account) in E.164 format.

    bash
    curl -X POST http://localhost:3000/api/sms/send \
         -H "Content-Type: application/json" \
         -d '{
               "to": "+1DESTINATIONNUMBER",
               "text": "Hello from Fastify and Plivo!"
             }'
  3. Check Response: You should receive a JSON response like the success example above, and the destination phone should receive the SMS shortly after. Check your server logs (npm start terminal) for detailed request/response information and potential errors.

<!-- EXPAND: Could add Postman collection example or testing alternatives (Type: Enhancement) --> <!-- DEPTH: Testing section lacks expected timing (how long should SMS delivery take?) (Priority: Low) -->

4. Integrating with Necessary Third-Party Services (Plivo)

We've already integrated Plivo, but let's reiterate the key configuration steps.

  1. Obtain Credentials:
    • Navigate to your Plivo Console: https://console.plivo.com/
    • Auth ID & Auth Token: These are prominently displayed on the main dashboard page after logging in.
    • Plivo Phone Number: Go to Phone Numbers > Your Numbers. If you don't have one, click Buy Number, search for SMS-enabled numbers in your desired region, and purchase one. Copy the number in E.164 format (+ followed by country code and number, max 15 digits).
    • (Trial Account) Sandbox Numbers: Go to Phone Numbers > Sandbox Numbers to add and verify destination numbers you want to send messages to during trial.
<!-- GAP: Missing screenshot or step-by-step visual guide for finding credentials in Plivo console (Type: Substantive) --> <!-- DEPTH: No information about Plivo phone number costs or pricing tiers (Priority: Medium) -->
  1. Secure Credential Storage:

    • Place your Auth ID, Auth Token, and Plivo Sender Number into the .env file in the project root.
    • Crucially: Ensure .env is listed in your .gitignore file to prevent accidentally committing secrets to version control.
  2. Environment Variables Explained:

    .env

    env
    # Plivo Credentials - Found on your Plivo Console Dashboard (console.plivo.com/dashboard/)
    PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID # Purpose: Identifies your account to Plivo. Format: String. How to get: Plivo Dashboard.
    PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN # Purpose: Authenticates your API requests. Format: String (Secret). How to get: Plivo Dashboard.
    
    # Plivo Phone Number - An SMS-enabled number from your Plivo account
    PLIVO_SENDER_NUMBER=+1XXXXXXXXXX # Purpose: The 'From' number for outgoing SMS. Format: E.164 String (max 15 digits: + + 1-3 digit country code + max 12 digit subscriber number). How to get: Plivo Console > Phone Numbers > Your Numbers.
  3. Rate Limiting and Message Queuing: Plivo implements intelligent rate limiting and message queueing:

    • Default Limits: 5 messages/second per account for SMS; 100 concurrent API requests for MMS.
    • Smart Queue: You can send requests to Plivo at any rate. Messages are queued and throttled automatically based on account-level configuration and destination country regulations.
    • Scaling: To increase throughput, distribute traffic across multiple Plivo numbers to stay within per-number rate limits.
    • Error Handling: Implement exponential backoff when receiving HTTP 429 (rate limit) errors.
<!-- DEPTH: Rate limiting section lacks practical examples of how to handle bulk sending scenarios (Priority: High) --> <!-- GAP: Missing information about how to request rate limit increases from Plivo (Type: Substantive) -->
  1. Fallback Mechanisms (Conceptual): For this basic guide, we don't implement automatic fallback. In a production system, if Plivo experiences downtime, you might consider:
    • Retry Logic: Implement retries with exponential backoff (covered briefly in Error Handling).
    • Circuit Breaker Pattern: Prevent repeated calls to a failing service.
    • Alternative Provider: Integrate a secondary SMS provider and switch if Plivo fails (adds significant complexity).
<!-- EXPAND: Could add code example for circuit breaker pattern implementation (Type: Enhancement) -->

5. Implementing Error Handling, Logging, and Retry Mechanisms

We've incorporated comprehensive error handling and logging throughout the application.

  • Error Handling Strategy: Use try...catch blocks around external API calls (Plivo). Catch errors, log them with details (fastify.log.error), and return appropriate HTTP status codes (4xx for client errors, 5xx for server/Plivo errors, 429 for rate limits) and informative JSON error messages to the client. Map Plivo errors to relevant HTTP statuses where possible.

  • Logging: Fastify's built-in Pino logger (logger: true) provides efficient JSON-based logging for requests, responses, and errors. Custom logs (fastify.log.info, fastify.log.error) add context. In production, logs should be collected and forwarded to a centralized logging system (e.g., Datadog, ELK stack, Splunk).

<!-- GAP: Missing structured logging best practices and sensitive data redaction guidelines (Type: Critical) -->
  • Plivo-Specific Error Handling:
    • Authentication Errors (401): Invalid Auth ID or Auth Token. Verify credentials in .env.
    • Invalid Parameters (400): Invalid phone number format, missing required fields. Check E.164 format compliance.
    • Rate Limit Errors (429): Exceeded Plivo's rate limits (5 msg/sec default). Implement exponential backoff and retry logic.
    • Insufficient Credits: Account has insufficient balance. Top up account.
    • Network Errors (ENOTFOUND, ETIMEDOUT): Connectivity issues. Check network, firewalls, and Plivo's service status.
<!-- DEPTH: Error handling lacks complete list of all possible Plivo error codes and their meanings (Priority: Medium) -->
  • Retry Mechanisms (Conceptual): The Plivo Node.js SDK does not have built-in automatic retries for sending messages upon failure. Implementing robust retries requires careful consideration:
    • Identify Retryable Errors: Only retry on transient errors (e.g., network timeouts, Plivo 5xx errors, rate limits - 429), not permanent ones (e.g., invalid credentials - 401, invalid number format - 400).
    • Exponential Backoff: Increase the delay between retries exponentially (e.g., 1s, 2s, 4s, 8s, 16s) to avoid overwhelming the Plivo API during intermittent issues or rate limiting.
    • Max Retries: Limit the number of retries to prevent indefinite loops (e.g., 3-5 retries maximum).
    • Idempotency: Ensure retrying a request doesn't result in duplicate messages. Plivo handles some level of idempotency, but designing your system carefully is still important.
    • Implementation: You could use libraries like async-retry or p-retry, or build custom logic within your catch block. For this guide's scope, we only log the error and return appropriate status codes.

Example Retry Logic with Exponential Backoff (Conceptual):

javascript
const pRetry = require('p-retry');

async function sendSmsWithRetry(senderNumber, to, text) {
  return pRetry(
    async () => {
      return await plivoClient.messages.create(senderNumber, to, text);
    },
    {
      retries: 3,
      onFailedAttempt: (error) => {
        fastify.log.warn(`SMS send attempt ${error.attemptNumber} failed. ${error.retriesLeft} retries left.`);
      },
      // Only retry on specific errors
      shouldRetry: (error) => {
        return error.statusCode >= 500 || error.statusCode === 429; // Retry on 5xx or rate limits
      },
    }
  );
}
<!-- EXPAND: Should include working code example integrated into the route handler (Type: Enhancement) -->

Testing Error Scenarios:

  • Invalid Input: Send a request missing the to or text field, or with an invalid phone number format (e.g., 12345). Expect a 400 error from Fastify validation.
  • Invalid Credentials: Temporarily change PLIVO_AUTH_ID or PLIVO_AUTH_TOKEN in .env and restart the server. Send a valid request. Expect a 401 Unauthorized error from the Plivo API.
  • Invalid Sender Number: Use a number not associated with your Plivo account in PLIVO_SENDER_NUMBER. Expect an error from Plivo.
  • Trial Account Limit: Send to an unverified number using a trial account. Expect a specific Plivo error related to sandbox restrictions.
  • Rate Limit: Send rapid successive requests exceeding 5 messages/second. Expect HTTP 429 responses.
<!-- DEPTH: Testing section lacks automation script or test suite examples (Priority: Medium) -->

6. Creating a Database Schema and Data Layer

Not Applicable: This specific microservice focuses solely on the stateless action of sending an SMS via an external API. It does not require its own database to store state related to the messages (Plivo handles the message persistence and status tracking).

If you needed to track requests, log message attempts internally, or associate messages with users in your system, you would add a database (e.g., PostgreSQL, MongoDB) and a data access layer (e.g., using an ORM like Prisma or Sequelize, or a MongoDB driver).

<!-- EXPAND: Could add example schema design for audit logging of SMS sends (Type: Enhancement) --> <!-- GAP: Missing discussion of webhook implementation for delivery status tracking (Type: Substantive) -->

7. Adding Security Features

  • Input Validation: Implemented using Fastify's schema validation (sendSmsSchema). This prevents malformed requests and basic injection attempts in the to and text fields. The E.164 regex pattern (^\\+[1-9]\\d{1,14}$) enforces ITU-T E.164 standard format. Sanitize or strictly validate any user input that might be used in logs or other contexts.

  • Secure Credential Handling: Using .env and .gitignore prevents exposure of Plivo API keys. In production, use environment variables provided by the deployment platform or a dedicated secrets management system (e.g., HashiCorp Vault, AWS Secrets Manager, Azure Key Vault).

<!-- GAP: Missing explanation of secrets rotation best practices (Type: Substantive) -->
  • Rate Limiting: Protect against brute-force attacks or abuse by limiting the number of requests per IP address or user. Use the @fastify/rate-limit plugin (formerly fastify-rate-limit).

    1. Install: npm install @fastify/rate-limit
    2. Register in server.js:
      javascript
      // src/server.js
      // ... other requires ...
      const rateLimit = require('@fastify/rate-limit');
      
      // ... fastify instantiation ...
      
      // Register rate limiting plugin BEFORE your routes
      fastify.register(rateLimit, {
        max: 100, // Max requests per window
        timeWindow: '1 minute', // Time window
        errorResponseBuilder: (request, context) => {
          return {
            statusCode: 429,
            error: 'Too Many Requests',
            message: `Rate limit exceeded. You can make ${context.max} requests per ${context.after}. Retry after ${context.ttl}ms.`
          };
        }
      });
      
      // Register the SMS routes AFTER rate limiting
      fastify.register(require('./routes/sms'), { prefix: '/api' });
      
      // ... rest of server.js ...
<!-- DEPTH: Rate limiting example lacks guidance on choosing appropriate limits based on use case (Priority: Medium) -->
  • Security Headers: Use a plugin like @fastify/helmet to set various HTTP headers (like X-Frame-Options, Strict-Transport-Security, X-Content-Type-Options, etc.) to protect against common web vulnerabilities (XSS, clickjacking, etc.).

    1. Install: npm install @fastify/helmet
    2. Register in server.js:
      javascript
      // src/server.js
      // ... other requires ...
      const helmet = require('@fastify/helmet');
      
      // ... fastify instantiation ...
      
      // Register helmet BEFORE routes
      fastify.register(helmet);
      
      // ... register rate limit, routes, etc. ...
  • HTTPS: Always run your API over HTTPS in production using a reverse proxy like Nginx, Caddy, or your cloud provider's load balancer to handle SSL/TLS termination. Never expose plain HTTP APIs to the internet in production.

  • API Key Authentication (Production Enhancement): For production, add API key authentication to your /api/sms/send endpoint to prevent unauthorized usage. Store API keys securely and validate them in a Fastify hook before processing requests.

<!-- GAP: Missing complete working example of API key authentication implementation (Type: Critical) --> <!-- EXPAND: Could add JWT authentication example as an alternative (Type: Enhancement) -->

8. Handling Special Cases Relevant to the Domain

  • E.164 Format: Strictly enforce the E.164 format for phone numbers as required by Plivo. The ITU-T E.164 standard specifies: + sign, followed by country code (1-3 digits), followed by subscriber number (max 12 digits), total maximum 15 digits. Our schema validation uses the regex ^\\+[1-9]\\d{1,14}$ to enforce this format.

  • Character Encoding (GSM-7 vs. UCS-2/Unicode):

    • GSM-7 Encoding: Standard SMS uses the GSM-7 character set. A single segment contains 160 characters.
    • UCS-2/Unicode Encoding: Using characters outside the GSM-7 set (like many emojis, accented characters, non-Latin scripts) automatically switches the message to UCS-2 encoding, which limits segments to 70 characters.
    • Plivo handles this automatically, but be aware that messages containing even one Unicode character will be shorter per segment and may cost more if they split into more parts.
<!-- EXPAND: Could add table showing which characters are GSM-7 vs requiring Unicode (Type: Enhancement) --> <!-- DEPTH: Missing guidance on how to detect character encoding before sending to warn users (Priority: Medium) -->
  • Message Concatenation (Multipart SMS):
    • If a message exceeds the character limit for a single segment (160 for GSM-7, 70 for UCS-2), Plivo automatically splits it into multiple linked segments (concatenated SMS).
    • Header Overhead: Concatenated messages include a small header (6-7 bytes), reducing available characters per segment to 153 characters for GSM-7 or 67 characters for UCS-2.
    • Each segment consumes SMS credits. A long message can result in multiple billed messages. The Plivo SDK and API handle the splitting and concatenation headers automatically.
<!-- GAP: Missing cost calculator or formula for estimating message costs based on length (Type: Substantive) -->
  • Sender ID Restrictions:
    • US/Canada: Generally require using a Plivo long code (10-digit US number) or Toll-Free number as the sender ID (src). Alphanumeric sender IDs are often blocked by carriers.
    • Other Countries: Rules vary significantly. Some countries allow alphanumeric sender IDs (e.g., MyCompany), which may need pre-registration with carriers or regulatory bodies. Check Plivo's country-specific documentation for sender ID regulations.
    • Our code uses the PLIVO_SENDER_NUMBER environment variable, which should be set to a Plivo-provisioned phone number in E.164 format for US/Canada.
<!-- DEPTH: Sender ID section lacks comprehensive country-by-country breakdown of regulations (Priority: Low) --> <!-- EXPAND: Could add link to Plivo's country-specific SMS documentation (Type: Enhancement) -->
  • Rate Limits (Plivo):
    • Account-Level Default: 5 messages per second per account (as of 2025).
    • MMS Concurrent Requests: 100 simultaneous API requests for MMS.
    • Smart Queue: Plivo's intelligent queueing system manages delivery throttling based on destination country regulations and carrier requirements.
    • Scaling Strategy: To exceed single-number throughput limits, distribute traffic across multiple Plivo phone numbers.
    • Sending too rapidly can result in HTTP 429 (Too Many Requests) errors. Implement client-side throttling, queueing, or exponential backoff when handling bulk messages.

9. Implementing Performance Optimizations

  • Fastify Choice: Using Fastify already provides a high-performance foundation due to its low overhead and efficient request lifecycle. Fastify can serve 70,000+ requests/second in benchmarks (as of Fastify v4).

  • Asynchronous Operations: Using async/await ensures Node.js's event loop is not blocked during the Plivo API call, allowing the server to handle concurrent requests efficiently.

  • Logging: Pino (Fastify's default logger) is designed for high performance with minimal overhead. Avoid excessive or synchronous logging in hot paths.

  • Caching: Not directly applicable for the action of sending a unique SMS. Caching might be relevant if you frequently look up user data before sending, but not for the Plivo API call itself.

  • Connection Pooling: The Plivo Node.js SDK uses the underlying HTTP client (typically Node.js https module) which manages connection pooling automatically. Ensure keep-alive is enabled (default in Node.js).

  • Load Testing: Use tools like k6, autocannon, or wrk to simulate traffic and identify bottlenecks under load. Monitor CPU, memory, and event loop lag during tests.

    bash
    # Example using autocannon (install with: npm install -g autocannon)
    autocannon -m POST -H "Content-Type=application/json" -b '{"to":"+1DESTINATIONNUMBER", "text":"Load test message"}' http://localhost:3000/api/sms/send
  • Profiling: Use Node.js built-in profiler (node --prof) or tools like 0x, clinic.js, or Chrome DevTools to analyze CPU usage and identify performance bottlenecks within your code.

<!-- DEPTH: Performance section lacks actual benchmark results and performance targets (Priority: Medium) --> <!-- GAP: Missing discussion of horizontal scaling strategies (load balancing, clustering) (Type: Substantive) -->

10. Adding Monitoring, Observability, and Analytics

  • Health Checks: The /health endpoint provides a basic mechanism for load balancers or monitoring systems (like UptimeRobot, Nagios, Datadog Synthetics) to check if the service is running.
<!-- GAP: Health check endpoint should verify Plivo connectivity, not just server status (Type: Critical) -->
  • Performance Metrics: Instrument your application to track key metrics:
    • Request latency (Fastify can help).
    • Request rate (requests per second).
    • Error rates (specifically Plivo API errors vs. internal errors).
    • Node.js process metrics (CPU, memory, event loop lag).
    • Use libraries like prom-client for Prometheus integration or platform-specific agents (Datadog, New Relic).
<!-- EXPAND: Could add complete Prometheus metrics integration example (Type: Enhancement) -->
  • Error Tracking: Integrate with services like Sentry, Bugsnag, or Datadog APM to capture, aggregate, and alert on application errors in real-time, providing more context than just logs.

  • Structured Logging: The JSON logs produced by Pino are easily parseable by log aggregation platforms (ELK, Splunk, Datadog Logs, etc.), enabling searching, filtering, and dashboard creation based on log data (e.g., tracking SMS send rates, error types).

  • Dashboards: Create dashboards in your monitoring/logging tool to visualize key metrics: SMS send volume over time, error rates, API latency percentiles, health check status.

  • Alerting: Configure alerts based on critical thresholds:

    • High error rate (e.g., >5% Plivo API errors).
    • Increased latency (e.g., p95 latency > 1s).
    • Health check failures.
    • Server resource exhaustion (high CPU/memory).
<!-- DEPTH: Monitoring section lacks specific alert thresholds and SLA recommendations (Priority: Medium) -->

11. Troubleshooting and Caveats

  • Common Errors & Solutions:
    • Error: Plivo Auth ID or Auth Token not found...: Ensure PLIVO_AUTH_ID and PLIVO_AUTH_TOKEN are correctly set in your .env file and the file is being loaded (require('dotenv').config();).
    • 401 Unauthorized (from Plivo): Incorrect Auth ID or Auth Token. Double-check values in .env against the Plivo console.
    • Invalid 'src' parameter / Invalid 'dst' parameter (from Plivo): Phone numbers are likely not in E.164 format or are otherwise invalid. Ensure numbers start with + and country code, max 15 digits total. Check PLIVO_SENDER_NUMBER in .env and the to number in the request.
    • Message Sending disallowed to Sandbox Number (from Plivo): You are using a trial account and trying to send to a number not verified in Sandbox Numbers on the Plivo console. Add and verify the destination number.
    • Message Sender 'src' number not owned by you or is invalid (from Plivo): The PLIVO_SENDER_NUMBER in .env is incorrect or not associated with your Plivo account.
    • Fastify FST_ERR_VALIDATION (400 Bad Request): The request body doesn't match the schema defined in src/routes/sms.js (e.g., missing to or text, or they don't match the pattern/type requirements).
    • ENOTFOUND / ETIMEDOUT: Network issues connecting from your server to the Plivo API. Check server connectivity, firewalls, DNS resolution, and Plivo's service status page.
    • HTTP 429 Too Many Requests: Exceeded Plivo's rate limit (default 5 messages/second per account). Implement exponential backoff and retry logic. Consider distributing load across multiple Plivo numbers.
<!-- EXPAND: Could add FAQ section with more edge cases and solutions (Type: Enhancement) -->
  • Platform Limitations:

    • Trial Account: Can only send to verified sandbox numbers. Limited throughput. Plivo branding may be added to messages. Insufficient credits may cause failures.
    • Sender ID Rules: Vary significantly by country. US/Canada generally require Plivo numbers. Alphanumeric IDs may need registration elsewhere. Check Plivo's country-specific SMS guidelines.
    • Rate Limits: Plivo imposes limits on API calls per second/minute (5 msg/sec default for SMS, 100 concurrent for MMS). High-volume sending requires handling potential rate limit errors (HTTP 429) with queuing and backoff strategies.
  • Version Compatibility:

    • Fastify v5: Requires Node.js v20 or later (as of 2025).
    • Fastify v4: Supports Node.js v14-v22, but reaches EOL on June 30, 2025).
    • Plivo SDK: Check npm (npmjs.com/package/plivo) for the latest version and compatibility notes.
    • Ensure your Node.js version is compatible with the versions of Fastify and the Plivo SDK specified in package.json. Check the respective libraries' documentation for compatibility notes if issues arise.
  • Edge Cases:

    • Very long messages splitting into many segments (10+ segments).
    • Messages with complex Unicode characters or emoji combinations.
    • Destination numbers in regions with delivery issues, carrier blocks, or strict regulations.
    • Sending to numbers during carrier maintenance windows.
  • Workarounds:

    • For trial accounts, always verify numbers first in Plivo console under Sandbox Numbers.
    • For rate limits, implement client-side throttling, queueing (using Redis, RabbitMQ, or in-memory queues), or distribute load across multiple Plivo numbers.
    • For very long messages, consider splitting manually or warning users about multi-segment costs.
<!-- DEPTH: Troubleshooting section lacks debug mode instructions or verbose logging setup (Priority: Medium) -->

12. Deployment and CI/CD

Basic Deployment (Conceptual):

  1. Build/Preparation: Ensure all dependencies are installed (npm ci --production can skip devDependencies for smaller deployment packages).

  2. Environment Configuration: Set the necessary environment variables (PLIVO_AUTH_ID, PLIVO_AUTH_TOKEN, PLIVO_SENDER_NUMBER, PORT, NODE_ENV=production) on the target server or container environment. Do not deploy the .env file directly; use the platform's environment variable management (e.g., Docker secrets, Kubernetes ConfigMaps/Secrets, cloud provider environment variables).

  3. Node.js Version: Ensure the deployment environment has the correct Node.js version installed:

    • Node.js v20+ for Fastify v5
    • Node.js v14+ for Fastify v4 (EOL June 30, 2025)
  4. Process Management: Use a process manager like PM2 or rely on container orchestration (Docker, Kubernetes) to run and manage the Node.js process (node src/server.js).

    • PM2 Example:
      bash
      # Install PM2 globally
      npm install -g pm2
      # Start the application
      pm2 start src/server.js --name fastify-plivo-sms
      # View logs
      pm2 logs fastify-plivo-sms
      # Monitor processes
      pm2 monit
      # Save PM2 configuration
      pm2 save
      # Setup PM2 startup script
      pm2 startup
  5. Reverse Proxy (Recommended): Set up Nginx, Caddy, or similar to act as a reverse proxy in front of your Fastify app. This handles:

    • SSL/TLS termination (HTTPS) – Encrypts traffic
    • Load balancing – Distributes requests across multiple instances if scaling horizontally
    • Static file serving – If needed for other assets
    • Rate limiting – Additional layer of protection
    • Request buffering – Improves handling of slow clients
<!-- GAP: Missing Nginx configuration example for reverse proxy setup (Type: Substantive) -->

Docker Deployment Example:

dockerfile
# Dockerfile
FROM node:20-alpine

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install production dependencies
RUN npm ci --production

# Copy application code
COPY src ./src

# Expose port
EXPOSE 3000

# Set environment to production
ENV NODE_ENV=production

# Start application
CMD ["node", "src/server.js"]
<!-- EXPAND: Could add docker-compose.yml example with environment variables (Type: Enhancement) -->

CI/CD Pipeline (Conceptual):

A typical pipeline using services like GitHub Actions, GitLab CI, or Jenkins:

  1. Trigger: On push/merge to the main branch.
  2. Checkout Code: Get the latest source code from repository.
  3. Setup Node.js: Specify the required Node.js version (v20+ for Fastify v5).
  4. Install Dependencies: npm ci (uses package-lock.json for deterministic installs).
  5. Linting/Testing: Run code linters (e.g., ESLint) and automated tests (unit, integration).
  6. Security Scanning: Scan dependencies for vulnerabilities (npm audit, Snyk, etc.).
  7. Build (if necessary): If using TypeScript or a build step, compile the code.
  8. Package: Create a deployment artifact (e.g., Docker image, zip archive).
  9. Deploy: Push the artifact to the target environment (e.g., cloud provider, Kubernetes cluster, VM).
  10. Post-Deploy Steps: Run health checks, smoke tests, notify team on Slack/email.
  11. Rollback Strategy: Implement automated rollback if health checks fail post-deployment.

Example GitHub Actions Workflow:

yaml
name: CI/CD Pipeline

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run tests
        run: npm test

      - name: Build Docker image
        run: docker build -t fastify-plivo-sms:${{ github.sha }} .

      - name: Push to registry
        run: docker push fastify-plivo-sms:${{ github.sha }}

      - name: Deploy to production
        run: kubectl set image deployment/fastify-plivo-sms fastify-plivo-sms=fastify-plivo-sms:${{ github.sha }}
<!-- DEPTH: CI/CD example lacks security scanning step configuration and secrets management (Priority: High) --> <!-- GAP: Missing blue-green deployment or canary deployment strategies (Type: Substantive) -->

Summary

You've built a production-ready SMS API with Plivo, Node.js, and Fastify featuring:

  • Modern Stack: Fastify v5 with Node.js v20+ for optimal performance (or Fastify v4 with Node.js v14+ for compatibility until June 2025).
  • Verified Integration: Plivo Node.js SDK with proper authentication and E.164 format validation per ITU-T standard.
  • Robust Error Handling: Comprehensive error mapping for Plivo API errors, rate limits (HTTP 429), and network issues.
  • Security: Input validation, rate limiting, security headers, and secure credential management.
  • Production-Ready: Health checks, structured logging, performance optimization guidelines, and deployment strategies.
  • Rate Limit Awareness: Understanding of Plivo's 5 messages/second default limit and smart queue system.

Next Steps:

  • Implement retry logic with exponential backoff for transient failures.
  • Add API key authentication for production use.
  • Set up monitoring and alerting with your preferred observability platform.
  • Deploy to your cloud provider with proper environment variable management.
  • Scale horizontally by distributing load across multiple Plivo numbers if needed.
<!-- EXPAND: Next steps should include estimated costs for production deployment and scaling (Type: Enhancement) --> <!-- GAP: Missing reference to additional Plivo features (voice, MMS, verification) for future expansion (Type: Substantive) -->

Frequently Asked Questions

How to send SMS with Fastify and Plivo?

Set up a Node.js project with Fastify, the Plivo Node.js SDK, and dotenv. Create routes in Fastify to handle incoming SMS requests. Configure the Plivo client with your credentials, then use the client's API within your Fastify route to send messages based on incoming requests. Ensure your destination numbers are verified in your Plivo account if using a trial version.

What is the Plivo sender number format?

The Plivo sender number should be in E.164 format, for example, +12025551234. This international standard ensures consistent formatting for phone numbers and is required by Plivo for sending SMS. The plus sign '+' followed by the country code and the number is required.

Why does Fastify use schema validation?

Fastify's schema validation automatically validates incoming requests, reducing boilerplate code for manual checks and improving reliability. This built-in feature uses JSON Schema to define expected request parameters. Schema validation ensures your API receives correctly formatted data and strengthens security by preventing injection attacks.

When should I use environment variables for Plivo configuration?

Always use environment variables to store sensitive information like your Plivo Auth ID, Auth Token, and sender number. Place these in a `.env` file, and load it using `dotenv`. Never directly embed credentials in your code to prevent security vulnerabilities.

Can I use alphanumeric sender IDs with Plivo?

Alphanumeric sender IDs are subject to specific regulations depending on the destination country. While some countries permit their usage (potentially requiring pre-registration), countries like the US and Canada usually require a Plivo long code or Toll-Free number for sending SMS.

How to handle Plivo API errors in Fastify?

Wrap the Plivo API call in a try...catch block within your Fastify route handler to handle potential errors during the API call. Log detailed error messages and return appropriate HTTP status codes along with informative JSON error responses.

What is the role of .env in a Plivo/Fastify project?

The `.env` file stores sensitive credentials, including the Plivo Auth ID, Auth Token, and sender number. This file should never be committed to version control and should be added to the `.gitignore` file to keep credentials private. The `dotenv` library is used to load environment variables from `.env` into your application.

How to install necessary dependencies for a Fastify/Plivo SMS app?

Use npm or yarn to install the necessary packages: `npm install fastify plivo dotenv`. Fastify is the web framework, `plivo` is the Plivo Node.js SDK, and `dotenv` loads environment variables from your `.env` file.

How to structure a Fastify/Plivo project?

A typical project structure includes directories for routes (`src/routes`), configuration (`src/config`), and the main server file (`src/server.js`). The routes directory contains files defining API endpoints. The configuration directory keeps Plivo setup logic.

What is the maximum SMS message length with Plivo?

Standard SMS messages using the GSM-7 character set allow up to 160 characters per segment. Messages with Unicode characters (like emojis) switch to UCS-2 encoding, limiting segments to 70 characters. Plivo automatically handles this encoding and concatenation of longer messages.

Why is error handling important in sending SMS with Plivo?

Error handling, through try...catch blocks and robust logging, is crucial for managing issues such as network problems, incorrect Plivo credentials, invalid numbers, and rate limits. Logging helps diagnose and fix problems quickly.

How to test a Fastify/Plivo SMS API?

Use tools like `curl` or Postman to send test requests to the `/api/sms/send` endpoint. Verify that the destination number receives the SMS and that your Fastify server logs the request and response information correctly.

How to secure a Fastify API that sends SMS with Plivo?

Secure your API by using schema validation for all inputs, storing API keys as environment variables, implementing rate limiting using libraries like `fastify-rate-limit`, and always using HTTPS in production.

What are some performance optimizations for a Fastify/Plivo SMS service?

Performance optimizations include using Fastify's built-in efficiency, leveraging async/await for non-blocking operations, efficient logging, and load testing to identify potential bottlenecks under stress.

How to monitor a Fastify/Plivo SMS application?

Implement health checks, monitor key metrics like request latency, error rates, and resource utilization. Use logging and error tracking tools and set up alerts based on critical thresholds. Create dashboards to visualize key metrics and service health.