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 intoprocess.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.
-
Create Project Directory: Open your terminal or command prompt and create a new directory for the project. Navigate into it.
mkdir node-infobip-sms cd node-infobip-sms
-
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 apackage.json
file.npm init -y
(The
-y
flag automatically accepts the defaults) -
Install Dependencies: We need Express for the server framework, Axios for HTTP requests, and dotenv for managing environment variables.
npm install express axios dotenv
This command downloads the packages and adds them to your
package.json
andnode_modules
directory. -
Create Project Structure: Set up a basic structure for clarity:
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.
-
Configure
.gitignore
: Addnode_modules
and.env
to your.gitignore
file to prevent committing them to version control.# .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
-
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.
-
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.// 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 mockingaxios
. - 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
.
-
Configure Environment Variables: Open the
.env
file and add your Infobip API Key and Base URL. Never commit this file to Git.# .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:
- Log in to your Infobip Portal.
- Navigate to the ""Developers"" section or look for ""API Keys"" in your account settings.
- Create a new API key if you don't have one. Copy the key value.
- Your Base URL is displayed alongside your API key (it's unique to your account). Copy this URL without the
https://
. - For more details, see the Infobip API documentation.
- How to find credentials:
-
Edit
src/routes/smsRoutes.js
: Define the route handler.// 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
-
Edit
src/index.js
: Set up the Express application, load environment variables, enable JSON body parsing, and mount the routes.// 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;
-
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 bynpm install
is recommended for reproducibility.// 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 runningnpm install
or usenpm list --depth=0
. (If you want to use thedev
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
andINFOBIP_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 bydotenv
). 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 includehttps://
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. - Authentication: The
Authorization: App YOUR_API_KEY
header is used for authentication, as implemented ininfobipService.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.
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 a500 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.
- The
- Logging:
- We use
console.log
for informational messages (starting server, sending SMS) andconsole.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.
- We use
- 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
):// 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.
-
Concept: Store details about each SMS attempt (success or failure) in a persistent data store.
-
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.
-
Example Schema (Conceptual / 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]) }
-
Implementation Steps (if using Prisma):
- Install Prisma:
npm install prisma --save-dev
andnpm install @prisma/client
- Initialize Prisma:
npx prisma init --datasource-provider postgresql
(adjust provider if needed). This createsprisma/schema.prisma
and updates.env
withDATABASE_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
orinfobipService.js
to interact withprisma.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' } })
.
- Before calling
- Install Prisma:
-
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.
-
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). -
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
):npm install express-validator
// 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.)
-
-
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
):npm install express-rate-limit
// 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 ...
-
-
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
):npm install helmet
// 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 ...
-
-
Dependency Security: Regularly audit dependencies for known vulnerabilities using
npm audit
and update them (npm update
). Usenpm audit fix
to automatically fix compatible vulnerabilities.
8. Testing the Application
Testing ensures your application works as expected and helps catch regressions.
-
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):
(Requires setting up Jest:
// 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. });
npm install --save-dev jest
and adding""test"": ""jest""
topackage.json
scripts)
- Goal: Test the
-
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):
(Requires
// 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. });
npm install --save-dev supertest
)
- Goal: Test the
-
Manual Testing:
- Run the application locally:
npm start
. - Use a tool like Postman, Insomnia, or
curl
to sendPOST
requests tohttp://localhost:3000/api/sms/send
(or your configured port). - Request Body (JSON):
{ ""to"": ""YOUR_TEST_PHONE_NUMBER"", // Use your Infobip registered number for free trial ""message"": ""Hello from Node.js!"" }
- Headers: Set
Content-Type
toapplication/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
).
- Run the application locally:
9. Deployment Considerations
Moving your application from local development to a live environment requires several considerations.
-
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.
-
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
, andPORT
(if required by the platform) are configured in the production environment.
- Crucial: Do not deploy your
-
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.
-
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).
-
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.
-
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).
- Monitor application health (using the
-
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.
-
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.
-
Dockerfile Example (Conceptual): If using Docker:
# 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.