Send SMS Messages with Node.js, Express, and Vonage
This guide provides a complete walkthrough for building a production-ready Node.js application using the Express framework to send SMS messages via the Vonage Messages API. We will cover everything from initial project setup to deployment and verification.
By the end of this tutorial, you will have a simple but robust API endpoint capable of accepting requests and sending SMS messages programmatically.
Project Overview and Goals
What We're Building:
We will create a simple REST API endpoint using Node.js and Express. This endpoint will accept a destination phone number, a sender ID (your Vonage number), and a message text, then use the Vonage Messages API to send the SMS.
Problem Solved:
This project provides a foundational backend service for applications needing to send programmatic SMS notifications, alerts, verification codes, or other messages without requiring manual intervention.
Technologies Used:
- Node.js: A JavaScript runtime environment for building server-side applications. Chosen for its performance, large ecosystem (npm), and asynchronous nature, suitable for I/O-bound tasks like API calls.
- Express: A minimal and flexible Node.js web application framework. Chosen for its simplicity, speed, and widespread adoption for building APIs in Node.js.
- Vonage Messages API: A unified API from Vonage for sending messages across various channels (SMS, MMS, WhatsApp, etc.). We'll use its SMS capability. Chosen for its reliability and developer-friendly SDK.
- dotenv: A module to load environment variables from a
.env
file intoprocess.env
. Chosen for securely managing credentials and configuration outside of the codebase.
System Architecture:
The basic flow is straightforward:
Client (e.g., Web App, Mobile App, curl)
|
| HTTP POST Request (/api/send-sms)
v
+---------------------------+
| Node.js / Express Server |
| - API Endpoint Logic | ----> Vonage SDK Call ----> Vonage Messages API
| - Vonage Service Module | <---- API Response <---- (Success/Failure)
+---------------------------+
|
| HTTP Response (JSON Success/Failure)
v
Client
Prerequisites:
- Node.js and npm: Installed on your system (LTS version recommended). Verify with
node -v
andnpm -v
. - Vonage API Account: Sign up for a free account at Vonage. You'll get some free credits to start.
- Vonage Phone Number: You need to rent a Vonage virtual number capable of sending SMS. You can do this through the Vonage Dashboard.
- Basic understanding of JavaScript and Node.js.
- A text editor or IDE (e.g._ VS Code).
- (Optional) API testing tool: Like Postman or
curl
.
Final Outcome:
A running Node.js Express application with a single API endpoint (POST /api/send-sms
) that securely sends an SMS message using your Vonage account credentials and returns a JSON response indicating success or failure.
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 your project_ then navigate into it.
mkdir vonage-sms-sender cd vonage-sms-sender
-
Initialize npm Project: This command creates a
package.json
file_ which tracks your project's metadata and dependencies. The-y
flag accepts the default settings.npm init -y
-
Enable ES Modules: We'll use modern
import
/export
syntax. Open yourpackage.json
file and add the following line at the top level:// package.json { ""name"": ""vonage-sms-sender""_ ""version"": ""1.0.0""_ ""description"": """"_ ""main"": ""index.js""_ ""type"": ""module""_ // <-- Add this line ""scripts"": { ""start"": ""node index.js""_ // <-- Add or modify this script ""test"": ""echo \""Error: no test specified\"" && exit 1"" }_ ""keywords"": []_ ""author"": """"_ ""license"": ""ISC"" }
We also added a
start
script for convenience. -
Install Dependencies: We need Express for the web server_ the Vonage Server SDK to interact with the API_ and dotenv to manage environment variables.
npm install express @vonage/server-sdk dotenv
express
: Web framework.@vonage/server-sdk
: Official Vonage SDK for Node.js.dotenv
: Loads environment variables from a.env
file.
-
Create Project Structure: Let's create the necessary files and directories.
# On macOS / Linux touch index.js .env .gitignore mkdir lib touch lib/vonageService.js # On Windows (using PowerShell) New-Item index.js_ .env_ .gitignore -ItemType File New-Item lib -ItemType Directory New-Item lib/vonageService.js -ItemType File
index.js
: The main entry point for our Express application..env
: Stores sensitive credentials (API keys_ etc.). Never commit this file to Git..gitignore
: Specifies intentionally untracked files that Git should ignore (like.env
andnode_modules
).lib/
: A directory to hold our service modules.lib/vonageService.js
: A module dedicated to handling interactions with the Vonage API.
-
Configure
.gitignore
: Open the.gitignore
file and add the following lines to prevent committing sensitive information and unnecessary files:# .gitignore # Dependencies node_modules/ # Environment variables .env # Sensitive Keys private.key # Log files *.log # Operating system files .DS_Store Thumbs.db
Your project setup is now complete.
2. Implementing core functionality (Vonage Service)
We'll encapsulate the Vonage API interaction logic within its own module (lib/vonageService.js
). This promotes separation of concerns and makes the code easier to maintain and test.
-
Edit
lib/vonageService.js
: Open the file and add the following code:// lib/vonageService.js import { Vonage } from '@vonage/server-sdk'; import path from 'path'; // Import path module import { fileURLToPath } from 'url'; // To handle __dirname in ES modules // Recreate __dirname equivalent for ES Modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Ensure required environment variables are loaded if (!process.env.VONAGE_APPLICATION_ID || !process.env.VONAGE_PRIVATE_KEY_PATH) { console.error('Error: Missing Vonage Application ID or Private Key Path in .env file.'); // In a real app_ you might throw an error or handle this more gracefully process.exit(1); } // Construct the absolute path to the private key // Assumes the path in .env is relative to the project root const privateKeyPath = path.resolve(__dirname_ '..'_ process.env.VONAGE_PRIVATE_KEY_PATH); // Initialize Vonage client const vonage = new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID_ privateKey: privateKeyPath // Use the resolved path }_ { debug: process.env.NODE_ENV !== 'production' // Enable debug logging only in development }); /** * Sends an SMS message using the Vonage Messages API. * @param {string} to - The recipient phone number in E.164 format (e.g._ +14155552671). * @param {string} from - The Vonage virtual number or Alphanumeric Sender ID. * @param {string} text - The message content. * @returns {Promise<object>} - A promise that resolves with the Vonage API response. * @throws {Error} - Throws an error if the API call fails. */ export const sendSms = async (to, from, text) => { console.log(`Attempting to send SMS from ${from} to ${to}`); try { const response = await vonage.messages.send({ message_type: 'text', to: to, from: from, channel: 'sms', text: text }); console.log(`Vonage API response: ${JSON.stringify(response)}`); // The Messages API returns a 202 Accepted on success // The actual delivery status comes later via webhook (if configured) // For this simple sender, we primarily check if the request was accepted. if (response.message_uuid) { console.log(`Message submitted successfully with UUID: ${response.message_uuid}`); return { success: true, message_uuid: response.message_uuid }; } else { // This case might indicate an issue before sending (e.g., auth) // though usually errors are thrown. console.error('Message submission failed, no message_uuid received.', response); throw new Error('Vonage message submission failed.'); } } catch (error) { console.error('Error sending SMS via Vonage:', error?.response?.data || error.message || error); // Re-throw a more specific error or return a structured error object throw new Error(`Failed to send SMS: ${error?.response?.data?.title || error.message}`); } };
Explanation:
- We import the
Vonage
class from the SDK. - We use
path
andfileURLToPath
to correctly resolve the path to the private key file relative to the project root, which is crucial when running the application. - We check if the necessary environment variables (
VONAGE_APPLICATION_ID
,VONAGE_PRIVATE_KEY_PATH
) are present before initializing the client. - We initialize the
Vonage
client using the Application ID and the path to the private key file stored in environment variables. We enable debug mode ifNODE_ENV
is not 'production'. - The
sendSms
function is anasync
function that takes the recipient (to
), sender (from
), and message (text
) as arguments. - It uses
vonage.messages.send()
which is the method for the newer Messages API. We specify thechannel
assms
andmessage_type
astext
. - Basic logging is added to track the process.
- It returns an object containing the
message_uuid
on success. Vonage's Messages API typically returns202 Accepted
if the request format is valid, meaning it's queued for sending. Delivery confirmation usually comes via webhooks (not implemented in this basic guide). - Error handling is included using a
try...catch
block to log detailed errors from the Vonage SDK or API and throw a new, more user-friendly error.
- We import the
3. Building the API Layer
Now, let's set up the Express server and create the API endpoint that will use our vonageService
.
-
Edit
index.js
: Open the main application file and add the following code:// index.js import express from 'express'; import 'dotenv/config'; // Load .env variables into process.env import { sendSms } from './lib/vonageService.js'; // Basic input validation helper (can be expanded) const validateInput = (to, from, text) => { if (!to || !from || !text) { return 'Missing required fields: to, from, text'; } // Basic E.164 format check (can be more robust) if (!/^\+[1-9]\d{1,14}$/.test(to)) { // Use single quotes for the inner string to avoid complex escaping return 'Invalid \'to\' phone number format. Use E.164 (e.g., +14155552671)'; } // Add more checks if needed (e.g., text length) return null; // No validation errors }; const app = express(); // Middleware app.use(express.json()); // Parse JSON request bodies app.use(express.urlencoded({ extended: true })); // Parse URL-encoded request bodies // Environment Variable Check const PORT = process.env.PORT || 3000; const VONAGE_NUMBER = process.env.VONAGE_NUMBER; if (!VONAGE_NUMBER) { console.error('Error: VONAGE_NUMBER is not set in the .env file.'); process.exit(1); } // --- API Endpoints --- // Health Check Endpoint app.get('/health', (req, res) => { res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() }); }); // Send SMS Endpoint app.post('/api/send-sms', async (req, res) => { const { to, message, from: senderFromRequest } = req.body; // Use sender from request body if provided, otherwise default to .env variable const fromNumber = senderFromRequest || VONAGE_NUMBER; // Validate input const validationError = validateInput(to, fromNumber, message); if (validationError) { console.warn(`Validation Error: ${validationError}`, req.body); return res.status(400).json({ success: false, message: validationError }); } try { const result = await sendSms(to, fromNumber, message); // SendSms returns { success: true, message_uuid: '...' } on success res.status(202).json(result); // 202 Accepted aligns with Vonage API behavior } catch (error) { // Log the detailed error internally console.error(`Failed API request to /api/send-sms: ${error.message}`); // Return a generic error message to the client res.status(500).json({ success: false, message: 'Failed to send SMS due to an internal server error.' }); } }); // --- Start Server --- app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); console.log(`API endpoint available at http://localhost:${PORT}/api/send-sms`); console.log(`Health check available at http://localhost:${PORT}/health`); console.log(`Using Vonage Number: ${VONAGE_NUMBER}`); if (process.env.NODE_ENV !== 'production') { console.warn('Running in development mode. Vonage SDK debug logging might be enabled.'); } }); // Export app for potential testing export { app };
Explanation:
- We import
express
, load environment variables usingdotenv/config
, and import oursendSms
function. - We initialize an Express application instance (
app
). - Middleware
express.json()
andexpress.urlencoded()
are used to parse incoming request bodies. - We define the
PORT
(defaulting to 3000) and retrieve theVONAGE_NUMBER
from environment variables, exiting if the number isn't set. - A simple
/health
endpoint is added – a best practice for monitoring. - The
POST /api/send-sms
endpoint is defined:- It extracts
to
,message
, and optionallyfrom
from the JSON request body (req.body
). - It defaults to using the
VONAGE_NUMBER
from the.env
file but allows overriding it via the request body if needed. - Input Validation: A basic
validateInput
helper checks for required fields and performs a simple E.164 format check on theto
number. It returns a 400 Bad Request if validation fails. - It calls our
sendSms
function within atry...catch
block. - On success, it returns a
202 Accepted
status code along with the result fromsendSms
(containing themessage_uuid
). - On failure, it logs the specific error internally and returns a generic
500 Internal Server Error
to the client, avoiding leakage of sensitive error details.
- It extracts
- Finally,
app.listen
starts the server on the specified port and logs helpful startup messages. We also exportapp
to potentially allow importing it for integration tests.
- We import
4. Integrating with Vonage (Credentials and Configuration)
This is a crucial step involving your Vonage account and API credentials.
-
Sign Up/Log In to Vonage: Go to the Vonage API Dashboard and log in or sign up.
-
Find Your API Key and Secret (For Reference - Not Used in this Guide): On the main Dashboard page, you'll see your API Key and API Secret. While this guide uses Application ID and Private Key for the Messages API, it's good to know where these are if you ever use older Vonage APIs.
-
Create a Vonage Application:
- Navigate to ""Your applications"" in the left-hand menu.
- Click ""+ Create a new application"".
- Give your application a name (e.g.,
My Node SMS Sender
). - Click ""Generate public and private key"". This will automatically download a
private.key
file. Save this file securely. - Security Warning: For simplicity in this development guide, we will place the downloaded
private.key
file directly in the root of our project directory (vonage-sms-sender/private.key
). This approach carries security risks. This key file contains sensitive credentials and must NEVER be committed to version control (like Git). Ensure it is listed in your.gitignore
file. For production environments, use more secure methods like environment variables or secret management services (discussed in Section 7). - Scroll down to ""Capabilities"". Click the toggle to enable ""Messages"".
- You'll see fields for ""Inbound URL"" and ""Status URL"". For sending SMS only, these are not strictly required, but the Vonage platform often expects them. You can enter placeholder URLs like
https://example.com/webhooks/inbound
andhttps://example.com/webhooks/status
. If you later want to receive status updates or inbound messages, you'll need functional webhook URLs (potentially using ngrok during development). - Click ""Generate new application"".
-
Get Application ID: After creating the application, you'll be taken to its configuration page. Copy the Application ID – you'll need this for your
.env
file. -
Link Your Vonage Number:
- Still on the application's configuration page, scroll down to the ""Linked numbers"" section.
- Click the ""Link"" button next to the Vonage virtual number you want to use for sending SMS. If you don't have one, you'll need to go to ""Numbers"" > ""Buy numbers"" first.
- Confirm the link. Any SMS sent using this Application ID can now originate from this linked number.
-
Set Default SMS API (Important!):
- Go to your main Vonage Dashboard settings: Click your username/icon in the top right -> Settings.
- Scroll down to the ""API settings"" section.
- Find ""Default SMS Setting"".
- Ensure ""Messages API"" is selected. If it's set to ""SMS API"", change it to ""Messages API"".
- Click ""Save changes"". This ensures your account uses the correct API infrastructure for the SDK methods we are using.
-
Configure Environment Variables (
.env
file): Open the.env
file in your project root and add your credentials. Remember to replace the placeholder values with your actual credentials and number.# .env # --- Vonage Credentials --- # Found in your Vonage Application settings after creating an application VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID_HERE # Path to the private key file downloaded when creating the Vonage Application. # Relative to the project root directory. VONAGE_PRIVATE_KEY_PATH=./private.key # --- Vonage Number --- # Your Vonage virtual number linked to the application, in E.164 format VONAGE_NUMBER=+15551234567 # --- Application Settings --- # Port for the Express server to listen on PORT=3000 # --- Node Environment --- # Set to 'production' in your deployment environment # Set to 'development' or leave unset for local development (enables SDK debug logs) # NODE_ENV=development
Explanation of Variables:
VONAGE_APPLICATION_ID
: The unique ID for the Vonage Application you created. Found on the application's page in the Vonage dashboard.VONAGE_PRIVATE_KEY_PATH
: The relative path from your project root to theprivate.key
file you downloaded. We assume it's in the root (./private.key
).VONAGE_NUMBER
: Your purchased or assigned Vonage virtual phone number, linked to the application, in E.164 format (e.g.,+14155552671
). This will be the defaultfrom
number.PORT
: The port your Express server will run on.3000
is a common default for Node.js development.NODE_ENV
: Standard Node.js variable. Setting it toproduction
disables Vonage SDK debug logs and can be used for other environment-specific configurations.
Security Reminder: Ensure your
.env
file and yourprivate.key
file are listed in your.gitignore
file and are never committed to version control. Store theprivate.key
file securely. In production environments, use secure mechanisms provided by your hosting platform (e.g., environment variable management, secrets managers) instead of deploying a.env
file or the key file directly.
5. Implementing Error Handling and Logging
We've already added basic error handling and logging, but let's refine it slightly and discuss strategies.
- Consistent Error Structure: Our API currently returns
{ success: false, message: '...' }
. This is a good start. In more complex applications, you might standardize error responses further, potentially including error codes. - Logging Levels: We are using
console.log
,console.warn
, andconsole.error
. For production, consider using a dedicated logging library like Winston or Pino. These enable:- Different log levels (debug, info, warn, error, fatal).
- Structured logging (JSON format), making logs easier to parse and analyze by machines.
- Configurable outputs (console, file, external logging services).
- Example (Conceptual using Pino):
// import pino from 'pino'; // const logger = pino({ level: process.env.LOG_LEVEL || 'info' }); // ... // logger.info(`Sending SMS from ${from} to ${to}`); // logger.error({ err: error, reqId: req.id }, 'Failed to send SMS');
- Vonage Error Details: The
catch
block invonageService.js
attempts to log detailed errors from Vonage (error?.response?.data
). These often contain specific reasons for failure (e.g., invalid number, insufficient funds, throttling). Log these details internally for debugging but avoid exposing them directly to the client in the API response for security. - Retry Mechanisms: For sending a single SMS, automatic retries might not be necessary unless you encounter transient network issues before hitting the Vonage API. If the request reaches Vonage and returns a
202 Accepted
, Vonage handles the delivery attempts. Retries are more critical for receiving webhooks or making sequences of API calls. If needed, libraries likeasync-retry
can implement exponential backoff.
6. Database Schema and Data Layer
This specific application does not require a database. It's a stateless API endpoint that accepts a request, interacts with an external service (Vonage), and returns a response.
If you were building features like storing message history, user accounts, or scheduled messages, you would need to add a database (e.g., PostgreSQL, MongoDB) and a data layer (using an ORM like Prisma or Sequelize, or native drivers).
7. Adding Security Features
Security is paramount, especially when dealing with APIs and external services.
-
Input Validation and Sanitization:
- We added basic validation in
index.js
(validateInput
). This should be expanded based on requirements. - Validate phone number formats rigorously (consider using libraries like
libphonenumber-js
). - Validate message length against SMS standards (typically 160 GSM-7 characters, less for UCS-2).
- Sanitize inputs if they are ever stored or reflected back, although for just sending SMS, the primary risk is invalid data causing API errors.
- We added basic validation in
-
Environment Variables for Secrets:
- Done. We are correctly using
.env
forVONAGE_APPLICATION_ID
,VONAGE_PRIVATE_KEY_PATH
, andVONAGE_NUMBER
.
- Done. We are correctly using
-
Rate Limiting:
- Protect your API from abuse and ensure fair usage. Implement rate limiting on the
/api/send-sms
endpoint. - Use middleware like
express-rate-limit
:npm install express-rate-limit
// index.js import rateLimit from 'express-rate-limit'; // ... other imports import express from 'express'; // Ensure express is imported if not already import 'dotenv/config'; import { sendSms } from './lib/vonageService.js'; // ... other potential imports like path, url const app = express(); // Apply rate limiting BEFORE other middleware/routes for the target endpoint const smsLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many SMS requests from this IP, please try again after 15 minutes', standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); // Apply the rate limiting middleware specifically to the SMS endpoint app.use('/api/send-sms', smsLimiter); // ... (rest of middleware like express.json()) app.use(express.json()); app.use(express.urlencoded({ extended: true })); // ... (rest of the code: validateInput, env checks, routes, etc.) // Basic input validation helper (can be expanded) const validateInput = (to, from, text) => { if (!to || !from || !text) { return 'Missing required fields: to, from, text'; } if (!/^\+[1-9]\d{1,14}$/.test(to)) { return 'Invalid \'to\' phone number format. Use E.164 (e.g., +14155552671)'; } return null; // No validation errors }; // Environment Variable Check const PORT = process.env.PORT || 3000; const VONAGE_NUMBER = process.env.VONAGE_NUMBER; if (!VONAGE_NUMBER) { console.error('Error: VONAGE_NUMBER is not set in the .env file.'); process.exit(1); } // --- API Endpoints --- app.get('/health', (req, res) => { res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() }); }); app.post('/api/send-sms', async (req, res) => { // This route now has the smsLimiter applied const { to, message, from: senderFromRequest } = req.body; const fromNumber = senderFromRequest || VONAGE_NUMBER; const validationError = validateInput(to, fromNumber, message); if (validationError) { console.warn(`Validation Error: ${validationError}`, req.body); return res.status(400).json({ success: false, message: validationError }); } try { const result = await sendSms(to, fromNumber, message); res.status(202).json(result); } catch (error) { console.error(`Failed API request to /api/send-sms: ${error.message}`); res.status(500).json({ success: false, message: 'Failed to send SMS due to an internal server error.' }); } }); // --- Start Server --- app.listen(PORT, () => { console.log(`Server listening on port ${PORT}`); // ... other console logs }); // Export app for potential testing export { app };
- Adjust
windowMs
andmax
according to your expected usage patterns and security requirements.
- Protect your API from abuse and ensure fair usage. Implement rate limiting on the
-
API Endpoint Protection:
- Currently, the endpoint is open. In a real-world scenario, you would protect it. Options include:
- API Key: Require clients to send a secret API key in a header (
X-API-Key
). Validate it on the server. - JWT (JSON Web Tokens): For user-based applications, authenticate users and issue JWTs. Require a valid JWT for API access.
- IP Whitelisting: If only specific servers/clients should access the API.
- API Key: Require clients to send a secret API key in a header (
- Currently, the endpoint is open. In a real-world scenario, you would protect it. Options include:
-
Common Vulnerabilities:
- Denial of Service (DoS): Rate limiting helps mitigate this.
- Credential Leakage: Handled by using environment variables and
.gitignore
. Ensure secure handling ofprivate.key
. - Input Injection: Less relevant here as inputs are passed to a trusted SDK, but always validate input formats strictly.
-
HTTPS:
- Always run your Node.js application behind a reverse proxy (like Nginx or Caddy) that handles TLS/SSL termination, ensuring all traffic is served over HTTPS in production.
8. Handling Special Cases
Real-world SMS sending involves nuances:
- Phone Number Formatting: Always require and validate numbers in E.164 format (
+
followed by country code and number, e.g.,+14155552671
,+447700900000
). This is the standard Vonage expects. Libraries likelibphonenumber-js
can help parse and validate various formats. - Character Encoding and Limits:
- Standard SMS messages use GSM-7 encoding (160 characters).
- Using non-GSM characters (like emojis or certain accented letters) switches to UCS-2 encoding, reducing the limit to 70 characters per SMS segment.
- Long messages are split into multiple segments (concatenated SMS). Vonage handles this, but it affects pricing (you pay per segment). Be mindful of message length.
- Alphanumeric Sender IDs: In some countries, you can replace the
from
number with a custom string (e.g., ""MyBrand""). This requires pre-registration with Vonage and is subject to country-specific regulations. They cannot receive replies. Our code allows passingfrom
, so it supports this if configured in Vonage. - Delivery Reports (DLRs): Our current implementation sends the SMS and gets a
message_uuid
. To confirm actual delivery to the handset, you need to configure the ""Status URL"" in your Vonage Application and build a webhook endpoint to receive delivery reports from Vonage. - Opt-Outs/STOP Keywords: Regulations (like TCPA in the US) require handling STOP keywords. Vonage can manage this automatically at the account or number level if configured. Ensure compliance with local regulations.
- International Sending: Be aware of different regulations, costs, and potential sender ID restrictions when sending internationally.
9. Implementing Performance Optimizations
For this simple application, performance bottlenecks are unlikely within the Node.js code itself. The main latency will be the network call to the Vonage API.
- Vonage Client Initialization: We correctly initialize the
Vonage
client once when the module loads, not per request. This avoids unnecessary setup overhead. - Asynchronous Operations: Node.js and the Vonage SDK are asynchronous, preventing the server from blocking while waiting for the API response.
- Load Testing: Use tools like
k6
,autocannon
, or ApacheBench (ab
) to test how your API endpoint handles concurrent requests. This helps identify bottlenecks (e.g., CPU limits, network saturation, Vonage API rate limits).# Example using autocannon (install with: npm install -g autocannon) autocannon -c 100 -d 10 -p 10 -m POST -H ""Content-Type=application/json"" -b '{""to"": ""+1YOUR_TEST_NUMBER"", ""message"": ""Load test""}' http://localhost:3000/api/send-sms
- Caching: Not applicable for the core SMS sending logic.
- Resource Usage: Monitor CPU and memory usage of your Node.js process under load using tools like
pm2
or platform-specific monitoring.
10. Adding Monitoring, Observability, and Analytics
To understand how your service behaves in production:
-
Health Checks:
- Done. We have a basic
/health
endpoint. Monitoring systems (like AWS CloudWatch, Prometheus, UptimeRobot) can ping this endpoint to verify the service is running.
- Done. We have a basic
-
Logging:
- As discussed in Section 5, implement structured logging (e.g., Pino, Winston) and centralize logs using a service (e.g., Datadog Logs, AWS CloudWatch Logs, ELK stack). This allows searching, filtering, and alerting based on log content.
-
Error Tracking:
- Integrate an error tracking service like Sentry or Datadog APM. These automatically capture unhandled exceptions and report them with stack traces and context, making debugging much faster.
- Example (Conceptual Sentry):
# Install Sentry SDK npm install @sentry/node @sentry/tracing
// index.js (at the very top) import * as Sentry from '@sentry/node'; import * as Tracing from '@sentry/tracing'; import express from 'express'; // Assuming express is used // Make sure to import your app instance if it's defined elsewhere // import { app } from './app'; // Example if app is in another file // Initialize Sentry - Must be done before initializing Express app if possible Sentry.init({ dsn: 'YOUR_SENTRY_DSN', integrations: [ new Sentry.Integrations.Http({ tracing: true }), // Instrument HTTP requests // Initialize Express integration *after* Express app is created // new Tracing.Integrations.Express({ app }), // Pass the app instance here later ], tracesSampleRate: 1.0, // Capture 100% of transactions for performance monitoring }); // Create Express app *after* Sentry.init if not imported const app = express(); // Add Express integration *after* app is created // Ensure this is done before request handlers if you need app instance access Sentry.addIntegration(new Tracing.Integrations.Express({ app })); // RequestHandler creates a separate execution context using domains // Must be the first middleware app.use(Sentry.Handlers.requestHandler()); // TracingHandler creates a trace for every incoming request // Must come after requestHandler and before routes app.use(Sentry.Handlers.tracingHandler()); // ... (your regular middleware like express.json(), rate limiter, etc.) app.use(express.json()); // Example: app.use('/api/send-sms', smsLimiter); // Apply rate limiter // ... (your routes) // Example route: app.get('/', (req, res) => res.send('Hello')); // Example: app.post('/api/send-sms', async (req, res) => { /* ... */ }); // The Sentry error handler must be before any other error middleware // and after all controllers/routes app.use(Sentry.Handlers.errorHandler()); // Optional: Your own custom error handling middleware (after Sentry's) app.use(function onError(err, req, res, next) { // The error id is attached to `res.sentry` to be returned // and optionally displayed to the user for support. res.statusCode = 500; res.end(res.sentry + '\n'); }); // ... (Start server: app.listen(...))