Implement OTP Verification with Node.js, Express, and Plivo
Two-factor authentication (2FA) adds a critical layer of security to applications by requiring users to provide a second form of verification beyond just a password. One of the most common and user-friendly methods for 2FA is sending a one-time password (OTP) via SMS.
This guide provides a complete walkthrough for building a production-ready OTP verification system using Node.js, the Express framework, Redis for temporary OTP storage, and the Plivo communications platform for sending SMS messages. We will cover everything from project setup and core logic to security best practices, error handling, deployment, and testing.
Project Overview and Goals
Goal: To build a secure backend API service that handles OTP generation, delivery via Plivo SMS, and verification for user phone numbers within a Node.js Express application.
Problem Solved: This implementation addresses the need for enhanced application security by adding an SMS-based 2FA layer, protecting user accounts even if passwords are compromised. It provides a reliable way to verify user phone numbers during registration or critical actions.
Technologies:
- Node.js: A JavaScript runtime environment for building scalable server-side applications.
- Express.js: A minimal and flexible Node.js web application framework used to build the API endpoints.
- Plivo: A cloud communications platform used for its reliable SMS API to send OTP messages globally. We use the official Plivo Node.js SDK.
- Redis: An in-memory data structure store used for its speed and ability to set expirations (TTL) on OTPs, making it ideal for temporary storage.
- dotenv: A module to load environment variables from a
.env
file intoprocess.env
, keeping sensitive credentials out of the codebase. - express-rate-limit: Middleware to limit repeated requests to public APIs, crucial for preventing abuse of the OTP endpoints.
- express-validator: Middleware for validating and sanitizing API request inputs (phone numbers, OTP codes).
- crypto: Node.js built-in module for cryptographically strong random number generation.
System Architecture:
graph LR
A[User Client (Web/Mobile)] -- Request OTP --> B(Node.js/Express API);
B -- Generate OTP & Store (Redis) --> C{Redis};
B -- Send SMS via Plivo --> D[Plivo SMS API];
D -- Delivers SMS --> E[User's Phone];
A -- Submit OTP --> B;
B -- Verify OTP (Redis) --> C;
B -- Send Verification Result --> A;
style B fill:#f9f,stroke:#333,stroke-width:2px
style C fill:#ccf,stroke:#333,stroke-width:2px
style D fill:#9cf,stroke:#333,stroke-width:2px
Prerequisites:
- Node.js and npm (or yarn) installed. (Node.js v14.x or later recommended).
- Access to a terminal or command prompt.
- A Plivo account (Sign up for free). You will need your Auth ID, Auth Token, and an SMS-enabled Plivo phone number.
- Redis installed and running locally or accessible via a cloud provider. (Installation guides: Official Redis Docs).
- Basic understanding of Node.js, Express, REST APIs, and asynchronous JavaScript.
- A tool for making API requests (like
curl
, Postman, or Insomnia).
Final Outcome: A secure, robust, and testable Node.js Express API service with endpoints to request and verify SMS OTPs using Plivo.
1. Setting up the Project
Let's start by creating the project directory, initializing Node.js, and installing the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
mkdir node-plivo-otp cd node-plivo-otp
-
Initialize Node.js Project: This creates a
package.json
file to manage project dependencies and scripts.npm init -y
-
Install Dependencies: Install Express, the Plivo SDK, the Redis client, dotenv for environment variables, and middleware for rate limiting and validation.
npm install express plivo redis dotenv express-rate-limit express-validator
-
Install Development Dependencies (Optional but Recommended):
nodemon
automatically restarts the server during development when file changes are detected.npm install --save-dev nodemon
-
Create Project Structure: Set up a basic structure for clarity.
mkdir src touch src/app.js src/config.js src/otp.service.js src/routes.js .env .gitignore
src/app.js
: Main application file (Express server setup).src/config.js
: Application configuration (loaded from environment variables).src/otp.service.js
: Business logic for OTP generation, storage, and Plivo interaction.src/routes.js
: Defines the API endpoints..env
: Stores sensitive credentials (Plivo Auth ID, Token, Number, Redis URL). Never commit this file..gitignore
: Specifies intentionally untracked files that Git should ignore (likenode_modules
and.env
).
-
Configure
.gitignore
: Add the following lines to your.gitignore
file:# Dependencies node_modules/ # Environment variables .env # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Optional editor directories .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw?
-
Configure Environment Variables (
.env
): Open the.env
file and add your Plivo credentials, Plivo phone number, and Redis connection details. Replace the placeholders with your actual values. For production Redis instances requiring authentication, use the formatredis://:yourpassword@your_redis_host:6379
.# .env # Plivo Credentials (Find these on your Plivo Console Dashboard: https://console.plivo.com/dashboard/) PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN PLIVO_PHONE_NUMBER=YOUR_PLIVO_SMS_ENABLED_NUMBER_IN_E164_FORMAT # e.g., +14151112222 # Redis Configuration # For local unsecured Redis: REDIS_URL=redis://127.0.0.1:6379 # For production Redis with password: # REDIS_URL=redis://:YOUR_REDIS_PASSWORD@your_redis_host.com:6379 # Application Port PORT=3000 # OTP Settings OTP_LENGTH=6 OTP_EXPIRY_SECONDS=300 # 5 minutes
PLIVO_AUTH_ID
/PLIVO_AUTH_TOKEN
: Your API credentials from the Plivo dashboard. Essential for authenticating API requests.PLIVO_PHONE_NUMBER
: The SMS-enabled number purchased from Plivo, used as the sender ID for OTP messages. Must be in E.164 format (e.g.,+12125551234
).REDIS_URL
: Connection string for your Redis instance. Used by the Redis client to connect. Ensure it includes credentials if needed for production.PORT
: The port your Express application will listen on.OTP_LENGTH
: The desired number of digits for the OTP.OTP_EXPIRY_SECONDS
: How long (in seconds) the OTP remains valid in Redis.
-
Load Environment Variables (
src/config.js
): Configure the application to load these variables usingdotenv
.// src/config.js require('dotenv').config(); // Load .env file variables into process.env const config = { plivo: { authId: process.env.PLIVO_AUTH_ID, authToken: process.env.PLIVO_AUTH_TOKEN, phoneNumber: process.env.PLIVO_PHONE_NUMBER, }, redis: { url: process.env.REDIS_URL, }, app: { port: parseInt(process.env.PORT, 10) || 3000, }, otp: { length: parseInt(process.env.OTP_LENGTH, 10) || 6, expiry: parseInt(process.env.OTP_EXPIRY_SECONDS, 10) || 300, // Default 5 minutes }, }; // Basic validation to ensure essential variables are set if (!config.plivo.authId || !config.plivo.authToken || !config.plivo.phoneNumber) { console.error('FATAL ERROR: Plivo credentials are not set in the environment variables.'); process.exit(1); // Exit if essential config is missing } if (!config.redis.url) { console.error('FATAL ERROR: Redis URL is not set in the environment variables.'); process.exit(1); } module.exports = config;
- Why
dotenv
? It keeps sensitive information like API keys out of your source code, which is crucial for security, especially when using version control like Git. - Why validation? Checking for essential config variables at startup prevents runtime errors later and provides clear feedback if the setup is incomplete.
- Why
-
Add Run Scripts to
package.json
: Modify thescripts
section in yourpackage.json
. Note that the defaulttest
script is a placeholder; for production, you should implement actual unit and integration tests.// package.json (scripts section) ""scripts"": { ""start"": ""node src/app.js"", ""dev"": ""nodemon src/app.js"", ""test"": ""echo \""Error: no test specified\"" && exit 1"" },
npm start
: Runs the application using Node.npm run dev
: Runs the application usingnodemon
, which watches for file changes and restarts the server automatically – ideal for development.npm test
: Placeholder script. Real tests using frameworks like Jest or Mocha are recommended.
Now the basic project structure, dependencies, and configuration loading are in place.
2. Implementing Core Functionality
This section focuses on the heart of the OTP system: generating, storing, sending, and verifying OTPs. We'll implement this logic within src/otp.service.js
.
-
Initialize Plivo and Redis Clients: We need instances of the Plivo client to send SMS and the Redis client to store/retrieve OTPs.
// src/otp.service.js const plivo = require('plivo'); const redis = require('redis'); const crypto = require('crypto'); // Use crypto for better random number generation const config = require('./config'); // Load configuration // --- Client Initialization --- // Initialize Plivo Client const plivoClient = new plivo.Client(config.plivo.authId, config.plivo.authToken); // Initialize Redis Client const redisClient = redis.createClient({ url: config.redis.url, }); redisClient.on('error', (err) => console.error('Redis Client Error:', err)); redisClient.on('connect', () => console.log('Connected to Redis successfully.')); // Connect to Redis (important!) // Added catch here in case initial connection fails during startup redisClient.connect().catch(err => { console.error('Failed to connect to Redis on startup:', err); // Depending on the app's requirements, you might want to exit here // process.exit(1); }); // --- OTP Generation --- /** * Generates a cryptographically strong random numeric OTP. * @param {number} length - The desired length of the OTP. * @returns {string} The generated OTP. */ function generateOtp(length = config.otp.length) { const otpLength = Math.max(1, Math.floor(length)); try { // Calculate the min and max values for the desired length const min = Math.pow(10, otpLength - 1); const max = Math.pow(10, otpLength); // Max is exclusive for crypto.randomInt // Generate a random integer within the range [min, max) and convert to string return crypto.randomInt(min, max).toString(); } catch (error) { console.error('Error generating OTP using crypto.randomInt:', error); // Fallback or throw error - simple fallback for demo purposes let fallbackOtp = ''; for (let i = 0; i < otpLength; i++) { fallbackOtp += Math.floor(Math.random() * 10); } console.warn('Using Math.random as fallback for OTP generation.'); return fallbackOtp; } } // --- OTP Storage (Redis) --- /** * Stores the OTP in Redis with an expiry time. * @param {string} phoneNumber - The phone number (used as part of the key). * @param {string} otp - The OTP to store. * @returns {Promise<void>} */ async function storeOtp(phoneNumber, otp) { const key = `otp:${phoneNumber}`; try { // Check if client is ready before attempting to set if (!redisClient.isReady) { console.error('Redis client is not ready. Cannot store OTP.'); throw new Error('Failed to store OTP - Redis connection issue.'); } await redisClient.set(key, otp, { EX: config.otp.expiry, // Set expiry time in seconds NX: false, // Allow overwriting existing key if needed }); console.log(`OTP stored for ${phoneNumber}, expires in ${config.otp.expiry} seconds.`); } catch (error) { console.error(`Error storing OTP for ${phoneNumber}:`, error); // Avoid re-throwing the exact same error message if it's already specific if (error.message.includes('Redis connection issue')) { throw error; } throw new Error('Failed to store OTP'); // Re-throw for API layer handling } } // --- OTP Sending (Plivo SMS) --- /** * Sends the OTP via SMS using Plivo. * @param {string} recipientNumber - The E.164 formatted phone number to send the SMS to. * @param {string} otp - The OTP code to send. * @returns {Promise<object>} Plivo API response. */ async function sendOtpSms(recipientNumber, otp) { const message = `Your verification code is: ${otp}`; console.log(`Attempting to send OTP to ${recipientNumber}`); try { const response = await plivoClient.messages.create( config.plivo.phoneNumber, // Sender number (from config) recipientNumber, // Recipient number message // Message text ); console.log(`SMS sent successfully to ${recipientNumber}. Message UUID:`, response.messageUuid); return response; } catch (error) { console.error(`Error sending SMS via Plivo to ${recipientNumber}:`, error); // Provide more specific error feedback if possible let errorMessage = 'Failed to send OTP SMS.'; if (error.message) { errorMessage += ` Plivo error: ${error.message}`; } throw new Error(errorMessage); // Re-throw for API layer handling } } // --- OTP Verification --- /** * Verifies the submitted OTP against the one stored in Redis. * @param {string} phoneNumber - The phone number associated with the OTP. * @param {string} submittedOtp - The OTP submitted by the user. * @returns {Promise<boolean>} True if OTP is valid, false otherwise. */ async function verifyOtp(phoneNumber, submittedOtp) { const key = `otp:${phoneNumber}`; try { // Check if client is ready if (!redisClient.isReady) { console.error('Redis client is not ready. Cannot verify OTP.'); throw new Error('Failed to verify OTP - Redis connection issue.'); } const storedOtp = await redisClient.get(key); if (!storedOtp) { console.log(`No OTP found for ${phoneNumber} (likely expired or never existed).`); return false; // OTP expired or never existed } if (storedOtp === submittedOtp) { console.log(`OTP verification successful for ${phoneNumber}.`); // Optional: Delete the OTP immediately after successful verification // to prevent reuse, though expiry handles this eventually. // Use await with del, and handle potential errors try { await redisClient.del(key); } catch (delError) { console.error(`Error deleting OTP key ${key} after successful verification:`, delError); // Continue, as verification succeeded, but log the cleanup failure. } return true; } else { console.log(`OTP verification failed for ${phoneNumber}. Submitted: ${submittedOtp}`); // Avoid logging expected OTP // Optional: Implement attempt tracking here if needed return false; // Incorrect OTP } } catch (error) { console.error(`Error verifying OTP for ${phoneNumber}:`, error); // Avoid re-throwing the exact same error message if it's already specific if (error.message.includes('Redis connection issue')) { throw error; } throw new Error('Failed to verify OTP due to a system error.'); // Re-throw } } // --- Export Functions --- module.exports = { generateOtp, storeOtp, sendOtpSms, verifyOtp, redisClient, // Export client for graceful shutdown in app.js // plivoClient, // Export if needed elsewhere };
- Why Redis? Redis is ideal for OTPs because it's fast (in-memory) and has built-in support for key expiration (
EX
), automatically cleaning up old OTPs. - Why
async/await
? Plivo and Redis operations are asynchronous (network I/O).async/await
provides a cleaner way to handle promises compared to.then()
chains. - Error Handling:
try...catch
blocks are used. Errors are logged, and custom error messages are thrown to be handled by the API layer. Redis connection readiness is checked. - OTP Generation: Uses Node's built-in
crypto.randomInt
for generating a cryptographically stronger pseudo-random numeric OTP, which is preferred for production overMath.random
. - Plivo SMS: The
sendOtpSms
function usesplivoClient.messages.create
with the sender number from config, the recipient number, and the message text including the OTP. - Verification Logic:
verifyOtp
fetches the OTP from Redis using the phone number key. It checks if the OTP exists (not expired) and if it matches the submitted code. It deletes the key on successful verification (best practice).
- Why Redis? Redis is ideal for OTPs because it's fast (in-memory) and has built-in support for key expiration (
3. Building the API Layer
Now, let's create the Express routes that expose our OTP functionality as API endpoints.
-
Define Routes (
src/routes.js
): We need two main endpoints: one to request an OTP and one to verify it. We'll also useexpress-validator
for input validation.// src/routes.js const express = require('express'); const { body, validationResult } = require('express-validator'); const otpService = require('./otp.service'); const config = require('./config'); const router = express.Router(); // --- Input Validation Middleware --- const validatePhoneNumber = [ body('phoneNumber') .trim() .isMobilePhone('any', { strictMode: true }) // Validates E.164 format implicitly for many locales .withMessage('Valid E.164 formatted phone number is required (e.g., +14155552671).') // Add custom validation/sanitization if needed, e.g., remove non-digit chars except '+' .customSanitizer(value => value.replace(/[^+\d]/g, '')), ]; const validateVerificationPayload = [ ...validatePhoneNumber, // Reuse phone number validation body('otp') .trim() .isLength({ min: config.otp.length, max: config.otp.length }) .withMessage(`OTP must be exactly ${config.otp.length} digits.`) .isNumeric() .withMessage('OTP must contain only numeric digits.'), ]; // Middleware to handle validation results const handleValidationErrors = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { // Log the validation errors for debugging console.warn('Validation Errors:', errors.array()); // Return a 400 Bad Request with the first error message return res.status(400).json({ message: errors.array()[0].msg }); } next(); // Proceed to the route handler if validation passes }; // --- API Endpoints --- /** * @route POST /api/otp/request * @desc Request an OTP to be sent to a phone number * @access Public * @body { ""phoneNumber"": ""+14155552671"" } */ router.post( '/request', validatePhoneNumber, // Apply phone number validation handleValidationErrors, // Handle any validation errors async (req, res) => { const { phoneNumber } = req.body; // Already validated and sanitized try { // 1. Generate OTP const otp = otpService.generateOtp(); // 2. Store OTP (associating it with the phone number) await otpService.storeOtp(phoneNumber, otp); // 3. Send OTP via SMS await otpService.sendOtpSms(phoneNumber, otp); // IMPORTANT: Do NOT log the actual OTP in production environments. console.log(`OTP request successful for ${phoneNumber}. (OTP sent via Plivo)`); res.status(200).json({ message: 'OTP has been sent successfully.' }); } catch (error) { console.error(`Error processing OTP request for ${phoneNumber}:`, error); // Check for specific errors thrown by the service if (error.message.includes('Plivo error')) { res.status(502).json({ message: 'Failed to send OTP due to provider issue.' }); // Bad Gateway } else if (error.message.includes('Failed to store OTP')) { res.status(500).json({ message: 'Failed to process OTP request due to storage issue.' }); } else { res.status(500).json({ message: 'An internal server error occurred.' }); } } } ); /** * @route POST /api/otp/verify * @desc Verify an OTP submitted for a phone number * @access Public * @body { ""phoneNumber"": ""+14155552671"", ""otp"": ""123456"" } */ router.post( '/verify', validateVerificationPayload, // Apply phone number and OTP validation handleValidationErrors, // Handle any validation errors async (req, res) => { const { phoneNumber, otp } = req.body; // Validated and sanitized try { // 1. Verify OTP against stored value const isValid = await otpService.verifyOtp(phoneNumber, otp); if (isValid) { res.status(200).json({ message: 'OTP verified successfully.' }); } else { // Use 400 for invalid/expired OTP - it's a client-side issue (wrong input or delay) res.status(400).json({ message: 'Invalid or expired OTP.' }); } } catch (error) { console.error(`Error processing OTP verification for ${phoneNumber}:`, error); res.status(500).json({ message: 'An internal server error occurred during verification.' }); } } ); module.exports = router;
- Input Validation:
express-validator
checks ifphoneNumber
looks like a valid mobile number (usingisMobilePhone
) and if theotp
has the correct length and is numeric. This prevents invalid data from reaching the service layer. ThehandleValidationErrors
middleware centralizes error reporting for validation failures. - Route Logic: Each route handler calls the corresponding functions from
otp.service.js
. It wraps calls intry...catch
to handle errors gracefully and return appropriate HTTP status codes (e.g., 200 for success, 400 for bad input/invalid OTP, 500/502 for server/provider errors). - Sanitization: Using
.trim()
and.customSanitizer
helps clean up input before validation and processing. - Security: The line logging the actual OTP code in the
/request
endpoint has been removed.
- Input Validation:
-
Integrate Routes into Express App (
src/app.js
): Set up the Express application, include necessary middleware (JSON body parsing, rate limiting), and mount the OTP routes.// src/app.js const express = require('express'); const rateLimit = require('express-rate-limit'); const config = require('./config'); const otpRoutes = require('./routes'); const { redisClient } = require('./otp.service'); // Import redisClient for graceful shutdown const app = express(); // --- Middleware --- // 1. Body Parser: Enable Express to parse JSON request bodies app.use(express.json()); // 2. Rate Limiting: Apply basic rate limiting to all API requests // Adjust limits based on expected traffic and security needs. const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many 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 }); app.use('/api', limiter); // Apply limiter only to API routes // Apply stricter rate limiting specifically to OTP routes const otpLimiter = rateLimit({ windowMs: 5 * 60 * 1000, // 5 minutes max: 10, // Limit each IP to 10 OTP requests/verifications per 5 minutes message: 'Too many OTP requests or verification attempts, please try again later.', standardHeaders: true, legacyHeaders: false, }); app.use('/api/otp', otpLimiter); // --- Routes --- app.get('/', (req, res) => { res.send(`OTP Service is running. Current date: ${new Date().toISOString()}`); // Simple health check / info endpoint }); // Mount the OTP API routes app.use('/api/otp', otpRoutes); // --- Basic Error Handling Middleware (Optional but Recommended) --- // Catches errors thrown from async route handlers if not caught locally app.use((err, req, res, next) => { console.error('Unhandled Error:', err.stack || err); // Avoid sending stack traces in production res.status(500).json({ message: 'Something went wrong on the server.' }); }); // --- Start Server --- const PORT = config.app.port; // Assign the return value of app.listen to the server variable const server = app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); console.log(`API endpoints available at http://localhost:${PORT}/api/otp`); console.log(`Using Plivo Number: ${config.plivo.phoneNumber}`); // Verify Redis connection is established before accepting requests fully // (Redis client connection is initiated in otp.service.js) if (redisClient.isReady) { console.log('Redis client is ready.'); } else { console.warn('Redis client is not yet connected. Waiting for connection...'); } }); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM signal received: closing HTTP server'); server.close(() => { console.log('HTTP server closed'); // Close Redis connection if it's open and the client exists if (redisClient && redisClient.isReady) { redisClient.quit() .then(() => console.log('Redis connection closed successfully.')) .catch((err) => console.error('Error closing Redis connection:', err)); } else { console.log('Redis client not connected or unavailable, skipping quit.'); } }); });
express.json()
: Middleware to parse incoming JSON request bodies, makingreq.body
available.- Rate Limiting:
express-rate-limit
is crucial to prevent brute-force attacks or abuse of the OTP system. - Route Mounting: The OTP routes are mounted under
/api/otp
. - Basic Error Handler: A final middleware catches unhandled errors.
- Graceful Shutdown: The
app.listen
return value is assigned toserver
. TheredisClient
is imported fromotp.service.js
andredisClient.quit()
is called within the shutdown handler, ensuring resources are released properly. Checks are added to ensure the client exists and is ready before attempting to quit.
4. Integrating with Plivo (Configuration Recap)
We've already initialized the Plivo client using credentials from .env
. Let's recap the crucial configuration steps:
- Sign Up for Plivo: Create an account at plivo.com.
- Get Credentials: Navigate to the Plivo Console dashboard. Your Auth ID and Auth Token are displayed prominently.
- Buy an SMS-Enabled Number: Go to ""Phone Numbers"" -> ""Buy Numbers"" in the console. Search for a number with SMS capability. Ensure it supports sending messages to your target regions.
- Add Credits: Ensure you have sufficient credits in your Plivo account.
- Securely Store Credentials: Place your Auth ID, Auth Token, and Plivo phone number in the
.env
file. Never commit.env
to version control.
Fallback Mechanisms: While this guide focuses on SMS OTP, Plivo supports Voice calls for OTP delivery. Implementing a fallback (e.g., if SMS fails or user requests it) would typically involve:
- Using
plivoClient.calls.create
to initiate a text-to-speech call reading the OTP. - Setting up a Plivo Application in the Plivo console.
- Configuring an ""Answer URL"" for that application, pointing to another endpoint in your Node.js app that returns Plivo XML (specifically the
<Speak>
element) to read the code. Alternatively, Plivo's visual workflow builder, PHLO, can orchestrate this. - Adding logic in your service/routes to trigger the voice call under specific conditions. This setup is beyond the scope of this API guide but is documented on Plivo's developer portal.
5. Error Handling, Logging, and Retry Mechanisms
Robust error handling and logging are essential for production systems.
-
Consistent Error Strategy:
- Service Layer (
otp.service.js
): Catches specific errors, logs them, and throws standardized errors. - API Layer (
routes.js
): Catches service errors, logs them, sends appropriate HTTP status codes (4xx/5xx) and user-friendly JSON messages. - App Layer (
app.js
): Includes a final middleware for unhandled exceptions.
- Service Layer (
-
Logging:
- Current Implementation: Uses
console.log/warn/error
. - Production Logging: Use a library like
winston
orpino
for structured logging (JSON), log levels, and multiple transports (file, external services). - What to Log: Successful requests/verifications (INFO, never log the OTP code), validation failures (WARN), errors during storage/sending (ERROR, include context like phone prefix/suffix carefully), Plivo/Redis errors (ERROR), rate limit hits (INFO/WARN).
- Current Implementation: Uses
-
Retry Mechanisms:
- Plivo SMS: Plivo handles some retries internally. For transient network errors between your server and Plivo, implement retries in
otp.service.js
. - Simple Retry Example:
Note: This is a basic example. Production retries often involve exponential backoff and jitter.
async function sendOtpSmsWithRetry(recipientNumber, otp, retries = 2) { try { // It's better to call the core function that interacts with Plivo directly // Assuming sendOtpSms is the function making the actual Plivo call return await sendOtpSms(recipientNumber, otp); } catch (error) { console.error(`Attempt failed: ${error.message}. Retries left: ${retries}`); if (retries > 0) { // Optional: Add a delay before retrying (e.g., exponential backoff) await new Promise(resolve => setTimeout(resolve, 1000)); // 1-second delay return sendOtpSmsWithRetry(recipientNumber, otp, retries - 1); } else { console.error('Max retries reached. Failed to send OTP via Plivo.'); throw error; // Re-throw the final error } } }
- Plivo SMS: Plivo handles some retries internally. For transient network errors between your server and Plivo, implement retries in