code examples

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

Send SMS with Node.js and Infobip API: Complete Express Integration Guide

Learn how to build a secure Node.js Express application to send SMS messages using the Infobip API. Complete guide with authentication, error handling, testing, and deployment.

Send SMS Messages with Node.js and Infobip API

This guide provides a complete walkthrough for building a Node.js application using the Express framework to send SMS messages via the Infobip API. We'll cover everything from project setup and core functionality to error handling, security, and deployment, resulting in a robust foundation for integrating SMS capabilities into your applications.

By the end of this guide, you will have a functional Express API endpoint capable of accepting a phone number and message content, securely interacting with the Infobip API to send the SMS, and handling potential errors gracefully. This solves the common need for applications to send transactional notifications, alerts, or simple communications via SMS.

Project Overview and Goals

  • Goal: Create a simple, secure, and reliable Node.js service to send outbound SMS messages using the Infobip API.

  • Problem Solved: Provides a backend mechanism for applications to trigger SMS messages without exposing sensitive API credentials directly to client-side code. Enables programmatic sending of notifications, alerts, verification codes, etc.

  • Technologies Used:

    • Node.js: A JavaScript runtime environment ideal for building efficient I/O-bound applications like API servers.
    • Express.js: A minimal and flexible Node.js web application framework providing robust features for web and mobile applications.
    • Axios: A popular promise-based HTTP client for making requests to the Infobip API.
    • dotenv: A module to load environment variables from a .env file into process.env, keeping sensitive data out of source code.
    • Infobip API: The third-party service used for dispatching SMS messages.
  • Prerequisites:

    • Node.js and npm (or yarn) installed locally. (Download Node.js)
    • An active Infobip account. (Create an Infobip Account)
    • Basic understanding of JavaScript, Node.js, REST APIs, and the command line.
    • A text editor or IDE (like VS Code).
    • Optional: Postman or curl for testing the API endpoint.
  • System Architecture:

    (Note: A diagram illustrating the flow: Client -> Node.js/Express API -> Infobip API -> Recipient's Phone, with .env for credentials, was intended here.)

  • Final Outcome: A Node.js Express application running locally with a single API endpoint (e.g., POST /api/sms/send) that accepts a destination phone number and message text, sends the SMS via Infobip, and returns a success or error response.

1. Setting up the Project

Let's initialize our Node.js project and install the necessary dependencies.

  1. Create Project Directory: Open your terminal or command prompt and create a new directory for the project. Navigate into it.

    bash
    mkdir node-infobip-sms
    cd node-infobip-sms
  2. Initialize Node.js Project: Run npm init and follow the prompts. You can accept the defaults by pressing Enter repeatedly, or customize the details. This creates a package.json file.

    bash
    npm init -y

    (The -y flag automatically accepts the defaults)

  3. Install Dependencies: We need Express for the server framework, Axios for HTTP requests, and dotenv for managing environment variables.

    bash
    npm install express axios dotenv

    This command downloads the packages and adds them to your package.json and node_modules directory.

  4. Create Project Structure: Set up a basic structure for clarity:

    bash
    mkdir src
    mkdir src/routes
    mkdir src/services
    touch src/index.js
    touch src/routes/smsRoutes.js
    touch src/services/infobipService.js
    touch .env
    touch .gitignore
    • src/index.js: The main entry point for our application.
    • src/routes/smsRoutes.js: Defines the API endpoint(s) related to SMS.
    • src/services/infobipService.js: Contains the logic for interacting with the Infobip API.
    • .env: Stores sensitive configuration like API keys (will be ignored by Git).
    • .gitignore: Specifies files and directories that Git should ignore.
  5. Configure .gitignore: Add node_modules and .env to your .gitignore file to prevent committing them to version control.

    text
    # .gitignore
    
    # Dependencies
    node_modules/
    
    # Environment variables
    .env
    
    # Logs
    logs
    *.log
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
    lerna-debug.log*
    
    # Optional files
    .DS_Store
  6. Environment Setup: The steps above work across macOS, Linux, and Windows (using terminals like Git Bash or WSL). Ensure Node.js and npm are correctly installed and accessible in your terminal's PATH.

2. Implementing Core Functionality (Infobip Service)

We'll encapsulate the logic for interacting with the Infobip API within a dedicated service file. This promotes modularity and makes the code easier to test and maintain.

  1. Edit src/services/infobipService.js: Open this file and add the following code. This adapts the logic from the Infobip blog post research, using Axios and structuring it as an asynchronous function.

    javascript
    // src/services/infobipService.js
    
    const axios = require('axios');
    
    /**
     * Constructs the full API endpoint URL.
     * @param {string} domain - Your Infobip base URL (e.g., xxx.api.infobip.com).
     * @returns {string} The full URL for the send SMS endpoint.
     */
    const buildUrl = (domain) => {
        if (!domain) {
            throw new Error('Infobip domain (base URL) is required.');
        }
        // Ensure the domain doesn't have a trailing slash for consistency
        const cleanDomain = domain.replace(/\/$/, '');
        return `https://${cleanDomain}/sms/2/text/advanced`;
    };
    
    /**
     * Constructs the necessary HTTP headers for Infobip API authentication.
     * @param {string} apiKey - Your Infobip API key.
     * @returns {object} Headers object for Axios request config.
     */
    const buildHeaders = (apiKey) => {
        if (!apiKey) {
            throw new Error('Infobip API key is required.');
        }
        return {
            'Authorization': `App ${apiKey}`,
            'Content-Type': 'application/json',
            'Accept': 'application/json', // Explicitly accept JSON responses
        };
    };
    
    /**
     * Constructs the request body according to Infobip's API specification.
     * @param {string} destinationNumber - The recipient's phone number in international format.
     * @param {string} message - The text message content.
     * @param {string} [senderName=""InfoSMS""] - Optional sender ID (Alphanumeric, check Infobip regulations).
     * @returns {object} Request body object.
     */
    const buildRequestBody = (destinationNumber, message, senderName = 'InfoSMS') => {
        if (!destinationNumber || !message) {
            throw new Error('Destination number and message are required.');
        }
    
        // Basic validation for international format (starts with +, followed by digits)
        // Note: More robust validation might be needed depending on requirements.
        if (!/^\+?\d+$/.test(destinationNumber)) {
             // Use template literal correctly
             console.warn(`Destination number ${destinationNumber} might not be in the correct international format.`);
        }
    
        const destinationObject = {
            to: destinationNumber,
        };
    
        const messageObject = {
            from: senderName, // Optional: Sender ID. Check Infobip docs for restrictions.
            destinations: [destinationObject],
            text: message,
        };
    
        return {
            messages: [messageObject],
        };
    };
    
    /**
     * Sends an SMS message using the Infobip API.
     * @param {string} domain - Your Infobip base URL.
     * @param {string} apiKey - Your Infobip API key.
     * @param {string} destinationNumber - Recipient's phone number.
     * @param {string} message - SMS text content.
     * @returns {Promise<object>} A promise that resolves with the parsed success response from Infobip.
     * @throws {Error} Throws an error if the request fails or inputs are invalid.
     */
    const sendSms = async (domain, apiKey, destinationNumber, message) => {
        try {
            // Validate inputs (basic checks)
            if (!domain || !apiKey || !destinationNumber || !message) {
                throw new Error('Missing required parameters for sending SMS.');
            }
    
            const url = buildUrl(domain);
            const headers = buildHeaders(apiKey);
            const requestBody = buildRequestBody(destinationNumber, message);
    
            console.log(`Sending SMS to ${destinationNumber} via Infobip URL: ${url}`);
    
            const response = await axios.post(url, requestBody, { headers: headers });
    
            // Check for successful status codes (e.g., 2xx)
            if (response.status >= 200 && response.status < 300) {
                console.log('Infobip API Success Response:', JSON.stringify(response.data, null, 2));
    
                // Safely parse response: Check if messages array exists and has elements
                const firstMessage = (response.data && Array.isArray(response.data.messages) && response.data.messages.length > 0)
                    ? response.data.messages[0]
                    : null;
    
                if (!firstMessage) {
                    console.warn('Infobip success response did not contain expected message details.');
                    // Decide how to handle this - return basic success or throw a specific error
                    return {
                        success: true,
                        details: response.data // Return full data if parsing failed
                    };
                }
    
                return {
                    success: true,
                    messageId: firstMessage.messageId,
                    status: firstMessage.status?.name, // Use optional chaining for status properties
                    description: firstMessage.status?.description,
                    details: response.data // Include full response data if needed
                };
            } else {
                // Handle non-2xx success codes if Infobip uses them differently
                 console.error(`Infobip API returned non-success status: ${response.status}`);
                 throw new Error(`Infobip API request failed with status ${response.status}`);
            }
    
        } catch (error) {
            console.error('Error sending SMS via Infobip:', error.message);
    
            // Provide more detailed error info if available from Axios/Infobip
            if (error.response) {
                // The request was made and the server responded with a status code
                // that falls out of the range of 2xx
                console.error('Infobip Error Response Data:', JSON.stringify(error.response.data, null, 2));
                console.error('Infobip Error Response Status:', error.response.status);
                console.error('Infobip Error Response Headers:', error.response.headers);
    
                // Extract specific error message from Infobip if possible
                const serviceException = error.response.data?.requestError?.serviceException;
                const errorMessage = serviceException?.text || `Infobip API error: Status ${error.response.status}`;
                throw new Error(errorMessage);
    
            } else if (error.request) {
                // The request was made but no response was received
                console.error('Infobip Error Request:', error.request);
                throw new Error('No response received from Infobip API. Check network or API status.');
            } else {
                // Something happened in setting up the request that triggered an Error
                throw new Error(`Failed to send SMS: ${error.message}`);
            }
        }
    };
    
    module.exports = {
        sendSms,
        // Export helper functions if needed for testing or other modules
        // buildUrl,
        // buildHeaders,
        // buildRequestBody
    };

    Why this approach?

    • Separation of Concerns: Isolates Infobip-specific logic, making the API route cleaner.
    • Reusability: The sendSms function can be reused elsewhere in the application if needed.
    • Testability: Easier to write unit tests for infobipService.js by mocking axios.
    • Async/Await: Uses modern JavaScript syntax for handling asynchronous operations cleanly.
    • Error Handling: Includes specific checks for different types of errors (response errors, request errors, setup errors) provided by Axios, and safe parsing of the success response.

3. Building the API Layer (Express Route)

Now, let's create the Express server and define the API endpoint that will use our infobipService.

  1. Configure Environment Variables: Open the .env file and add your Infobip API Key and Base URL. Never commit this file to Git.

    dotenv
    # .env
    # --- Infobip Credentials ---
    # Replace placeholder values with your actual Infobip API Key and Base URL
    # Find these in your Infobip account dashboard (Developers -> API Keys)
    INFOBIP_API_KEY=PASTE_YOUR_INFOBIP_API_KEY_HERE
    INFOBIP_BASE_URL=PASTE_YOUR_INFOBIP_BASE_URL_HERE # Example format: y1q2w3.api.infobip.com (Do NOT include https://)
    
    # --- Server Configuration ---
    PORT=3000 # Optional: Define the port the server will run on
    • How to find credentials:
      1. Log in to your Infobip Portal.
      2. Navigate to the ""Developers"" section or look for ""API Keys"" in your account settings.
      3. Create a new API key if you don't have one. Copy the key value.
      4. Your Base URL is displayed alongside your API key (it's unique to your account). Copy this URL without the https://.
      5. For more details, see the Infobip API documentation.
  2. Edit src/routes/smsRoutes.js: Define the route handler.

    javascript
    // src/routes/smsRoutes.js
    const express = require('express');
    const { sendSms } = require('../services/infobipService'); // Import the service function
    
    const router = express.Router();
    
    // POST /api/sms/send - Endpoint to send an SMS
    router.post('/send', async (req, res) => {
        // Basic input validation
        const { to, message } = req.body; // Expecting 'to' (phone number) and 'message' in the JSON body
    
        if (!to || !message) {
            return res.status(400).json({
                success: false,
                // Use double quotes for the outer string to allow single quotes inside
                message: ""Missing required fields: 'to' (destination phone number) and 'message'."",
            });
        }
    
        // It's crucial to validate/sanitize 'to' and 'message' more thoroughly in production
        // (e.g., check phone number format, message length, prevent injection attacks - see Section 7)
    
        try {
            const apiKey = process.env.INFOBIP_API_KEY;
            const domain = process.env.INFOBIP_BASE_URL;
    
            if (!apiKey || !domain || apiKey === 'PASTE_YOUR_INFOBIP_API_KEY_HERE' || domain === 'PASTE_YOUR_INFOBIP_BASE_URL_HERE') {
                 console.error('Infobip API Key or Base URL is not configured correctly in environment variables.');
                 return res.status(500).json({
                    success: false,
                    message: 'Server configuration error: Infobip credentials missing or not replaced.',
                 });
            }
    
            const result = await sendSms(domain, apiKey, to, message);
    
            // Send successful response back to the client
            res.status(200).json({
                success: true,
                message: 'SMS submitted successfully.',
                details: result, // Include details like messageId and status from Infobip
            });
    
        } catch (error) {
            console.error('Error in /api/sms/send route:', error.message);
    
            // Send error response back to the client
            // Avoid exposing detailed internal errors unless necessary for debugging
            res.status(500).json({
                success: false,
                message: `Failed to send SMS: ${error.message}`, // Provide the error message from the service
            });
        }
    });
    
    module.exports = router; // Export the router
  3. Edit src/index.js: Set up the Express application, load environment variables, enable JSON body parsing, and mount the routes.

    javascript
    // src/index.js
    require('dotenv').config(); // Load environment variables from .env file first
    const express = require('express');
    const smsRoutes = require('./routes/smsRoutes'); // Import the router
    
    const app = express();
    const PORT = process.env.PORT || 3000; // Use port from .env or default to 3000
    
    // Middleware
    app.use(express.json()); // Enable parsing of JSON request bodies
    app.use(express.urlencoded({ extended: true })); // Enable parsing of URL-encoded bodies (optional but common)
    
    // --- Health Check Endpoint ---
    // Useful for monitoring services to see if the app is running
    app.get('/health', (req, res) => {
        res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
    });
    
    
    // --- Mount API Routes ---
    // All routes defined in smsRoutes will be prefixed with /api/sms
    app.use('/api/sms', smsRoutes);
    
    
    // --- Global Error Handler (Basic Example) ---
    // Catches errors not handled in specific routes (place after all routes)
    app.use((err, req, res, next) => {
        // Use single quotes for the string literal
        console.error('Unhandled Error:', err.stack);
        res.status(500).json({
             success: false,
             message: 'An unexpected internal server error occurred.',
             // In development, you might want to expose the error stack:
             // error: process.env.NODE_ENV === 'development' ? err.stack : undefined
        });
    });
    
    
    // --- Start the Server ---
    // Assign the server instance to a variable for graceful shutdown
    const server = app.listen(PORT, () => {
        console.log(`Server listening on port ${PORT}`);
        // Check if essential env vars are loaded and placeholders replaced
        if (!process.env.INFOBIP_API_KEY || !process.env.INFOBIP_BASE_URL ||
            process.env.INFOBIP_API_KEY === 'PASTE_YOUR_INFOBIP_API_KEY_HERE' ||
            process.env.INFOBIP_BASE_URL === 'PASTE_YOUR_INFOBIP_BASE_URL_HERE') {
            console.warn('WARNING: INFOBIP_API_KEY or INFOBIP_BASE_URL environment variables are not set or are still placeholders!');
            console.warn('SMS functionality will likely fail.');
        } else {
             console.log('Infobip credentials loaded successfully.');
        }
    });
    
    // Optional: Handle graceful shutdown
    process.on('SIGTERM', () => {
        console.log('SIGTERM signal received: closing HTTP server');
        // Add cleanup logic here if needed (e.g., close database connections)
        server.close(() => { // Use the 'server' variable assigned above
             console.log('HTTP server closed');
             process.exit(0); // Exit gracefully
        });
        // If server.close() doesn't exit quickly, force exit after a timeout
        setTimeout(() => {
            console.error('Could not close connections in time, forcefully shutting down');
            process.exit(1);
        }, 10000); // e.g., 10 seconds timeout
    });
    
    // Export the app for potential testing
    module.exports = app;
  4. Add Start Script and Specify Dependency Versions: Update package.json to include a convenient start script and list specific, tested versions for dependencies. Using exact versions installed by npm install is recommended for reproducibility.

    json
    // package.json (add/modify the ""scripts"" and ""dependencies"" sections)
    {
      ""name"": ""node-infobip-sms"",
      ""version"": ""1.0.0"",
      ""description"": ""Node.js service to send SMS via Infobip"",
      ""main"": ""src/index.js"",
      ""scripts"": {
        ""start"": ""node src/index.js"",
        ""dev"": ""nodemon src/index.js"",
        ""test"": ""echo \""Error: no test specified\"" && exit 1""
      },
      ""keywords"": [
        ""nodejs"",
        ""express"",
        ""infobip"",
        ""sms""
      ],
      ""author"": """",
      ""license"": ""ISC"",
      ""dependencies"": {
        ""axios"": ""^1.6.8"",    // Example: Use a specific recent version
        ""dotenv"": ""^16.4.5"",  // Example: Use a specific recent version
        ""express"": ""^4.19.2""  // Example: Use a specific recent version
        // Note: Use the actual versions installed by 'npm install' for your project
      },
      ""devDependencies"": {
        // ""nodemon"": ""^3.1.0"" // Optional: Install if using 'dev' script
      }
    }

    To ensure you use the correct versions, check your package-lock.json after running npm install or use npm list --depth=0. (If you want to use the dev script, install nodemon: npm install --save-dev nodemon)

4. Integrating with Infobip (Recap & Details)

We've already set up the core integration, but let's reiterate the key points for clarity:

  • Credentials: The INFOBIP_API_KEY and INFOBIP_BASE_URL are essential. They are obtained directly from your Infobip account dashboard under the API Keys section.

  • Security: These credentials are sensitive and must be stored securely using environment variables (via the .env file loaded by dotenv). Never hardcode them in your source code or commit the .env file. Ensure the placeholder values in .env are replaced with your actual credentials.

  • Base URL: Ensure you use the correct Base URL provided by Infobip, specific to your account. It looks like [unique_id].api.infobip.com. Do not include https:// in the .env file value.

  • API Endpoint: We are using the /sms/2/text/advanced endpoint, which is versatile for sending single or multiple messages. Note: Infobip also offers a newer /sms/3/messages endpoint with additional features. This tutorial uses v2 for broader compatibility and simplicity.

  • Authentication: The Authorization: App YOUR_API_KEY header is used for authentication, as implemented in infobipService.js.

  • Request Body: The structure { messages: [{ from: 'SenderID', destinations: [{ to: 'PhoneNumber' }], text: 'MessageContent' }] } is required by the API endpoint.

  • Sender ID (from): This is often regulated. Check Infobip's documentation and local regulations regarding allowed Sender IDs. Some destinations might replace alphanumeric IDs with a generic number if not pre-registered. Using a dedicated number purchased through Infobip is often more reliable. For testing, 'InfoSMS' might work, but customize as needed.

  • Free Trial Limitations: If using an Infobip free trial account, you can typically only send SMS messages to the phone number you used to register the account.

  • Message Character Limits and Encoding:

    • GSM-7 Encoding: Standard SMS supports 160 characters per message using GSM-7 encoding (default for English and common European languages).
    • Unicode (UCS-2) Encoding: Messages containing special characters, emojis, or non-Latin scripts (Arabic, Chinese, etc.) use Unicode encoding, limiting capacity to 70 characters per message.
    • Concatenated Messages: Messages exceeding the character limit are automatically split into multiple parts. For GSM-7: 153 characters per segment. For Unicode: 67 characters per segment. Recipients receive these as a single message, but you are charged for each segment.
    • Message Validity Period: By default, Infobip stores undelivered messages for 48 hours (extendable to 72 hours) if the recipient is out of signal range. Messages are delivered once the recipient regains coverage.
    • Best Practice: Monitor message length in your application to avoid unexpected costs from message concatenation. Source: Infobip SMS Documentation (accessed January 2025).

5. Error Handling, Logging, and Retries

Our current implementation includes basic error handling and logging.

  • Error Handling:
    • The infobipService.js catches errors during the Axios request, attempts to parse specific Infobip error messages, and throws a meaningful error. Includes safe access to the response structure.
    • The smsRoutes.js catches errors from the service call and returns a 500 Internal Server Error response with the error message.
    • Basic input validation returns 400 Bad Request if required fields are missing.
    • The global error handler in index.js catches any unhandled exceptions.
  • Logging:
    • We use console.log for informational messages (starting server, sending SMS) and console.error for errors.
    • In production, consider using a more robust logging library like Winston or Pino for structured logging (e.g., JSON format), different log levels (debug, info, warn, error), and routing logs to files or external services.
    • Example Logging Points:
      • Incoming request details (maybe anonymized).
      • Parameters passed to infobipService.
      • Success response from Infobip (including messageId).
      • Detailed errors from Infobip or Axios.
  • Retry Mechanisms:
    • Network glitches or temporary Infobip issues (like 5xx errors) might cause requests to fail intermittently. Implementing a retry strategy can improve reliability.

    • Strategy: Use libraries like axios-retry to automatically retry failed requests.

    • Configuration: Configure axios-retry to only retry on specific conditions (e.g., network errors, 500, 502, 503, 504 status codes) and use exponential backoff (increasing delays between retries).

    • Caution: Avoid retrying non-transient errors (e.g., 4xx errors like invalid API key, invalid phone number, insufficient funds) to prevent wasting resources or sending duplicate messages unintentionally.

    • Example Integration (Conceptual - add to infobipService.js):

      javascript
      // At the top of infobipService.js
      // const axiosRetry = require('axios-retry').default; // Use .default for ES modules/TS compatibility if needed
      
      // Inside sendSms, before making the call:
      /*
      // --- Axios Retry Example (Conceptual) ---
      const retryClient = axios.create(); // Create a separate instance or configure the main one
      axiosRetry(retryClient, {
          retries: 3, // Number of retries
          retryDelay: (retryCount) => {
              console.log(`Retry attempt: ${retryCount}`);
              return retryCount * 1000; // Exponential back-off (1s, 2s, 3s)
          },
          retryCondition: (error) => {
              // Retry on network errors or specific server errors
              return (
                  axiosRetry.isNetworkOrIdempotentRequestError(error) ||
                  (error.response && error.response.status >= 500)
              );
          },
          shouldResetTimeout: true, // Reset timeout on retries
      });
      
      // Use the retryClient for the request:
      // const response = await retryClient.post(url, requestBody, { headers: headers });
      // --- End Axios Retry Example ---
      */
      // Note: The actual implementation might involve configuring the main axios instance
      // or creating a dedicated instance for Infobip calls if using axios elsewhere.
      // See axios-retry documentation for detailed usage: https://github.com/softonic/axios-retry

6. Database Schema and Data Layer (Optional Logging)

(This section describes an optional enhancement for production environments. It is not required for the core SMS sending functionality.)

Logging sent messages to a database provides crucial tracking, auditing, and debugging capabilities.

  1. Concept: Store details about each SMS attempt (success or failure) in a persistent data store.

  2. Technology Choice: For simplicity, you could use SQLite locally. For production, PostgreSQL or MongoDB are common choices. ORMs like Prisma or Sequelize simplify database interactions in Node.js.

  3. Example Schema (Conceptual / Prisma):

    prisma
    // schema.prisma (Example using Prisma)
    
    datasource db {
      // Note: The `Json` type below is well-supported by PostgreSQL.
      // If using a different provider (e.g., older SQLite versions, MySQL < 5.7),
      // you might need to store JSON as a String and handle parsing manually,
      // or use provider-specific JSON types if available.
      provider = ""postgresql""
      url      = env(""DATABASE_URL"")
    }
    
    generator client {
      provider = ""prisma-client-js""
    }
    
    model SmsLog {
      id              String   @id @default(cuid()) // Unique log entry ID
      createdAt       DateTime @default(now())      // When the log entry was created
      updatedAt       DateTime @updatedAt          // When the log entry was last updated
    
      recipient       String                      // Destination phone number (consider masking/encryption for PII)
      messageContent  String?                     // Optional: Log message content (consider PII/length)
      senderId        String?                     // Sender ID used
    
      infobipMessageId String?  @unique            // Message ID returned by Infobip (if successful submission)
      infobipStatus    String?                     // Status name from Infobip (e.g., PENDING_ACCEPTED)
      infobipDetails   Json?                       // Store the full Infobip response details (requires compatible provider)
    
      attemptStatus    String                      // Status of our attempt (e.g., SUCCESS, FAILED, PENDING_RETRY)
      errorMessage     String?                     // Error message if the attempt failed
    
      @@index([recipient])
      @@index([infobipMessageId])
      @@index([createdAt])
    }
  4. Implementation Steps (if using Prisma):

    • Install Prisma: npm install prisma --save-dev and npm install @prisma/client
    • Initialize Prisma: npx prisma init --datasource-provider postgresql (adjust provider if needed). This creates prisma/schema.prisma and updates .env with DATABASE_URL.
    • Define the schema as above in prisma/schema.prisma.
    • Configure your DATABASE_URL in .env.
    • Run migrations: npx prisma migrate dev --name init (creates the table).
    • Instantiate Prisma Client: const { PrismaClient } = require('@prisma/client'); const prisma = new PrismaClient();
    • Modify smsRoutes.js or infobipService.js to interact with prisma.smsLog:
      • Before calling sendSms: prisma.smsLog.create({ data: { recipient, messageContent, attemptStatus: 'PENDING' } }).
      • On success: prisma.smsLog.update({ where: { id: logEntryId }, data: { infobipMessageId, infobipStatus, infobipDetails, attemptStatus: 'SUCCESS' } }).
      • On failure: prisma.smsLog.update({ where: { id: logEntryId }, data: { errorMessage, attemptStatus: 'FAILED' } }).
  5. Performance/Scale: Index frequently queried columns (recipient, infobipMessageId, createdAt). Use appropriate database types. For very high volume, consider asynchronous logging (e.g., pushing log jobs to a queue) or using specialized logging databases/services.

7. Adding Security Features

Security is paramount, especially when dealing with external APIs and potentially user-provided data.

  1. API Key Security: Already addressed by using .env files and .gitignore. Ensure your server environment variables are managed securely in deployment (e.g., using platform secrets management).

  2. Input Validation and Sanitization:

    • Need: Prevent invalid data, errors, and potential injection attacks. Crucial for any data coming from external sources.

    • Implementation: Use a validation library like express-validator.

    • Example (src/routes/smsRoutes.js):

      bash
      npm install express-validator
      javascript
      // src/routes/smsRoutes.js
      const express = require('express');
      const { sendSms } = require('../services/infobipService');
      const { body, validationResult } = require('express-validator'); // Import validation functions
      
      const router = express.Router();
      
      router.post(
          '/send',
          // --- Validation Middleware Array ---
          [
              body('to')
                  .trim() // Remove leading/trailing whitespace
                  .notEmpty().withMessage('Recipient phone number (""to"") is required.')
                  // Add more specific validation, e.g., using isMobilePhone
                  // .isMobilePhone('any', { strictMode: false }).withMessage('Invalid phone number format.'),
                  .escape(), // Escape HTML characters to prevent XSS if this data is ever rendered
      
              body('message')
                  .trim()
                  .notEmpty().withMessage('Message content (""message"") is required.')
                  .isLength({ min: 1, max: 1600 }).withMessage('Message length exceeds allowed limit.') // Check Infobip limits
                  .escape(), // Escape HTML characters
          ],
          // --- Route Handler ---
          async (req, res) => {
              // Check for validation errors
              const errors = validationResult(req);
              if (!errors.isEmpty()) {
                  return res.status(400).json({ success: false, errors: errors.array() });
              }
      
              // If validation passes, proceed with existing logic...
              const { to, message } = req.body;
      
              try {
                  const apiKey = process.env.INFOBIP_API_KEY;
                  const domain = process.env.INFOBIP_BASE_URL;
      
                  // ... (rest of the try block as before) ...
      
              } catch (error) {
                  // ... (catch block as before) ...
              }
          }
      );
      
      module.exports = router;

      (Note: The isMobilePhone validator might require adjustments based on the expected phone number formats. escape() prevents basic XSS if the input is ever reflected.)

  3. Rate Limiting:

    • Need: Prevent abuse (intentional or accidental) of the API endpoint, which could lead to high costs or service degradation.

    • Implementation: Use middleware like express-rate-limit.

    • Example (src/index.js):

      bash
      npm install express-rate-limit
      javascript
      // src/index.js
      // ... other imports ...
      const rateLimit = require('express-rate-limit');
      
      const app = express();
      // ... PORT, middleware ...
      
      // --- Rate Limiter Configuration ---
      const smsLimiter = rateLimit({
          windowMs: 15 * 60 * 1000, // 15 minutes
          max: 100, // Limit each IP to 100 requests per windowMs
          message: 'Too many SMS requests created 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
      });
      
      // Apply the rate limiting middleware specifically to the SMS sending route
      // or globally if desired (app.use(smsLimiter);)
      app.use('/api/sms/send', smsLimiter);
      
      // --- Mount API Routes ---
      app.use('/api/sms', smsRoutes);
      
      // ... error handler, server start ...
  4. Helmet:

    • Need: Set various HTTP headers to improve application security (e.g., prevent XSS, clickjacking, sniffing).

    • Implementation: Use the helmet middleware.

    • Example (src/index.js):

      bash
      npm install helmet
      javascript
      // src/index.js
      // ... other imports ...
      const helmet = require('helmet');
      
      const app = express();
      // ... PORT ...
      
      // --- Security Middleware ---
      app.use(helmet()); // Apply Helmet defaults
      
      // ... other middleware (express.json, etc.) ...
      // ... rate limiter, routes, error handler, server start ...
  5. Dependency Security: Regularly audit dependencies for known vulnerabilities using npm audit and update them (npm update). Use npm audit fix to automatically fix compatible vulnerabilities.

8. Testing the Application

Testing ensures your application works as expected and helps catch regressions.

  1. Unit Testing (infobipService.js):

    • Goal: Test the sendSms function in isolation, without making actual API calls.
    • Tool: Use a testing framework like Jest or Mocha with an assertion library like Chai.
    • Technique: Mock the axios.post method using Jest's mocking capabilities (jest.mock('axios')) or a library like Sinon.
    • Example (Conceptual Jest):
      javascript
      // src/services/infobipService.test.js
      const axios = require('axios');
      const { sendSms } = require('./infobipService');
      
      jest.mock('axios'); // Mock the axios module
      
      describe('Infobip Service - sendSms', () => {
          const domain = 'test.api.infobip.com';
          const apiKey = 'test-api-key';
          const to = '+1234567890';
          const message = 'Test message';
      
          beforeEach(() => {
              // Reset mocks before each test
              axios.post.mockReset();
          });
      
          it('should call Infobip API with correct parameters and return success details', async () => {
              const mockSuccessResponse = {
                  data: {
                      messages: [{ messageId: 'msg-123', status: { name: 'ACCEPTED', description: 'Ok' } }]
                  },
                  status: 200,
              };
              axios.post.mockResolvedValue(mockSuccessResponse); // Mock successful response
      
              const result = await sendSms(domain, apiKey, to, message);
      
              expect(axios.post).toHaveBeenCalledTimes(1);
              expect(axios.post).toHaveBeenCalledWith(
                  `https://${domain}/sms/2/text/advanced`,
                  expect.objectContaining({ // Check parts of the body
                      messages: expect.arrayContaining([
                          expect.objectContaining({ to: to, text: message })
                      ])
                  }),
                  { headers: { 'Authorization': `App ${apiKey}`, 'Content-Type': 'application/json', 'Accept': 'application/json' } }
              );
              expect(result).toEqual({
                  success: true,
                  messageId: 'msg-123',
                  status: 'ACCEPTED',
                  description: 'Ok',
                  details: mockSuccessResponse.data,
              });
          });
      
          it('should throw an error if Infobip API returns an error status', async () => {
              const mockErrorResponse = {
                  response: { // Axios error structure
                      status: 401,
                      data: { requestError: { serviceException: { text: 'Invalid credentials' } } }
                  }
              };
              axios.post.mockRejectedValue(mockErrorResponse); // Mock failed response
      
              await expect(sendSms(domain, apiKey, to, message))
                  .rejects
                  .toThrow('Invalid credentials'); // Check if the correct error is thrown
          });
      
          // Add more tests for missing parameters, network errors, etc.
      });
      (Requires setting up Jest: npm install --save-dev jest and adding ""test"": ""jest"" to package.json scripts)
  2. Integration Testing (API Endpoint):

    • Goal: Test the /api/sms/send endpoint, including routing, request handling, and interaction with the (mocked) service.
    • Tool: Use a library like Supertest along with your testing framework (Jest/Mocha).
    • Technique: Start your Express app instance and make HTTP requests to it using Supertest. Mock the infobipService.sendSms function to avoid actual API calls during these tests.
    • Example (Conceptual Jest + Supertest):
      javascript
      // src/routes/smsRoutes.test.js
      const request = require('supertest');
      const app = require('../index'); // Import your configured Express app
      const infobipService = require('../services/infobipService');
      
      // Mock the service function used by the route
      jest.mock('../services/infobipService');
      
      describe('POST /api/sms/send', () => {
          beforeEach(() => {
              // Reset mocks and potentially environment variables if needed
              infobipService.sendSms.mockReset();
              // Ensure required env vars are set for the test context if checked in route
              process.env.INFOBIP_API_KEY = 'fake-key';
              process.env.INFOBIP_BASE_URL = 'fake.url.com';
          });
      
          it('should return 200 OK and success message on valid request', async () => {
              const mockServiceResult = { success: true, messageId: 'msg-abc', status: 'SENT' };
              infobipService.sendSms.mockResolvedValue(mockServiceResult); // Mock service success
      
              const response = await request(app)
                  .post('/api/sms/send')
                  .send({ to: '+15551234', message: 'Integration test' });
      
              expect(response.statusCode).toBe(200);
              expect(response.body.success).toBe(true);
              expect(response.body.message).toBe('SMS submitted successfully.');
              expect(response.body.details).toEqual(mockServiceResult);
              expect(infobipService.sendSms).toHaveBeenCalledWith(
                  process.env.INFOBIP_BASE_URL,
                  process.env.INFOBIP_API_KEY,
                  '+15551234',
                  'Integration test'
              );
          });
      
          it('should return 400 Bad Request if ""to"" is missing', async () => {
              const response = await request(app)
                  .post('/api/sms/send')
                  .send({ message: 'Missing recipient' });
      
              expect(response.statusCode).toBe(400);
              expect(response.body.success).toBe(false);
              expect(response.body.message).toContain(""'to' (destination phone number)""); // Check error message
              expect(infobipService.sendSms).not.toHaveBeenCalled();
          });
      
          it('should return 500 Internal Server Error if service throws an error', async () => {
              const errorMessage = 'Infobip unavailable';
              infobipService.sendSms.mockRejectedValue(new Error(errorMessage)); // Mock service failure
      
              const response = await request(app)
                  .post('/api/sms/send')
                  .send({ to: '+15559876', message: 'Test failure' });
      
              expect(response.statusCode).toBe(500);
              expect(response.body.success).toBe(false);
              expect(response.body.message).toBe(`Failed to send SMS: ${errorMessage}`);
          });
      
          // Add tests for missing message, unconfigured credentials, etc.
      });
      (Requires npm install --save-dev supertest)
  3. Manual Testing:

    • Run the application locally: npm start.
    • Use a tool like Postman, Insomnia, or curl to send POST requests to http://localhost:3000/api/sms/send (or your configured port).
    • Request Body (JSON):
      json
      {
        ""to"": ""YOUR_TEST_PHONE_NUMBER"", // Use your Infobip registered number for free trial
        ""message"": ""Hello from Node.js!""
      }
    • Headers: Set Content-Type to application/json.
    • Verify the response from your API and check if the SMS arrives on your test phone. Test with invalid inputs, missing fields, and potentially trigger error conditions (e.g., temporarily remove API key from .env).

9. Deployment Considerations

Moving your application from local development to a live environment requires several considerations.

  1. Platform Choice:

    • PaaS (Platform as a Service): Services like Heroku, Render, Fly.io, Google App Engine, AWS Elastic Beanstalk. Often simpler to manage, handle scaling, load balancing.
    • IaaS (Infrastructure as a Service): Virtual machines (e.g., AWS EC2, Google Compute Engine, Azure VMs). More control but requires manual setup of OS, Node.js, reverse proxy, process manager, etc.
    • Containers: Docker packaged applications deployed to container orchestrators like Kubernetes (e.g., GKE, EKS, AKS) or simpler container platforms (e.g., AWS Fargate, Google Cloud Run, Azure Container Apps). Provides consistency across environments.
  2. Environment Variables:

    • Crucial: Do not deploy your .env file.
    • Use the deployment platform's mechanism for setting environment variables (e.g., Heroku Config Vars, Render Environment Groups, AWS Secrets Manager, Kubernetes Secrets).
    • Set NODE_ENV=production. This often enables optimizations in Express and other libraries.
    • Ensure INFOBIP_API_KEY, INFOBIP_BASE_URL, and PORT (if required by the platform) are configured in the production environment.
  3. Process Management:

    • Need: Keep your Node.js application running reliably, restart it if it crashes, and manage multiple instances for load balancing.
    • Tool: Use a process manager like PM2.
    • Usage: Install PM2 globally on the server (npm install pm2 -g) and start your app: pm2 start src/index.js --name infobip-sms-api. PM2 handles clustering, logging, monitoring, and restarts. Many PaaS platforms handle this automatically.
  4. Reverse Proxy:

    • Need: Handle incoming HTTP requests, terminate SSL (HTTPS), perform load balancing, serve static files (if any), and provide caching.
    • Tool: Nginx or Apache are common choices if managing your own server/VM. PaaS platforms usually provide this as part of their service.
    • Configuration: Set up the reverse proxy to forward requests to your Node.js application running on its local port (e.g., 3000). Configure SSL certificates (e.g., using Let's Encrypt).
  5. Logging in Production:

    • Configure your logging library (Winston, Pino) to output structured logs (JSON).
    • Route logs to standard output (stdout/stderr) so the deployment platform or process manager (like PM2) can capture them.
    • Consider integrating with external logging services (e.g., Datadog, Logz.io, Papertrail, AWS CloudWatch Logs, Google Cloud Logging) for aggregation, searching, and alerting.
  6. Monitoring and Alerting:

    • Monitor application health (using the /health endpoint), performance (CPU, memory, response times), and error rates.
    • Use platform-provided monitoring or integrate with services like Datadog, New Relic, Sentry (for error tracking), Prometheus/Grafana.
    • Set up alerts for critical issues (e.g., high error rate, service down, high resource usage).
  7. Database (If Used):

    • Use a managed database service provided by your cloud provider (e.g., AWS RDS, Google Cloud SQL, Azure Database for PostgreSQL/MySQL, MongoDB Atlas) for reliability, backups, and scaling.
    • Configure connection strings securely using environment variables.
  8. Build Process (Optional):

    • For larger applications, you might use a build tool like Webpack or transpile code (e.g., TypeScript to JavaScript) before deployment. Ensure your deployment process includes the build step.
  9. Dockerfile Example (Conceptual): If using Docker:

    dockerfile
    # Dockerfile
    
    # 1. Choose base Node.js image (use specific LTS version)
    FROM node:18-alpine AS base
    
    # 2. Set working directory
    WORKDIR /usr/src/app
    
    # 3. Copy package files and install dependencies (separate layer for caching)
    COPY package*.json ./
    RUN npm ci --only=production --ignore-scripts --prefer-offline
    
    # 4. Copy application code
    COPY . .
    
    # 5. Set Node environment to production
    ENV NODE_ENV=production
    # Expose the port the app runs on (should match PORT env var)
    EXPOSE 3000
    
    # 6. Command to run the application
    CMD [ ""node"", ""src/index.js"" ]
    
    # --- Optional: Build stage if needed ---
    # FROM base AS build
    # RUN npm install --ignore-scripts --prefer-offline
    # RUN npm run build # If you have a build script
    
    # --- Final stage using production dependencies ---
    # FROM node:18-alpine
    # WORKDIR /usr/src/app
    # COPY package*.json ./
    # RUN npm ci --only=production --ignore-scripts --prefer-offline
    # COPY --from=build /usr/src/app/dist ./dist # Copy built files if applicable
    # COPY --from=build /usr/src/app/src ./src # Or copy source if no build step
    # ENV NODE_ENV=production
    # EXPOSE 3000
    # CMD [ ""node"", ""dist/index.js"" ] # Or src/index.js

10. Conclusion and Next Steps

Congratulations! You have successfully built a Node.js and Express application capable of sending SMS messages via the Infobip API.

Recap:

  • We set up a Node.js project with Express, Axios, and Dotenv.
  • We created a modular infobipService to handle API interactions.
  • We built an Express API endpoint (/api/sms/send) to receive requests and trigger SMS sending.
  • We implemented essential error handling, logging basics, and security measures (environment variables, input validation, rate limiting, Helmet).
  • We discussed testing strategies (unit, integration, manual) and deployment considerations.

Potential Next Steps:

  • Implement Delivery Reports: Infobip can send delivery reports (DLRs) back to your application via webhooks. Set up an endpoint to receive these reports and update the status of sent messages (e.g., in your SmsLog database). See Infobip Delivery Reports Documentation.
  • Advanced Validation: Implement more robust phone number validation (e.g., using libraries like libphonenumber-js) and potentially check against allowed country codes or formats.
  • Message Templating: If sending similar messages frequently, implement a templating engine (like Handlebars or EJS) to manage message content.
  • Queueing: For high-volume sending or to decouple the API response from the actual sending process, implement a message queue (e.g., RabbitMQ, Redis BullMQ, AWS SQS). Your API endpoint would add a job to the queue, and a separate worker process would pick up jobs and call the infobipService.
  • User Interface: Build a simple front-end application (using React, Vue, Angular, or plain HTML/JS) that interacts with your new API endpoint.
  • Enhanced Logging/Monitoring: Integrate with production-grade logging and monitoring services as discussed in the deployment section.
  • Database Integration: Fully implement the optional database logging schema (SmsLog) for tracking and auditing.
  • Two-Way SMS: Explore Infobip's features for receiving incoming SMS messages if needed for your application.

This project provides a solid foundation for integrating SMS functionality into various applications, from sending simple notifications to more complex communication workflows. Remember to consult the official Infobip API Documentation for the most up-to-date information and features.

Frequently Asked Questions

How to send SMS with Node.js and Express?

This guide details building a Node.js application with Express to send SMS messages using the Infobip API. It covers project setup, handling errors, security best practices, and deployment, providing a robust way to add SMS features to your apps. You'll create an Express API endpoint that takes a phone number and message, securely sends the SMS via Infobip, and manages any potential errors.

What is the Infobip API used for?

The Infobip API is a third-party service that allows you to send SMS messages programmatically. This guide uses it to enable your Node.js application to send transactional notifications, alerts, or other communications via SMS without directly exposing your API credentials in client-side code.

Why use dotenv in a Node.js project?

Dotenv is used to load environment variables from a `.env` file into `process.env`. This is crucial for keeping sensitive data, like API keys, out of your source code and protecting them from being exposed publicly, especially in version control systems like Git.

When should I use Axios in Node.js?

Axios is a promise-based HTTP client. It's useful when your Node.js application needs to make external HTTP requests, such as interacting with a third-party API. In this project, Axios sends data to the Infobip API to trigger SMS messages.

How to install necessary dependencies for sending SMS?

You'll need to install `express`, `axios`, and `dotenv`. Use the command `npm install express axios dotenv` in your terminal after initializing your Node.js project with `npm init -y`. This adds the packages to your project and allows you to use them in your code.

What is the project structure for sending SMS with Node.js?

The guide recommends creating folders: `src`, `src/routes`, and `src/services`. Key files include `index.js` (entry point), `smsRoutes.js` (API endpoint), `infobipService.js` (Infobip interaction logic), `.env` (for API keys), and `.gitignore`. This promotes modularity and maintainability.

How to set up Infobip API credentials?

Find your API Key and Base URL in your Infobip account dashboard (Developers -> API Keys). In your project's `.env` file, add `INFOBIP_API_KEY=your_key` and `INFOBIP_BASE_URL=your_base_url` replacing the placeholders. Never commit this file to version control, as it contains private information.

What is the role of smsRoutes.js?

The `smsRoutes.js` file defines the API endpoint (e.g., `/api/sms/send`) that your application will use to send SMS messages. It handles incoming requests, validates the data, calls the `infobipService`, and sends back a response to the client.

How to implement error handling for Infobip SMS?

The provided code includes error handling to check for missing parameters, manages Infobip API response errors, and uses try-catch blocks to manage unexpected issues. Robust logging using tools like Winston or Pino is recommended for production environments.

What security measures should I consider for sending SMS messages?

Protect API keys with `.env` files and environment variables in production. Validate and sanitize user inputs using libraries like `express-validator`. Implement rate limiting with `express-rate-limit` and utilize the `helmet` package for additional HTTP header security.

How do I test my Infobip SMS integration?

The guide recommends unit testing the `infobipService.js` by mocking `axios` using Jest or similar tools. For integration testing, use Supertest with your Express app and mock the service calls. Finally, test manually using Postman or `curl` with valid and invalid input data.

What are the deployment considerations for a Node.js SMS app?

Consider using a PaaS (like Heroku or Render), IaaS (like AWS EC2), or containerization (Docker and Kubernetes). Manage environment variables securely, use a process manager (like PM2), set up a reverse proxy (like Nginx), and configure proper logging and monitoring.

How to implement retry mechanisms with Axios?

Use `axios-retry` to handle network errors and temporary Infobip issues. Only retry on specific status codes (like 5xx errors), avoiding infinite retries. Configure exponential backoff to increase delay between attempts and configure the retry logic as per the axios-retry documentation for optimal results.

Can I log sent SMS messages to a database?

While optional, logging SMS attempts to a database is recommended for production. The guide provides an example schema using Prisma and suggests storing details like recipient, message content, Infobip response, and attempt status. Technologies like SQLite, PostgreSQL, or MongoDB are suitable for persistence.

What are potential next steps after setting up basic SMS sending?

Consider implementing delivery reports using Infobip's webhooks, add more robust phone number validation, utilize message templating, implement a message queue for high volume, add a user interface, or explore two-way SMS features offered by Infobip.