This guide provides a step-by-step walkthrough for adding Two-Factor Authentication (2FA) via SMS One-Time Passwords (OTP) to your Node.js Express application using the Vonage Verify API. We'll build a simple application that requests an OTP for a user's phone number and then verifies the code entered by the user. This guide serves as a starting point, demonstrating core concepts and discussing considerations for production environments.
This implementation enhances security by requiring users to possess their phone to receive a code, adding a critical layer beyond just a password.
Project Goals:
- Build a Node.js Express application demonstrating OTP request and verification.
- Securely integrate the Vonage Verify API for sending and checking OTPs.
- Implement basic error handling and user feedback mechanisms.
- Provide a foundation for adding 2FA to existing or new web applications, highlighting key differences between the demo setup and production requirements.
Technologies Used:
- Node.js: JavaScript runtime environment.
- Express.js: Minimalist web framework for Node.js.
- Vonage Verify API: Service for sending and verifying OTPs via SMS and voice calls.
- @vonage/server-sdk: Official Vonage Node.js library for interacting with the API.
- Nunjucks: Templating engine for rendering HTML views (similar to Jinja2).
- dotenv: Module for loading environment variables from a
.env
file.
System Architecture:
+-------------+ +---------------------+ +------------------+ +--------------+
| User |------>| Express Application |------>| Vonage Verify API|----->| User's Phone |
| (Browser) |<------| (Node.js Server) |<------| |<-----| (SMS/Call) |
+-------------+ +---------------------+ +------------------+ +--------------+
| 1. Enters Phone # | 2. POST /request-otp | 3. Send OTP | 4. Receives OTP
| 5. Enters OTP | 6. POST /verify-otp | 7. Check OTP
| 8. Receives Success/Fail| 9. Render Success/Error
(Note: The ASCII diagram above is functional but might render differently depending on screen width.)
Prerequisites:
- Node.js and npm (or yarn): Installed on your development machine. Download Node.js
- Vonage API Account: Sign up for a free account at Vonage API Dashboard. You'll get free credits to start.
- Vonage API Key and Secret: Found on the top of your Vonage API Dashboard after signing up.
- Basic understanding of Node.js_ Express_ and asynchronous JavaScript (Promises_ async/await).
1. Setting Up the Project
Let's initialize our Node.js project and install the necessary dependencies.
1. Create Project Directory: Open your terminal or command prompt and create a new directory for the project_ then navigate into it:
mkdir vonage-otp-express
cd vonage-otp-express
2. Initialize Node.js Project:
Create a package.json
file to manage dependencies and project metadata:
npm init -y
3. Install Dependencies:
Install Express_ the Vonage SDK_ Nunjucks for templating_ and dotenv
for managing environment variables. Note that we use Express's built-in middleware for body parsing_ so body-parser
is not needed as a separate dependency for modern Express versions.
npm install express @vonage/server-sdk nunjucks dotenv
4. Project Structure: Create the basic file and directory structure:
vonage-otp-express/
├── views/
│ ├── index.html
│ ├── verify.html
│ ├── success.html
│ └── error.html
├── .env
├── .gitignore
├── index.js
└── package.json
views/
: Contains HTML templates..env
: Stores sensitive credentials (API Key_ Secret). Never commit this file. Should be placed in the project root directory..gitignore
: Specifies files/directories Git should ignore (like.env
andnode_modules
).index.js
: Main application file containing the Express server logic.package.json
: Project configuration and dependencies.
5. Configure Environment Variables (.env
):
Create a file named .env
in the project root and add your Vonage API credentials.
.env
VONAGE_API_KEY=YOUR_API_KEY
VONAGE_API_SECRET=YOUR_API_SECRET
Replace YOUR_API_KEY
and YOUR_API_SECRET
with the actual values from your Vonage Dashboard.
6. Configure Git Ignore (.gitignore
):
Create a .gitignore
file to prevent committing sensitive information and unnecessary files:
.gitignore
node_modules/
.env
npm-debug.log
7. Basic Express Server Setup (index.js
):
Create the main application file index.js
and set up the initial Express server_ environment variables_ middleware_ and Nunjucks configuration.
index.js
// 1. Import dependencies
require('dotenv').config(); // Load environment variables from .env file first (ensure it's in project root)
const express = require('express');
const nunjucks = require('nunjucks');
const { Vonage } = require('@vonage/server-sdk');
// --- Basic Application Setup ---
const app = express();
const port = process.env.PORT || 3000; // Use port from env or default to 3000
// --- Vonage Client Initialization ---
// Check if API Key and Secret are set
if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET) {
console.error('_ Missing Vonage API Key or Secret in .env file. Please check your configuration.');
process.exit(1); // Exit if credentials are missing
}
const vonage = new Vonage({
apiKey: process.env.VONAGE_API_KEY_
apiSecret: process.env.VONAGE_API_SECRET
});
console.log('__ Vonage client initialized.');
// --- Middleware Setup ---
// Use built-in Express middleware for parsing JSON and URL-encoded bodies
app.use(express.json()); // Parse JSON bodies (as sent by API clients)
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies (as sent by HTML forms)
// --- Nunjucks Templating Engine Setup ---
nunjucks.configure('views'_ {
autoescape: true_ // Enable automatic escaping to prevent XSS
express: app // Link Nunjucks to the Express app
});
app.set('view engine'_ 'html'); // Set default view engine extension
console.log('_ Middleware and Templating Engine configured.');
// --- Temporary Storage for Request IDs ---
// !! IMPORTANT !!: This in-memory object is for demonstration purposes ONLY.
// It is NOT suitable for production environments because:
// 1. Data is lost when the server restarts.
// 2. It does not scale across multiple server instances.
// See Section 6 for discussion on production-ready storage (e.g._ Redis_ Database).
const verificationRequests = {};
console.warn(""__ Using in-memory store for verification requests. This is NOT production-ready. Data will be lost on restart."");
// --- Routes Will Go Here ---
// GET /
// POST /request-otp
// POST /verify-otp
// GET /success
// GET /error
// --- Start Server ---
app.listen(port_ () => {
console.log(`__ Server listening on http://localhost:${port}`);
});
// Basic error handling middleware (optional but recommended)
app.use((err, req, res, next) => {
console.error(""__ Global Error Handler:"", err.stack);
res.status(500).render('error.html', { errorMessage: 'An unexpected server error occurred.' });
});
Explanation:
require('dotenv').config()
: Loads variables from.env
intoprocess.env
. Ensure the.env
file is in the project root.express()
,nunjucks
: Standard setup.Vonage
Initialization: Creates the client instance using credentials from.env
. Includes a check for credential existence.- Middleware:
express.json()
andexpress.urlencoded()
are built-in Express middleware for parsing request bodies.extended: true
allows for rich objects and arrays to be encoded. - Nunjucks Config: Configures the template engine.
autoescape: true
is crucial for security. verificationRequests
: Crucially, this simple in-memory object is only for demonstration. It maps phone numbers to their pendingrequest_id
. It is explicitly not suitable for production. Section 6 discusses proper persistent storage solutions.app.listen
: Starts the server.
Run node index.js
. You should see console logs indicating successful initialization. Visiting http://localhost:3000
will initially fail as routes aren't defined yet.
2. Implementing Core Functionality (Frontend Views)
Let's create the simple HTML pages for user interaction. (Note: Inline styles are used for simplicity; external CSS is recommended for larger applications).
1. Phone Number Input Form (views/index.html
):
views/index.html
<!DOCTYPE html>
<html lang=""en"">
<head>
<meta charset=""UTF-8"">
<meta name=""viewport"" content=""width=device-width_ initial-scale=1.0"">
<title>Enter Phone Number</title>
<style> /* Basic styling for clarity */
body { font-family: sans-serif; padding: 20px; }
label, input, button { display: block; margin-bottom: 10px; }
input[type=""tel""] { padding: 8px; width: 250px; }
button { padding: 10px 15px; cursor: pointer; }
.error { color: red; margin-bottom: 15px; }
</style>
</head>
<body>
<h1>Enter Phone Number for Verification</h1>
{% if errorMessage %}
<p class=""error"">{{ errorMessage }}</p>
{% endif %}
<form method=""POST"" action=""/request-otp"">
<label for=""phoneNumber"">Phone Number (with country code, e.g., 14155552671):</label>
<input type=""tel"" id=""phoneNumber"" name=""phoneNumber"" required placeholder=""14155552671"">
<button type=""submit"">Send Verification Code</button>
</form>
</body>
</html>
2. OTP Code Input Form (views/verify.html
):
views/verify.html
<!DOCTYPE html>
<html lang=""en"">
<head>
<meta charset=""UTF-8"">
<meta name=""viewport"" content=""width=device-width_ initial-scale=1.0"">
<title>Enter Verification Code</title>
<style> /* Basic styling */
body { font-family: sans-serif; padding: 20px; }
label, input, button { display: block; margin-bottom: 10px; }
input[type=""text""] { padding: 8px; width: 100px; }
button { padding: 10px 15px; cursor: pointer; }
.error { color: red; margin-bottom: 15px; }
.info { color: grey; margin-bottom: 15px; font-size: 0.9em;}
</style>
</head>
<body>
<h1>Enter Verification Code</h1>
<p class=""info"">A code was sent to your phone number (it may take a moment). Please enter it below.</p>
{% if errorMessage %}
<p class=""error"">{{ errorMessage }}</p>
{% endif %}
<form method=""POST"" action=""/verify-otp"">
<label for=""otpCode"">Verification Code:</label>
<input type=""text"" id=""otpCode"" name=""otpCode"" required pattern=""\d{4_6}"" title=""Enter the 4 or 6 digit code"">
<!-- Hidden field to pass the request ID back -->
<input type=""hidden"" name=""requestId"" value=""{{ requestId }}"">
<input type=""hidden"" name=""phoneNumber"" value=""{{ phoneNumber }}""> <!-- Also pass phone number back -->
<button type=""submit"">Verify Code</button>
</form>
<!-- Optional: Add a cancel or resend link here (See Section 8) -->
<!-- <a href=""/request-otp?resend=true&phone={{ phoneNumber }}"">Resend Code</a> -->
</body>
</html>
3. Success Page (views/success.html
):
views/success.html
<!DOCTYPE html>
<html lang=""en"">
<head>
<meta charset=""UTF-8"">
<meta name=""viewport"" content=""width=device-width_ initial-scale=1.0"">
<title>Verification Successful</title>
<style> body { font-family: sans-serif; padding: 20px; color: green; } </style>
</head>
<body>
<h1>Verification Successful!</h1>
<p>Your phone number has been successfully verified.</p>
<p><a href="" />Start Over</a></p>
</body>
</html>
4. Error Page (views/error.html
):
views/error.html
<!DOCTYPE html>
<html lang=""en"">
<head>
<meta charset=""UTF-8"">
<meta name=""viewport"" content=""width=device-width_ initial-scale=1.0"">
<title>Verification Error</title>
<style> body { font-family: sans-serif; padding: 20px; color: red; } </style>
</head>
<body>
<h1>Verification Failed</h1>
<p>An error occurred:</p>
<p><strong>{{ errorMessage | default('An unknown error occurred.') }}</strong></p>
<p><a href="" />Try Again</a></p>
</body>
</html>
3. Building the API Layer (Express Routes)
Now, let's implement the backend logic in index.js
.
1. Root Route (GET /
):
Renders the initial phone number input form.
Add this inside index.js
where // --- Routes Will Go Here ---
is marked:
// --- Routes ---
// Render the initial page to enter phone number
app.get('/', (req, res) => {
res.render('index.html');
});
2. Request OTP Route (POST /request-otp
):
Handles phone number submission, calls Vonage to send the OTP, stores the request_id
(temporarily), and renders the verification page.
Add this below the GET /
route in index.js
:
// Handle the request to send an OTP
app.post('/request-otp', async (req, res) => {
const phoneNumber = req.body.phoneNumber;
// Basic input validation
if (!phoneNumber) {
return res.status(400).render('index.html', { errorMessage: 'Phone number is required.' });
}
// VERY Basic format check (digits only, length 10-15).
// WARNING: This regex is simplistic and NOT robust for validating international E.164 formats.
// Use a library like 'libphonenumber-js' for proper validation in production (See Section 8).
if (!/^\d{10,15}$/.test(phoneNumber)) {
return res.status(400).render('index.html', { errorMessage: 'Invalid phone number format. Please include country code without symbols (e.g., 14155552671).' });
}
console.log(`__ Requesting OTP for ${phoneNumber}...`);
try {
// Start the Vonage Verify request
// Defaults: 4-digit code, 5-minute expiry. See optional params below.
const response = await vonage.verify.start({
number: phoneNumber,
brand: ""MyAppVerification"", // Customize your brand name shown in the SMS
// code_length: '6', // Optional: Specify '6' for 6-digit code
// workflow_id: 6 // Optional: Specify workflows (e.g., 6 for SMS only)
// See Vonage docs for workflow IDs
});
// Check if the request was successful based on Vonage response structure
if (response.request_id) {
const requestId = response.request_id;
console.log(`_ OTP Request successful. Request ID: ${requestId}`);
// Store the request ID associated with the phone number (TEMPORARY IN-MEMORY STORAGE)
verificationRequests[phoneNumber] = requestId;
console.log(""__ Stored Request ID (In-Memory):"", verificationRequests);
// Render the page to enter the OTP code
res.render('verify.html', { requestId: requestId, phoneNumber: phoneNumber });
} else {
// This block might be reached if the Vonage API structure changes or returns unexpected success format
console.error(""_ Unexpected Vonage response structure:"", response);
res.render('error.html', { errorMessage: 'Failed to initiate OTP request. Unexpected response.' });
}
} catch (error) {
console.error(""__ Error requesting OTP:"", error);
// Handle specific Vonage errors and other issues
let userErrorMessage = 'An error occurred while sending the verification code.';
// Check both HTTP status from the error object and Vonage-specific status in the response data
const httpStatus = error.response?.status;
const vonageErrorData = error.response?.data;
if (vonageErrorData) {
console.error(`Vonage Error Details (HTTP Status: ${httpStatus}):`, vonageErrorData);
// Example: Handle throttling or invalid number errors specifically
if (vonageErrorData.status === '10') { // Throttled
userErrorMessage = ""Please wait a moment before requesting another code for this number."";
} else if (vonageErrorData.status === '3') { // Invalid number
userErrorMessage = ""The phone number provided is invalid. Please check and try again."";
} else {
userErrorMessage = `Verification failed: ${vonageErrorData.error_text} (Status: ${vonageErrorData.status})`;
}
} else if (httpStatus) {
userErrorMessage = `Failed to send code. Server responded with status ${httpStatus}.`;
} // Else: Generic error message remains
// Render the index page again with an error message
res.status(httpStatus || 500).render('index.html', { errorMessage: userErrorMessage });
}
});
3. Verify OTP Route (POST /verify-otp
):
Handles code submission, calls Vonage to check the code against the request_id
, and renders success or error pages.
Add this below the POST /request-otp
route in index.js
:
// Handle the OTP verification
app.post('/verify-otp', async (req, res) => {
const otpCode = req.body.otpCode;
const requestId = req.body.requestId;
const phoneNumber = req.body.phoneNumber; // Retrieve phone number
// Basic validation
if (!otpCode || !requestId || !phoneNumber) {
return res.status(400).render('error.html', { errorMessage: 'Missing OTP code, request ID, or phone number.' });
}
if (!/^\d{4,6}$/.test(otpCode)) {
return res.status(400).render('verify.html', {
errorMessage: 'Invalid OTP format. Please enter the 4 or 6 digit code.',
requestId: requestId, // Pass context back to the form
phoneNumber: phoneNumber
});
}
console.log(`__ Verifying OTP ${otpCode} for Request ID: ${requestId}...`);
try {
// Check the OTP code with Vonage
const result = await vonage.verify.check(requestId, otpCode);
console.log(""__ Vonage Check Response:"", result);
// Check the status from the Vonage response
if (result && result.status === '0') {
// Status '0' means successful verification
console.log(`_ Verification successful for Request ID: ${requestId}`);
// IMPORTANT: Clear the stored request ID after successful verification (from temporary store)
if (verificationRequests[phoneNumber]) {
delete verificationRequests[phoneNumber];
console.log(""___ Cleared Request ID for:"", phoneNumber, ""(In-Memory)"");
}
res.render('success.html');
} else {
// Verification failed (wrong code, expired, etc.)
const errorMessage = result.error_text || `Verification failed with status: ${result.status}`;
console.warn(`_ Verification failed for Request ID: ${requestId}. Reason: ${errorMessage}`);
// Render the verification page again with an error
res.status(400).render('verify.html', {
errorMessage: `Verification Failed: ${errorMessage}`,
requestId: requestId, // Pass context back to the form
phoneNumber: phoneNumber
});
}
} catch (error) {
console.error(""__ Error verifying OTP:"", error);
let userErrorMessage = 'An error occurred during verification.';
const httpStatus = error.response?.status;
const vonageErrorData = error.response?.data;
if (vonageErrorData) {
console.error(`Vonage Error Details (HTTP Status: ${httpStatus}):`, vonageErrorData);
// Example: Specific handling for error status codes from check endpoint
if (vonageErrorData.status === '16') { // Incorrect code
userErrorMessage = ""The code you entered was incorrect. Please try again."";
} else if (vonageErrorData.status === '17') { // Wrong code too many times
userErrorMessage = ""You entered the wrong code too many times. Please request a new code."";
} else if (vonageErrorData.status === '6') { // Request not found / expired
userErrorMessage = ""Verification request expired or was not found. Please request a new code."";
} else {
userErrorMessage = `Verification error: ${vonageErrorData.error_text} (Status: ${vonageErrorData.status})`;
}
// Render the verification page again with the error
res.status(httpStatus || 500).render('verify.html', {
errorMessage: userErrorMessage,
requestId: requestId, // Pass context back
phoneNumber: phoneNumber
});
} else {
// Render a generic error page if no specific Vonage error info
res.status(httpStatus || 500).render('error.html', { errorMessage: userErrorMessage });
}
}
});
// Add placeholder routes for direct access to success/error (optional)
app.get('/success', (req, res) => res.render('success.html'));
app.get('/error', (req, res) => res.render('error.html', { errorMessage: 'An unspecified error occurred.'}));
Explanation:
- Input Validation: Added basic checks. Robust validation (e.g.,
express-validator
) is recommended for production. The phone regex is noted as overly simple. vonage.verify.start
: Initiates OTP.brand
appears in SMS. Default code length (4) and expiry (5 mins) are mentioned.request_id
Storage: Temporarily stores the ID in the insecureverificationRequests
object.vonage.verify.check
: Verifies the code against the ID.- Status '0': Indicates success. Other statuses are errors.
- Error Handling:
try...catch
blocks handle errors. Specific Vonage statuses (3
,6
,10
,16
,17
) and HTTP status codes (error.response.status
) are checked for better feedback. - Cleanup: Removes the entry from
verificationRequests
on success. Essential for the demo, but real storage needs proper management (TTL, etc.).
4. Integrating with Vonage (Recap and Details)
Key integration points:
- Credentials: Securely loaded from
.env
. - SDK Initialization:
@vonage/server-sdk
initialized once. - API Calls:
vonage.verify.start(options)
: Sends OTP. Returns{ request_id: '...' }
on success.vonage.verify.check(requestId, code)
: Checks OTP. Returns object withstatus
('0' = success).
- Dashboard: Your Vonage API Dashboard is essential for:
- Getting API Key/Secret.
- Viewing usage logs (debugging).
- Managing settings (default expiry, etc.).
- Checking account balance.
Environment Variables Summary:
VONAGE_API_KEY
: Your Vonage API Key.VONAGE_API_SECRET
: Your Vonage API Secret.PORT
(Optional): Server port (defaults to3000
).
5. Error Handling and Logging
The basic error handling can be improved for production:
- Specific Vonage Errors: Handle more statuses from the Vonage Verify API Reference.
- Network/SDK Errors: Log these comprehensively. Consider checking
error.response.status
(HTTP status code) in addition toerror.response.data.status
(Vonage status) for more context. - Logging: Use dedicated libraries (Winston, Pino) for structured JSON logging, log levels, and appropriate transports (console, file, external service). Include context (request IDs, user identifiers - anonymize PII if needed).
- Retry Mechanisms: Implement retries with backoff for transient network errors (e.g., using
async-retry
), especially forverify.check
. Be cautious retryingverify.start
due to throttling.
6. Production Considerations: Persistent Storage
The in-memory verificationRequests
object used in this guide is strictly for demonstration and NOT suitable for production.
Why In-Memory Storage Fails in Production:
- Data Loss: Server restarts wipe all pending verifications.
- Scalability: Cannot work with multiple server instances (load balancing). Each instance would have its own separate, inconsistent memory store.
Production Solutions:
- Redis: Highly recommended for this use case. It's fast and designed for temporary, expiring data.
- Store
request_id
keyed by phone number (or vice-versa). - Set a Time-To-Live (TTL) matching the Vonage expiry (e.g., 5 minutes). Redis auto-deletes expired keys.
- Store
- Database (SQL/NoSQL): Use your application's main database (PostgreSQL, MongoDB, etc.).
- Create a
VerificationRequests
table/collection. - Store
request_id
,phoneNumber
,createdAt
,expires_at
,status
('pending', 'verified', etc.). - Query based on
request_id
for verification. - Implement cleanup logic (e.g., a scheduled job) to remove old/expired entries based on
expires_at
.
- Create a
Conceptual Schema (SQL Example):
CREATE TABLE VerificationRequests (
request_id VARCHAR(50) PRIMARY KEY, -- Vonage Request ID
phone_number VARCHAR(20) NOT NULL,
status VARCHAR(10) NOT NULL DEFAULT 'pending', -- 'pending', 'verified', 'failed', 'expired'
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL, -- Set based on Vonage expiry (e.g., NOW() + INTERVAL '5 minutes')
attempts INT DEFAULT 0
);
CREATE INDEX idx_verification_phone ON VerificationRequests (phone_number);
CREATE INDEX idx_verification_expires ON VerificationRequests (expires_at);
Implementation: Replace verificationRequests[key] = value
and delete verificationRequests[key]
with calls to your chosen persistent store (e.g., using redis
, pg
, mongoose
). Handle potential data store errors.
7. Adding Security Features
Essential security practices:
- Input Validation: Use robust libraries (
express-validator
,joi
) for strict validation of phone numbers and OTP codes. Sanitize inputs. - Rate Limiting: Crucial to prevent abuse and brute-force attacks. Use
express-rate-limit
or similar.- Limit OTP requests per phone number/IP.
- Limit verification attempts per
request_id
/IP.
- Secure Secret Management: Use cloud provider secret managers (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) or inject environment variables securely via CI/CD in production. Never commit
.env
or secrets. - HTTPS: Enforce HTTPS in production.
- Session Management: If integrating into a login flow, use secure session practices (
express-session
with a secure store, secure cookie attributes:HttpOnly
,Secure
,SameSite
).
8. Handling Special Cases
- Phone Number Formatting: Vonage requires E.164 format. Use libraries like
libphonenumber-js
for reliable parsing, validation, and formatting on the server-side. Guide users in the UI. - International Numbers: Supported by Vonage, but ensure validation handles them. Be aware of potential cost differences.
- Concurrent Requests (Throttling): Vonage status
10
means too many requests to the same number recently (~30s window). Inform the user to wait. Check your persistent store before calling Vonage to see if a recent request for that number is still pending. - Code Expiry: Default is 5 minutes (can be configured via API or dashboard). Inform users. Check expiry (
expires_at
in DB or rely on Redis TTL) before attemptingverify.check
. Handle status6
(not found/expired) gracefully. - Resend Functionality:
- Add a ""Resend Code"" option.
- Important: Before calling
vonage.verify.start
for a resend, consider callingvonage.verify.cancel(previous_request_id)
if a previous request for the same number is still active within Vonage's system (typically within the expiry window). This prevents users from having multiple potentially valid codes active simultaneously. You'll need to retrieve theprevious_request_id
from your persistent store. - Generate a new
request_id
with the resend call. - Update your persistent store with the new
request_id
and reset theexpires_at
timestamp. - Apply rate limiting to the resend action itself.
- SMS vs. Voice: Use the
workflow_id
parameter inverify.start
to customize delivery (e.g., SMS only, voice only).
9. Implementing Performance Optimizations
- Data Store Speed: Redis is typically fastest for this. Optimize database indexes (
request_id
,phone_number
,expires_at
). - Asynchronous Operations: Ensure all I/O (Vonage API, data store) uses
async/await
or Promises correctly. - Load Testing: Use tools (k6, artillery) to test performance under load. Monitor Vonage API latency.
10. Adding Monitoring, Observability, and Analytics
Understand system health and performance:
- Health Checks:
/health
endpoint checking dependencies. - Performance Metrics: Track request latency, Vonage API latency, success/failure rates, rate limit counts (Prometheus/Grafana, Datadog, New Relic).
- Error Tracking: Use services (Sentry, Bugsnag) to capture and alert on errors.
- Dashboards: Visualize key metrics.
- Alerting: Set alerts for high error rates, latency spikes, health check failures.
11. Troubleshooting and Caveats
Common issues:
- Invalid Vonage Credentials: (Auth errors) Check
.env
and ensuredotenv
loads first. - Incorrect Phone Number Format: (Status
3
) Use E.164. Validate robustly. - Throttling: (Status
10
) Inform user to wait. Check pending requests before calling Vonage. - Expired/Not Found Request: (Status
6
) Prompt user to request a new code. Check your storage logic. - Incorrect Code: (Status
16
) Allow retries (with limits). - Too Many Wrong Codes: (Status
17
) Prompt user to request a new code. - Insufficient Vonage Balance: Monitor your account credit.
- Network Issues: Implement retries. Check firewalls.
- SDK/API Version Issues: Keep SDK updated, check Vonage changelogs.
- In-Memory Store Issues (Demo): This guide's use of
verificationRequests = {}
is only for demonstration and will fail in production or multi-instance environments. Use persistent storage (Section 6).
12. Deployment and CI/CD
- Platform Choice: Choose a suitable hosting platform (Heroku, AWS, GCP, Azure, etc.).
- Environment Variables: Configure
VONAGE_API_KEY
,VONAGE_API_SECRET
, and database/Redis credentials securely in the deployment environment. Do not commit secrets. PORT
Variable: Useprocess.env.PORT
.- Build Process:
npm install --production
. - Starting the App: Use
pm2
or platform mechanisms (Procfile
,Dockerfile CMD
). - CI/CD Pipeline: Automate testing, building, and deployment. Inject secrets securely.
- Rollback Plan: Ensure you can revert to previous versions easily.
Example Dockerfile
(Conceptual):
# Use an appropriate Node.js base image
FROM node:18-alpine AS base
WORKDIR /app
# Copy package files
COPY package.json package-lock.json ./
# Install production dependencies only
RUN npm ci --only=production
# Copy the rest of the application code
COPY . .
# Expose the port the app runs on (should match server config)
EXPOSE 3000
# Define the command to run the application
CMD [ ""node"", ""index.js"" ]
13. Verification and Testing
Ensure correctness:
1. Manual Verification Steps:
- Start server:
node index.js
. - Open
http://localhost:3000
. - Enter a valid test phone number (E.164 format).
- Submit -> Check phone for SMS -> Verify page loads.
- Enter correct code -> Submit -> Success page loads.
- Test failures: Invalid phone format, wrong code, expired code (>5 mins), too many wrong codes, missing code.
2. API Testing (using curl
or Postman):
-
Request OTP:
curl -X POST http://localhost:3000/request-otp \ -H ""Content-Type: application/x-www-form-urlencoded"" \ -d ""phoneNumber=YOUR_PHONE_NUMBER_HERE""
(Replace
YOUR_PHONE_NUMBER_HERE
with a valid E.164 number) -
Verify OTP (requires
requestId
from previous step's output/logs andphoneNumber
):curl -X POST http://localhost:3000/verify-otp \ -H ""Content-Type: application/x-www-form-urlencoded"" \ -d ""requestId=YOUR_REQUEST_ID&otpCode=YOUR_OTP_CODE&phoneNumber=YOUR_PHONE_NUMBER_HERE""
(Replace placeholders with actual values)
3. Automated Testing: Implement unit tests (Jest, Mocha) for individual functions/modules and integration tests (Supertest) to test API endpoints and workflows. Mock the Vonage API calls during testing.