code examples

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

Build a Bulk SMS API with Fastify and Infobip Node.js SDK

Learn how to build a scalable bulk SMS broadcast API using Fastify and the Infobip Node.js SDK. Complete guide covering setup, error handling, security, and deployment.

Build a Bulk SMS API with Fastify and Infobip Node.js SDK

This guide details how to build a robust and scalable API service using Fastify and the Infobip Node.js SDK to send bulk SMS messages. You'll learn everything from project setup and core integration to error handling, security, and deployment best practices.

This service solves the common need for applications to send programmatic, high-volume SMS notifications, alerts, or marketing messages reliably. Fastify offers exceptional performance and a developer-friendly plugin architecture, making it ideal for high-throughput API applications. Infobip provides a powerful and globally reachable communication platform with well-documented APIs and SDKs, simplifying the integration process for sending SMS messages at scale.

Project Overview and Goals

Goal: Create a Fastify API endpoint that accepts a list of phone numbers and message text, then uses the Infobip API to send that message to all recipients efficiently.

Key Features:

  • Accept POST requests with a message and multiple recipient phone numbers
  • Use the Infobip API via the official Node.js SDK for sending SMS
  • Securely manage Infobip API credentials
  • Include basic request validation and error handling
  • Provide logging for monitoring and troubleshooting
  • Offer guidance on testing, deployment, and common pitfalls

Technology Stack:

  • Runtime: Node.js (v18 or later recommended; LTS support until April 2025)
  • Framework: Fastify
  • Messaging API: Infobip SMS API
  • Infobip SDK: @infobip-api/sdk
  • Environment Variables: dotenv
  • Rate Limiting: @fastify/rate-limit (optional but recommended)

System Architecture:

Note: The following Mermaid diagram requires specific platform support or JavaScript libraries to render. It will appear as a raw code block in standard Markdown.

mermaid
graph LR
    Client[Client Application] -->|POST /broadcast/sms| API{Fastify API Service};
    API -->|Send SMS Request| Infobip[Infobip SMS API];
    Infobip -->|SMS Delivery| Recipients[End User Phones];
    Infobip -->|API Response/Status| API;
    API -->|Response (e.g., 202 Accepted)| Client;

Prerequisites:

  • Node.js and npm (or yarn) installed
  • An active Infobip account with API access – create a free trial account here
  • Your Infobip API Key and Base URL (found in your Infobip account dashboard)
  • Basic familiarity with Node.js, REST APIs, and terminal commands

1. Project Setup: Initialize Your Fastify SMS Service

Initialize your Node.js project and install the necessary dependencies.

  1. Create Project Directory:

    bash
    mkdir fastify-infobip-sms
    cd fastify-infobip-sms
  2. Initialize Node.js Project:

    bash
    npm init -y

    This creates a package.json file.

  3. Install Dependencies:

    bash
    npm install fastify @infobip-api/sdk dotenv
    • fastify – The core web framework
    • @infobip-api/sdk – The official Infobip Node.js SDK for interacting with their APIs
    • dotenv – To load environment variables from a .env file for secure credential management
  4. Install Development Dependencies (Optional but Recommended):

    bash
    npm install --save-dev nodemon pino-pretty
    • nodemon – Automatically restarts the server during development when files change
    • pino-pretty – Makes Fastify's default JSON logs more human-readable during development
  5. Configure package.json Scripts: Open your package.json file and add or modify the scripts section:

    json
    // package.json
    {
      // … other configurations
      "scripts": {
        "start": "node src/server.js",
        "dev": "nodemon src/server.js | pino-pretty"
      },
      // … other configurations
      "type": "module" // Add this line to enable ES Module syntax
    }
    • start – Runs the application directly with Node
    • dev – Runs the application using nodemon for auto-reloading and pipes logs through pino-pretty
    • "type": "module" – Enables modern ES Module syntax (import/export)
  6. Create Project Structure:

    bash
    mkdir src
    mkdir src/routes
    mkdir src/plugins
    touch src/server.js
    touch src/routes/broadcast.js
    touch src/plugins/infobip.js
    touch .env
    touch .gitignore
    • src/ – Contains your main application code
    • src/server.js – The main entry point for your Fastify server
    • src/routes/ – Holds route definitions
    • src/plugins/ – Contains Fastify plugins (like your Infobip client setup)
    • .env – Stores environment variables (API keys, etc.). Never commit this file!
    • .gitignore – Specifies intentionally untracked files that Git should ignore
  7. Configure .gitignore: Add the following lines to your .gitignore file to prevent sensitive information and unnecessary files from being committed:

    text
    # .gitignore
    node_modules
    .env
    npm-debug.log
    *.log
  8. Configure Environment Variables (.env): Open the .env file and add your Infobip credentials. You can find these in your Infobip account dashboard under API Keys management. The Base URL is specific to your account.

    dotenv
    # .env
    INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY
    INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL
    SENDER_ID=InfoSMS # Or your registered sender ID/number
    PORT=3000 # Optional: Port for the server to listen on
    LOG_LEVEL=info # Optional: Set log level (debug, info, warn, error)
    • INFOBIP_API_KEY – Your secret API key
    • INFOBIP_BASE_URL – The specific API endpoint URL provided by Infobip for your account
    • SENDER_ID – The phone number or alphanumeric ID that will appear as the sender of the SMS. This often needs to be registered or approved by Infobip depending on the destination country regulations.
    • PORT – The port your Fastify server will listen on. Defaults to 3000 if not set.
    • LOG_LEVEL – Controls the logging verbosity

    Why Environment Variables? Storing credentials directly in code is a major security risk. Environment variables allow you to configure the application differently for development, testing, and production without changing the code, and keep secrets out of version control. dotenv loads these variables into process.env during development (when called early in server.js). In production, set these variables directly in your deployment environment.

2. Implementing Core Functionality (Infobip Plugin)

Create a Fastify plugin to initialize and encapsulate the Infobip SDK client, making it reusable across your application.

  1. Create the Infobip Plugin: Open src/plugins/infobip.js and add the following code:

    javascript
    // src/plugins/infobip.js
    import fp from 'fastify-plugin';
    import { Infobip, AuthType } from '@infobip-api/sdk';
    
    async function infobipPlugin(fastify, options) {
      // Environment variables should be loaded by dotenv in server.js
      const apiKey = process.env.INFOBIP_API_KEY;
      const baseUrl = process.env.INFOBIP_BASE_URL;
    
      if (!apiKey || !baseUrl) {
        throw new Error('Infobip API Key or Base URL missing in environment variables.');
      }
    
      try {
        const infobipClient = new Infobip({
          baseUrl: baseUrl,
          apiKey: apiKey,
          authType: AuthType.ApiKey, // Specify API Key authentication
        });
    
        // Decorate Fastify instance with the client
        fastify.decorate('infobip', infobipClient);
        fastify.log.info('Infobip client initialized successfully.');
    
      } catch (error) {
        fastify.log.error('Failed to initialize Infobip client:', error);
        throw new Error('Infobip client initialization failed.');
      }
    }
    
    // Export the plugin using fastify-plugin to avoid encapsulation issues
    // and make the decorator available globally
    export default fp(infobipPlugin, {
      name: 'infobip-client' // Optional: name for the plugin
    });
    • Environment Variables: Relies on dotenv being called in server.js to populate process.env
    • Error Handling: Checks if required environment variables are present
    • new Infobip(…): Initializes the SDK client using the API key and base URL. AuthType.ApiKey explicitly tells the SDK how to authenticate.
    • fastify.decorate('infobip', …): Attaches the initialized infobipClient to the Fastify instance (and request/reply objects) under the name infobip. This makes it accessible in your route handlers via fastify.infobip or request.infobip.
    • fp(infobipPlugin, …): Wraps the plugin function using fastify-plugin. This ensures that decorators added by this plugin (like .infobip) are available globally within the Fastify instance, not just within the scope of the plugin itself.

3. Building the API Layer (Broadcast Route)

Create the API endpoint that receives broadcast requests and uses your Infobip plugin.

  1. Create the Broadcast Route Handler: Open src/routes/broadcast.js and add the following:

    javascript
    // src/routes/broadcast.js
    
    // Sender ID is read from environment variables (loaded in server.js)
    const senderId = process.env.SENDER_ID || 'InfoSMS'; // Fallback sender ID
    
    // Define schema for request body validation
    const broadcastSchema = {
      body: {
        type: 'object',
        required: ['to', 'text'],
        properties: {
          to: {
            type: 'array',
            minItems: 1,
            items: {
              type: 'string',
              // Basic pattern for E.164 format (adjust if needed)
              // Example: +14155552671 – This is a basic check; Infobip performs stricter validation
              // Double backslashes are needed to escape \ in a JS string for the regex engine
              pattern: "^\\+?[1-9]\\d{1,14}$"
            }
          },
          text: {
            type: 'string',
            minLength: 1,
            maxLength: 1600 // Max SMS length (consider multi-part messages)
          },
          // Optional: Add other Infobip parameters like notifyUrl, callbackData, etc.
          // notifyUrl: { type: 'string', format: 'uri' },
          // callbackData: { type: 'string', maxLength: 4000 }
        },
        additionalProperties: false
      },
      // Optional: Define response schema
      response: {
        202: {
          description: 'Request accepted for processing',
          type: 'object',
          properties: {
            message: { type: 'string' },
            bulkId: { type: 'string' },
            messages: {
              type: 'array',
              items: {
                type: 'object',
                properties: {
                  to: { type: 'string' },
                  status: { type: 'object', properties: { name: { type: 'string'}, description: { type: 'string'} } }, // Simplified status
                  messageId: { type: 'string' }
                }
              }
            }
          }
        }
        // Add schemas for 4xx/5xx errors if desired for OpenAPI spec
      }
    };
    
    async function broadcastRoutes(fastify, options) {
    
      fastify.post('/broadcast/sms', { schema: broadcastSchema }, async (request, reply) => {
        const { to, text /*, notifyUrl, callbackData */ } = request.body;
    
        // Map the 'to' array to the format expected by Infobip SDK
        const destinations = to.map(phoneNumber => ({ to: phoneNumber }));
    
        const payload = {
          messages: [
            {
              from: senderId,
              destinations: destinations,
              text: text,
              // Optional parameters:
              // notifyUrl: notifyUrl, // If you want delivery reports pushed
              // callbackData: callbackData // Custom data for tracking
            },
          ],
          // Optional: Add bulkId if you want to manage it
          // bulkId: `my-campaign-${Date.now()}`
        };
    
        try {
          fastify.log.info(`Sending bulk SMS to ${destinations.length} recipients.`);
    
          // Access the Infobip client decorated in the plugin
          // Use the generic `send` method which maps to /sms/2/text/advanced
          const response = await fastify.infobip.channels.sms.send(payload);
    
          fastify.log.info({ bulkId: response.data.bulkId, recipients: destinations.length }, 'Infobip bulk SMS request accepted.');
    
          // Infobip usually accepts the request quickly (2xx)
          // Actual delivery status comes later (via webhook if configured, or polling)
          // Return 202 Accepted to indicate the request was received
          // You might want to return the bulkId and initial message statuses from the response
          reply.code(202).send({
            message: 'SMS broadcast request accepted by Infobip.',
            bulkId: response.data.bulkId,
            messages: response.data.messages // Initial statuses
          });
    
        } catch (error) {
          fastify.log.error({ err: error?.response?.data || error.message }, 'Error sending SMS via Infobip');
    
          // Check if the error is from Infobip API (AxiosError structure)
          if (error.response && error.response.data) {
            // Forward Infobip's error details
            reply.code(error.response.status || 500).send({
              message: 'Infobip API error.',
              details: error.response.data
            });
          } else {
            // Generic server error
            reply.code(500).send({ message: 'Internal Server Error while sending SMS.' });
          }
        }
      });
    }
    
    export default broadcastRoutes;
    • Schema Validation: Define a broadcastSchema using Fastify's built-in AJV support. This automatically validates incoming request bodies. The pattern uses double backslashes (\\) as required within a JavaScript string literal to represent literal backslashes for the regex engine.
    • Payload Mapping: Transform the to array into the destinations array structure required by the Infobip SDK's send method
    • senderId: Use the SENDER_ID from the environment variables (loaded in server.js)
    • API Call: fastify.infobip.channels.sms.send(payload) makes the API call
    • Response Handling: Returns 202 Accepted along with the bulkId and initial message statuses from Infobip's synchronous response
    • Error Handling: The try…catch block handles potential errors, logging details and attempting to forward specific Infobip error information

4. Integrating with Third-Party Services (Infobip Setup)

This section focuses on configuring the Fastify application to use the Infobip service correctly, building upon the initial setup in Section 1.

  1. Obtain Infobip Credentials:

    • Log in to your Infobip Portal: https://portal.infobip.com/
    • API Key: Navigate to the "Developers" or "API Keys" section. Create or retrieve your API key. Copy the key value securely.
    • Base URL: Find your account-specific Base URL, usually on the same API Keys page or in API documentation (e.g., xxxxxx.api.infobip.com).
    • Sender ID: Determine the appropriate Sender ID (alphanumeric like "MyCompany" or a virtual number like +14155550100) based on your target countries and regulations. Check the "Numbers" or "Senders" section in the portal. Registration or approval might be required.
  2. Configure Environment Variables:

    • As detailed in Section 1, Step 8, ensure your .env file (for local development) or your production environment variables include the correct INFOBIP_API_KEY, INFOBIP_BASE_URL, and SENDER_ID.
  3. Secure API Keys:

    • Local: The .gitignore file (Section 1, Step 7) prevents committing the .env file
    • Production: Crucially, do not deploy the .env file. Set the environment variables (INFOBIP_API_KEY, INFOBIP_BASE_URL, SENDER_ID, PORT, LOG_LEVEL, etc.) directly through your hosting platform's configuration interface (e.g., Heroku Config Vars, AWS Secrets Manager, Kubernetes Secrets). This keeps secrets out of your codebase and version control.
  4. Plugin and Route Usage:

    • The Infobip plugin (src/plugins/infobip.js) reads INFOBIP_API_KEY and INFOBIP_BASE_URL from process.env to initialize the SDK
    • The broadcast route (src/routes/broadcast.js) reads SENDER_ID from process.env
  5. Fallback Mechanisms (Consideration):

    • For critical systems, consider adding retry logic (Section 5) or integrating a secondary SMS provider as a fallback if Infobip experiences issues

5. Error Handling, Logging, and Retry Strategies

Fastify uses Pino for efficient logging. Refine the basic error handling you've already added.

  1. Logging:

    • Default Logging: Fastify logs requests and responses. Your fastify.log.info and fastify.log.error calls add context.
    • Development: pino-pretty (via npm run dev) enhances readability
    • Production: Use the default JSON format for log aggregation (Datadog, Splunk, ELK). Control verbosity via the LOG_LEVEL environment variable (info, warn, error).
    • Server Start:
      bash
      # Production start
      npm start
      # Or using PM2 for clustering/management
      # npm install -g pm2
      # pm2 start src/server.js --name fastify-infobip-sms -i max
  2. Error Handling Strategy:

    • Validation Errors: Handled by Fastify schema (400 Bad Request)
    • Infobip API Errors: The catch block in src/routes/broadcast.js logs Infobip's detailed error (error.response.data) and forwards the status code and details to the client
    • Other Server Errors: Generic 500 Internal Server Error
    • Consistent Error Format: Aim for a standard JSON error response
  3. Retry Mechanisms (Advanced):

    • Recommended: Use a background job queue (e.g., BullMQ with Redis, RabbitMQ) for robust retries, especially for bulk operations:
      1. API endpoint validates, enqueues job, returns 202 Accepted
      2. Worker process picks up job, calls Infobip
      3. Worker handles retries (for 5xx, network errors, rate limits) with exponential backoff
      4. Worker marks job complete or failed (for success or 4xx errors)
    • Simple In-Handler Retry (Use with Caution): A small retry loop within the handler for transient errors (limit attempts and delay). Less robust than a queue.
  4. Testing Error Scenarios:

    • Test with invalid inputs (numbers, missing fields)
    • Simulate Infobip errors (invalid key, rate limits) by temporarily changing .env or mocking the SDK in tests (Section 13)

6. Creating a Database Schema and Data Layer (Optional Enhancement)

A database (PostgreSQL, MongoDB, etc.) with an ORM (Prisma, Sequelize) can add features like storing recipient lists, tracking broadcast jobs (bulkId, status), message templates, and delivery statuses (via webhooks). This adds complexity. For this guide, focus on direct API interaction.

7. Security Best Practices for SMS APIs

Protect your API and credentials.

  1. Environment Variables: Keep secrets out of code and Git. Use platform secrets management in production (Section 4).

  2. Input Validation: Implemented via Fastify schema (Section 3). Ensures data conforms to expectations (e.g., E.164 pattern, length limits).

  3. Rate Limiting: Protect against abuse and control costs:

    • Install: npm install @fastify/rate-limit
    • Register in src/server.js:
    javascript
    // src/server.js (inside buildServer)
    // import rateLimit from '@fastify/rate-limit'; // Add import
    // await fastify.register(rateLimit, {
    //   max: 100, // Adjust based on needs
    //   timeWindow: '1 minute'
    //   // Optional: Use Redis for distributed limiting
    // });
    • Adjust max and timeWindow appropriately
  4. HTTPS: Enforce HTTPS in production (usually handled by load balancers or PaaS)

  5. Authentication/Authorization: Protect the /broadcast/sms endpoint itself:

    • Implement API key checking (Authorization: Bearer YOUR_KEY) using @fastify/auth or similar
    • Use JWT for user-based authentication if applicable
  6. Helmet: Set security-related HTTP headers:

    • Install: npm install @fastify/helmet
    • Register: await fastify.register(helmet); in src/server.js
  7. Dependency Updates: Regularly run npm audit and update dependencies (npm update)

  8. Least Privilege (Infobip Key): Use Infobip API keys with the minimum required permissions if possible

8. SMS-Specific Considerations and Compliance

SMS specifics to consider:

  1. Character Limits and Encoding: GSM-7 (160 characters per segment) vs. UCS-2 (70 characters per segment for non-GSM characters). Long messages use multiple segments and cost more. Inform users. Infobip handles segmentation.
  2. Phone Number Formatting: Use E.164 format (+14155552671). Rely on Infobip's validation but handle their specific errors (EC_INVALID_DESTINATION_ADDRESS).
  3. Sender ID Restrictions: Alphanumeric IDs aren't allowed everywhere (e.g., US). Numeric IDs often provide better deliverability. Use appropriate, registered IDs. Handle EC_ILLEGAL_SENDER errors.
  4. Delivery Reports (DLRs): Delivery is asynchronous. Use Infobip webhooks (notifyUrl parameter in API call) or poll the DLR endpoint (GET /sms/1/reports) to get final status (DELIVERED, FAILED, etc.). Webhooks are preferred for real-time updates.
  5. Opt-outs and Compliance: Handle STOP or UNSUBSCRIBE requests (often managed by Infobip if configured; otherwise implement yourself) per regulations (TCPA, GDPR).
  6. Time Zones and Scheduling: Use Infobip's sendAt parameter to schedule messages appropriately for recipient time zones, especially for marketing.

9. Performance Optimization for High-Volume SMS

Make the service handle high load:

  1. Batching API Calls: Your use of the /sms/2/text/advanced endpoint with the destinations array is batching. Break very large lists (millions) into multiple API calls (e.g., 1,000 recipients per call), ideally managed via a job queue.
  2. Asynchronous Processing: Use a message queue (BullMQ, RabbitMQ) to decouple API requests from Infobip calls. API returns 202 Accepted quickly; workers handle Infobip calls, retries, and rate limiting. This is the most impactful optimization for high throughput.
  3. Efficient Logging: Pino is fast. Avoid excessive logging in handlers. Use JSON format in production; adjust LOG_LEVEL.
  4. Node.js Event Loop Monitoring: Use clinic.js to detect event loop blocks
  5. Infrastructure Scaling: Run multiple instances (PM2 cluster, Docker/Kubernetes replicas) behind a load balancer. Scale databases or Redis if used.
  6. Infobip API Limits: Be aware of your account's MPS (Messages Per Second) limit. Implement outbound rate limiting in workers if needed to avoid 429 Too Many Requests errors.

10. Monitoring, Observability, and Analytics

Understand service health and performance:

  1. Health Checks: Add a /health endpoint checking essential services (Infobip client initialization, DB connection if used). Use for liveness and readiness probes (see example in src/server.js, Section 13).
  2. Performance Metrics: Use @fastify/metrics to expose Prometheus metrics (/metrics) for request rates, latency, error rates, queue size, and custom metrics (infobip_sms_sent_total). Visualize in Grafana.
  3. Error Tracking: Integrate Sentry, Datadog APM, etc., using Fastify plugins (@sentry/node, etc.) for real-time error reporting and alerting.
  4. Distributed Tracing: Use OpenTelemetry (@autotelic/fastify-opentelemetry) in complex microservice setups
  5. Infobip Analytics: Use the Infobip Portal's reporting features (correlate with bulkId)
  6. Dashboards and Alerting: Create dashboards (Grafana, Datadog) and set alerts (Prometheus Alertmanager, Datadog Monitors) for key metrics (error rates, latency, health check failures, Infobip error codes, queue backlog).

11. Troubleshooting Common Infobip API Issues

Common issues:

  • Error: Infobip API Key or Base URL missing… – Check .env or production environment variables
  • Infobip 401 AUTHENTICATION_ERROR – Invalid API key. Verify key and ensure it's active.
  • Infobip 400 BAD_REQUEST – Invalid payload. Check details: EC_INVALID_DESTINATION_ADDRESS (bad number format), EC_INVALID_SENDER_OR_FROM_FIELD (bad sender ID), empty or excessively long text. Use E.164 format. Verify sender ID registration.
  • Infobip 429 MESSAGE_LIMIT_EXCEEDED – Exceeded account MPS. Slow down requests (implement outbound rate limiting in workers).
  • Infobip 5xx Server Error – Temporary Infobip issue. Retry with backoff (via queue). Check Infobip status page.
  • Messages Sent but Not Received – Check DLRs (webhooks or polling) for errors (EC_ABSENT_SUBSCRIBER, EC_ANTI_SPAM_REJECTION, EC_INSUFFICIENT_FUNDS). Verify number, check balance, sender ID validity, and country regulations. Contact Infobip support with messageId.
  • SDK Version Compatibility – Pin SDK version in package.json. Review changelogs before upgrading.
  • Cost Management – Monitor Infobip costs. Multi-part messages cost more. Clean lists.
  • No Delivery Reports – Implement webhooks (notifyUrl) or polling if final status is needed

12. Deployment and CI/CD Strategies

Get your service into production:

  1. Build Step: Not needed for plain JavaScript unless using TypeScript (npm run build via tsc) or bundling.

  2. Deployment Environments:

    • PaaS (Heroku, Render, Fly.io): Simple. Push code, configure environment variables via dashboard, define Procfile (web: npm start). Handles load balancing and HTTPS.
    • Containers (Docker): Build image, push to registry, deploy to Kubernetes, ECS, etc. Requires Dockerfile. Ensure NODE_ENV=production, HOST=0.0.0.0, and do not copy .env into the image. Pass secrets via runtime environment.
    • VMs (EC2, GCE): Manual setup (Node, Nginx/proxy, PM2, firewall)
  3. Process Management (PM2): Use PM2 for restarts, clustering, and logging in VM or non-containerized environments:

    • npm install -g pm2
    • pm2 start src/server.js --name fastify-infobip-sms -i max (cluster mode)
    • pm2 list, pm2 logs, pm2 reload
  4. CI/CD Pipeline (GitHub Actions Example):

    • Create .github/workflows/deploy.yml to automate build, test, and deploy on push to main
    • Steps: Checkout code, setup Node, npm ci (clean install), lint, test (npm test), deploy (e.g., trigger PaaS hook curl -X POST ${{ secrets.RENDER_DEPLOY_HOOK_URL }}, docker push, etc.)
    • Store secrets (RENDER_DEPLOY_HOOK_URL, DOCKER_PASSWORD) in GitHub Actions secrets
  5. Rollback Procedures: Use platform features (PaaS deploy history, Docker image tags, Kubernetes rollbacks) or manual redeploy of previous versions. Consider blue-green or canary deployments.

13. Testing Your SMS Broadcast API

Ensure correctness and reliability:

  1. Manual Verification (curl):

    • Start server: npm run dev
    • Send test requests (success case, validation errors like bad number or missing field) using curl. Verify expected responses (202 Accepted with bulkId, 400 Bad Request with error details).
    • Check Infobip portal logs and test phones
  2. Automated Testing (Unit/Integration):

    • Use tap (Fastify's default), Jest, or Node's test runner
    • Install runner: npm install --save-dev tap
    • Add test script to package.json: "test": "tap test/**/*.test.js"
    • Create test files (e.g., test/broadcast.test.js)
    • Key Techniques:
      • Export build function: Modify src/server.js to export a build function that creates the Fastify instance without starting it (see final server.js code below)
      • Mocking: In tests, replace app.infobip.channels.sms.send with a mock function (async (payload) => { … return mockResponse; } or throw mockError;) to isolate tests from the actual Infobip API and control scenarios
      • app.inject(): Use Fastify's injection mechanism to simulate HTTP requests directly against the app instance
  3. Test Coverage:

    • Configure runner for coverage reports (tap --coverage-report=html). Aim for high coverage.
  4. Verification Checklist:

      • Project setup complete?
      • Infobip plugin loads successfully?
      • /broadcast/sms accepts valid POST requests?
      • Validation rejects invalid payloads (400)?
      • Success triggers (mock) Infobip call?
      • Success returns 202 with bulkId?
      • Simulated Infobip errors handled gracefully?
      • Logs are informative?
      • Environment variables used correctly?
      • (Manual) Messages logged in Infobip portal?
      • (Manual) Messages received?
      • Security measures active?
      • Automated tests cover key scenarios?
      • Test coverage sufficient?

Frequently Asked Questions (FAQ)

How do I get an Infobip API key?

Log in to your Infobip Portal at https://portal.infobip.com/, navigate to the "Developers" or "API Keys" section, and create or retrieve your API key. You'll also find your account-specific Base URL on this page.

What is the maximum number of recipients per API call?

While Infobip's /sms/2/text/advanced endpoint supports batching multiple recipients, it's recommended to limit batches to around 1,000 recipients per API call for optimal performance. For larger lists, break them into multiple calls, ideally managed via a job queue.

How do I track SMS delivery status?

SMS delivery is asynchronous. Use Infobip webhooks by adding the notifyUrl parameter to your API call for real-time delivery reports, or poll the DLR endpoint (GET /sms/1/reports) using the bulkId or messageId. Webhooks are the preferred method for production systems.

Why are my messages being rejected with EC_ILLEGAL_SENDER?

This error indicates your Sender ID isn't approved or allowed in the destination country. Verify that your Sender ID is properly registered in your Infobip account and complies with the destination country's regulations. Some countries require pre-registration of sender IDs.

How can I reduce SMS costs for bulk messaging?

To reduce costs: (1) Keep messages under 160 characters for GSM-7 encoding to avoid multi-part messages, (2) Clean and validate your recipient lists to avoid failed deliveries, (3) Use appropriate message routing through Infobip's optimization features, and (4) Monitor your usage patterns to identify inefficiencies.

What should I do if I hit rate limits (429 errors)?

If you receive 429 Too Many Requests errors, you're exceeding your account's MPS (Messages Per Second) limit. Implement outbound rate limiting in your worker processes using a job queue system like BullMQ, and consider contacting Infobip support to increase your account limits if needed.

Next Steps and Additional Resources

After implementing your bulk SMS API, consider these enhancements:

  • Implement webhook handling for delivery reports to track message success rates
  • Add message templates and personalization for marketing campaigns
  • Set up monitoring dashboards using Prometheus and Grafana
  • Explore Infobip's other channels like WhatsApp, Viber, or Email for multi-channel messaging
  • Review SMS compliance requirements for your target markets (TCPA for US, GDPR for EU)

For more information, consult the official Infobip API documentation and the Fastify documentation.

Final Code Structure & Server Entry Point

Ensure your src/server.js ties everything together:

javascript
// src/server.js
import Fastify from 'fastify';
import dotenv from 'dotenv';
import infobipPlugin from './plugins/infobip.js';
import broadcastRoutes from './routes/broadcast.js';
// Import other plugins if added
// import rateLimit from 'fastify-rate-limit';
// import helmet from 'fastify-helmet';

// Load environment variables early, once
dotenv.config();

const buildServer = async () => {
  const fastify = Fastify({
    logger: {
      level: process.env.LOG_LEVEL || 'info', // Default log level from .env or 'info'
      // Consider pino-pretty transport only for development
      // transport: process.env.NODE_ENV !== 'production'
      //   ? { target: 'pino-pretty' }
      //   : undefined,
    },
  });

  // Register Plugins (Helmet, Rate Limit are examples, uncomment if used)
  // await fastify.register(helmet);
  // await fastify.register(rateLimit, { max: 100, timeWindow: '1 minute' });
  await fastify.register(infobipPlugin);

  // Register Routes
  await fastify.register(broadcastRoutes);

  // Basic health check route
  fastify.get('/health', async (request, reply) => {
     try {
       if (!fastify.infobip) {
         throw new Error('Infobip client not initialized');
       }
       // Add more checks if needed (e.g., DB ping)
       return { status: 'ok', timestamp: new Date().toISOString() };
     } catch (error) {
       fastify.log.error({ err: error }, 'Health check failed');
       reply.code(503).send({ status: 'error', message: error.message });
     }
  });

  return fastify;
};

// Export build function for testing purposes
export const build = buildServer;

// Start the server only if this script is run directly (node src/server.js)
// Avoids starting the server when imported in tests
import { fileURLToPath } from 'url';
const currentPath = fileURLToPath(import.meta.url);
// Check if the executed script path matches the current module's path
if (process.argv[1] === currentPath) {
  const start = async () => {
    let server;
    try {
      server = await buildServer();
      const port = process.env.PORT || 3000;
      // Use '0.0.0.0' to listen on all available interfaces, needed for containers or VMs accessed externally.
      const host = process.env.HOST || '127.0.0.1';
      await server.listen({ port: parseInt(port, 10), host: host });
      // Fastify's logger automatically logs the listening address on start
    } catch (err) {
      // Log the error using the logger if available, otherwise console.error
      if (server && server.log) {
        server.log.error(err, 'Error starting server');
      } else {
        console.error('Error starting server:', err);
      }
      process.exit(1);
    }
  };
  start();
}

Frequently Asked Questions

How to send bulk SMS messages with Fastify?

Build a Fastify API endpoint that accepts recipient numbers and message text, then integrates with the Infobip SMS API via its Node.js SDK. This allows your application to send high-volume SMS notifications, alerts, or marketing messages programmatically and reliably. The Fastify framework's performance and plugin architecture make it ideal for this purpose.

What is the Infobip Node.js SDK used for?

The Infobip Node.js SDK simplifies interaction with the Infobip SMS API, allowing you to send SMS messages programmatically within your Node.js application. It handles authentication, request formatting, and response parsing, making integration straightforward.

Why use Fastify for a bulk SMS API?

Fastify is chosen for its speed and extensible plugin system, which are beneficial for high-throughput applications like bulk SMS sending. Its lightweight nature and ease of use also contribute to efficient development and a smaller footprint.

When should I use a message queue for SMS sending?

For high-throughput SMS sending, a message queue like BullMQ or RabbitMQ is highly recommended. It decouples API requests from actual SMS delivery, allowing the API to respond quickly while background workers handle sending, retries, and rate limiting.

Can I use environment variables for Infobip API credentials?

Yes, storing your Infobip `API_KEY` and `BASE_URL` in environment variables (`.env` file locally, platform secrets in production) is crucial for security. This keeps sensitive information out of your codebase and allows for environment-specific configurations.

How to handle Infobip API errors in Fastify?

Implement a `try...catch` block around the `fastify.infobip.channels.sms.send` call to handle potential errors during the API request. Log error details using `fastify.log.error` and forward relevant error information to the client, including Infobip's error codes if available.

What is the role of the Infobip plugin in this architecture?

The Infobip plugin initializes and encapsulates the Infobip Node.js SDK client, making it accessible throughout the Fastify application. It uses environment variables to configure the client and provides a reusable way to interact with the Infobip API.

How to implement rate limiting for the SMS API?

Use the `fastify-rate-limit` plugin to control the rate of incoming requests to your SMS API. This protects against abuse, helps manage costs, and prevents exceeding Infobip's rate limits. Configure the `max` requests and `timeWindow` according to your needs.

How to format phone numbers for Infobip SMS API?

Always format phone numbers using the international E.164 format (e.g., +14155552671). While Infobip performs validation, ensuring correct formatting on your end minimizes errors like EC_INVALID_DESTINATION_ADDRESS.

What to do if SMS messages are sent but not delivered?

Check Infobip's delivery reports (DLRs) either via webhooks (using the notifyUrl parameter) or by polling the DLR endpoint. These reports provide detailed status information, such as DELIVERED, FAILED, or error codes like EC_ABSENT_SUBSCRIBER, which help identify the reason for non-delivery.

How to secure the bulk SMS API endpoint in Fastify?

Use environment variables, input validation, rate limiting, HTTPS, and authentication mechanisms like API keys or JWT. Consider using Helmet for setting security headers to protect against common web vulnerabilities.

What is the maximum length of an SMS message?

SMS messages are limited by character encoding: GSM-7 allows 160 characters per segment, while UCS-2 (for non-GSM characters) allows 70. Longer messages are split into multiple segments and cost more. Infobip handles this segmentation automatically.

Why is a 202 Accepted status code returned after sending SMS?

The 202 Accepted status indicates that the SMS broadcast request has been received and accepted for processing by Infobip. Actual delivery is asynchronous and happens later, the status of which can be tracked via delivery reports.

How to deploy the Fastify SMS API to production?

Use PaaS (Heroku, Render, Fly.io), containers (Docker, Kubernetes), or VMs (EC2, GCE). Ensure environment variables are set correctly in production and use process management tools like PM2. Implement a CI/CD pipeline for automated builds, tests, and deployments.

How to test the Fastify SMS API integration with Infobip?

Use manual verification with curl for basic checks. Implement automated unit and integration tests using a testing framework like Tap or Jest. Mock the Infobip API calls in tests to isolate testing logic and simulate different scenarios like successes and errors.