Two-factor authentication (2FA), often implemented via One-Time Passwords (OTP) sent over SMS, adds a critical layer of security to applications. It verifies user identity by combining something they know (password) with something they have (their phone).
This guide provides a complete walkthrough for adding SMS OTP verification to your web application using Node.js with Express for the backend, Vite (with React, though adaptable for Vue) for the frontend, and the Vonage Verify API for sending and checking OTPs.
Project Goal: Build a simple web application where a user can:
- Enter their phone number.
- Receive an OTP via SMS.
- Enter the OTP to verify their phone number.
Problem Solved: Securely verify user phone numbers, enabling functionalities like 2FA for login, password resets, or transaction confirmations, significantly reducing automated attacks and unauthorized access.
Technologies Used:
- Node.js & Express: A popular, robust combination for building backend APIs.
- Vite (React): A modern, fast frontend build tool and development server. We'll use React for examples, but the frontend logic is easily adaptable to Vue or other frameworks.
- Vonage Verify API: A dedicated service for handling OTP generation, delivery (SMS, voice), and verification logic, simplifying the implementation.
dotenv
: For managing environment variables securely.@vonage/server-sdk
: The official Vonage Node.js library.cors
: To handle Cross-Origin Resource Sharing between the frontend and backend during development.
System Architecture:
graph LR
A[User's Browser (Vite/React)] -- 1. Enter Phone --> B(Node.js/Express API);
B -- 2. Request OTP --> C(Vonage Verify API);
C -- 3. Send SMS --> D[User's Phone];
D -- 4. User Enters OTP --> A;
A -- 5. Submit OTP & RequestID --> B;
B -- 6. Check OTP --> C;
C -- 7. Verification Result --> B;
B -- 8. Send Result --> A;
classDef default fill:#f9f9f9,stroke:#333,stroke-width:2px;
Note: Ensure your publishing platform supports Mermaid diagrams. If not, replace the code block with a static image of the diagram and provide descriptive alt text.
Prerequisites:
- Node.js and npm (or yarn) installed.
- A Vonage API account (sign up for free credit).
- Basic understanding of Node.js, Express, React (or Vue), and asynchronous JavaScript.
- A text editor (like VS Code).
- A tool for testing APIs (like
curl
or Postman).
Final Outcome: A working application demonstrating the OTP request and verification flow, with separate frontend and backend components.
1. Setting up the Project
We'll create two main directories: backend
for our Node.js API and frontend
for our Vite/React application.
A. Backend Setup (Node.js/Express)
-
Create Project Directory & Initialize:
mkdir backend cd backend npm init -y
-
Install Dependencies:
npm install express dotenv @vonage/server-sdk cors body-parser
express
: Web framework.dotenv
: Loads environment variables from a.env
file.@vonage/server-sdk
: Vonage API client library.cors
: Enables Cross-Origin Resource Sharing (needed for frontend/backend communication on different ports).body-parser
: Parses incoming request bodies. Update: Modern Express (4.16+
) includesexpress.json()
andexpress.urlencoded()
, makingbody-parser
less critical but harmless to include.
-
Create Core Files:
touch server.js .env .gitignore
-
Configure
.gitignore
: Addnode_modules
and.env
to prevent committing them.node_modules .env
-
Set up Environment Variables (
.env
): You need your Vonage API Key and Secret. Find these on the Vonage API Dashboard after signing up.# Vonage API Credentials - Obtain from Vonage Dashboard VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET # Server Configuration PORT=3001 # Port for the backend API server
- Purpose:
VONAGE_API_KEY
andVONAGE_API_SECRET
authenticate your application with the Vonage API.PORT
defines where your backend server listens. Storing these in.env
keeps secrets out of your code repository.
- Purpose:
B. Frontend Setup (Vite/React)
-
Create Project Directory (from the root folder, outside
backend
):# Ensure you are in the main project directory, *not* inside ./backend npm create vite@latest frontend -- --template react cd frontend
(You can replace
react
withvue
if preferred). -
Install Dependencies: Vite handles the initial setup.
npm install
-
Run Development Server:
npm run dev
This usually starts the Vite development server on
http://localhost:5173
(or the next available port).
Project Structure:
your-otp-project/
├── backend/
│ ├── node_modules/
│ ├── .env # <-- IMPORTANT: Keep secret!
│ ├── .gitignore
│ ├── package.json
│ ├── package-lock.json
│ └── server.js # <-- Our API logic
└── frontend/
├── node_modules/
├── public/
├── src/ # <-- Our React code
├── .gitignore
├── index.html
├── package.json
├── package-lock.json
└── vite.config.js
2. Implementing Core Functionality (Backend API)
We'll build two API endpoints in backend/server.js
:
POST /api/request-otp
: Takes a phone number_ requests Vonage to send an OTP_ and returns arequest_id
.POST /api/verify-otp
: Takes therequest_id
and the user-entered OTP code_ asks Vonage to verify them_ and returns the result.
backend/server.js
:
// backend/server.js
import express from 'express';
import { Vonage } from '@vonage/server-sdk';
import { Verify } from '@vonage/verify'; // Specifically import Verify
import cors from 'cors';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
const app = express();
const port = process.env.PORT || 3001;
// --- Middleware ---
// Enable CORS for requests from your frontend development server
// Adjust origin in production!
app.use(cors({ origin: 'http://localhost:5173' })); // Or your Vite dev port
app.use(express.json()); // Parse JSON request bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies
// --- Vonage Client Initialization ---
// Validate essential environment variables
if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET) {
console.error('__ Error: VONAGE_API_KEY or VONAGE_API_SECRET is missing in .env');
process.exit(1); // Exit if keys are missing
}
const vonage = new Vonage({
apiKey: process.env.VONAGE_API_KEY_
apiSecret: process.env.VONAGE_API_SECRET_
});
const vonageVerify = new Verify(vonage.auth_ vonage.options);
// --- In-Memory Store (for demo purposes) ---
// **!! WARNING: Replace with a database (e.g._ Redis_ PostgreSQL) in production !!**
// **This simple object stores request IDs temporarily. It will be lost on server restart.**
const verificationRequests = {};
// --- API Routes ---
/**
* @route POST /api/request-otp
* @desc Request an OTP code to be sent to a phone number.
* @access Public
* @body { phoneNumber: string } - Phone number in E.164 format (e.g._ +14155552671)
*/
app.post('/api/request-otp'_ async (req_ res) => {
const { phoneNumber } = req.body;
// Basic Input Validation
if (!phoneNumber || !/^\+[1-9]\d{1,14}$/.test(phoneNumber)) {
return res.status(400).json({ error: 'Invalid phone number format. Use E.164 format (e.g., +14155552671).' });
}
console.log(`__ Requesting OTP for ${phoneNumber}...`);
try {
const result = await vonageVerify.start({
number: phoneNumber,
brand: 'YourAppName', // Customize this! Max 11 alphanumeric characters.
code_length: '6' // Optional: default is 4, can be 4-10
// workflow_id: 6 // Optional: Specify workflow (6=SMS->TTS->TTS) Check Vonage docs.
});
if (result.request_id) {
// Store the request ID (in production, associate with user ID in DB)
verificationRequests[result.request_id] = { phoneNumber: phoneNumber, status: 'pending' };
console.log(`_ OTP Request successful. Request ID: ${result.request_id}`);
res.status(200).json({ requestId: result.request_id });
} else {
// This path might indicate an issue even without an explicit error throw
console.error('__ Vonage Error: Unexpected response structure', result);
res.status(500).json({ error: 'Failed to start verification. Unexpected response.', details: result });
}
} catch (error) {
console.error('__ Vonage Error:', error.response ? error.response.data : error.message);
// Provide more specific feedback based on Vonage error codes if possible
let statusCode = 500;
let errorMessage = 'Failed to start verification process.';
if (error.response && error.response.data) {
errorMessage = error.response.data.title || error.response.data.error_text || errorMessage;
if (error.response.status === 429) { // Throttled
statusCode = 429;
errorMessage = 'Too many requests. Please try again later.';
} else if (error.response.status === 400) { // Bad request (e.g., invalid number format server-side)
statusCode = 400;
}
// Add more specific Vonage error code handling here (e.g., invalid number '3')
}
res.status(statusCode).json({ error: errorMessage, details: error.response?.data });
}
});
/**
* @route POST /api/verify-otp
* @desc Verify the OTP code entered by the user.
* @access Public
* @body { requestId: string, code: string }
*/
app.post('/api/verify-otp', async (req, res) => {
const { requestId, code } = req.body;
// Basic Input Validation
if (!requestId || !code || typeof code !== 'string' || !/^\d{4,10}$/.test(code)) {
return res.status(400).json({ error: 'Invalid input. Request ID and a valid code (4-10 digits) are required.' });
}
// Check if requestId exists in our temporary store (or DB in production)
if (!verificationRequests[requestId]) {
console.warn(`__ Verification attempt for unknown Request ID: ${requestId}`);
return res.status(404).json({ error: 'Invalid or expired request ID.' });
}
console.log(`__ Verifying OTP for Request ID: ${requestId}...`);
try {
const result = await vonageVerify.check(requestId, code);
// Check Vonage result status explicitly
if (result.status === '0') { // Status '0' means success
console.log(`_ Verification successful for Request ID: ${requestId}`);
verificationRequests[requestId].status = 'verified'; // Update status
// In production: Mark user/phone as verified in your database here.
res.status(200).json({ verified: true, message: 'Phone number verified successfully.' });
// Optional: Clean up verified requests from memory store after some time
setTimeout(() => delete verificationRequests[requestId], 60000);
} else {
// Handle non-zero status codes from Vonage (means verification failed)
console.warn(`_ Verification failed for Request ID: ${requestId}. Vonage Status: ${result.status}`, result);
verificationRequests[requestId].status = 'failed'; // Update status
let errorMessage = 'Incorrect OTP code.';
let statusCode = 400; // Bad Request is appropriate for wrong code
// Map common Vonage error statuses to user-friendly messages
switch (result.status) {
case '16': // The code provided does not match the expected value
errorMessage = 'Incorrect OTP code entered.';
break;
case '6': // The Verify request associated with this request_id is not found or has expired.
errorMessage = 'Verification request expired or not found. Please request a new code.';
statusCode = 410; // Gone
delete verificationRequests[requestId]; // Remove expired request
break;
case '17': // A wrong code was provided too many times.
errorMessage = 'Too many incorrect attempts. Please request a new code.';
statusCode = 429; // Too Many Requests
delete verificationRequests[requestId]; // Invalidate request after too many attempts
break;
// Add more specific status codes as needed from Vonage docs
default:
errorMessage = `Verification failed. (Status: ${result.status})`;
}
res.status(statusCode).json({ verified: false, error: errorMessage, details: result });
}
} catch (error) {
console.error('__ Vonage Error during check:', error.response ? error.response.data : error.message);
let statusCode = 500;
let errorMessage = 'Failed to verify code.';
if (error.response && error.response.data) {
errorMessage = error.response.data.title || error.response.data.error_text || errorMessage;
if (error.response.status === 400) { // E.g., request_id format incorrect
statusCode = 400;
} else if (error.response.status === 404) { // Request ID not found server-side
statusCode = 404;
errorMessage = 'Verification request not found on the server.';
delete verificationRequests[requestId]; // Clean up if server doesn't know it
} else if (error.response.status === 410) { // Request expired server-side
statusCode = 410;
errorMessage = 'Verification request has expired.';
delete verificationRequests[requestId]; // Clean up expired
}
}
// Ensure we clean up the local request state if Vonage says it's invalid/expired
if ([404, 410].includes(statusCode)) {
delete verificationRequests[requestId];
}
res.status(statusCode).json({ verified: false, error: errorMessage, details: error.response?.data });
}
});
// --- Basic Health Check Route ---
app.get('/health', (req, res) => {
res.status(200).json({ status: 'UP', timestamp: new Date().toISOString() });
});
// --- Start Server ---
app.listen(port, () => {
console.log(`__ Backend server listening at http://localhost:${port}`);
});
Explanation:
- Imports & Setup: Import necessary libraries, load
.env
, configure Express middleware (CORS, JSON parsing). - Vonage Initialization: Create the
Vonage
client and specifically theVerify
client using credentials from.env
. Added crucial checks for missing keys. - In-Memory Store: A simple JavaScript object
verificationRequests
is used to temporarily store therequest_id
received from Vonage. This is NOT suitable for production. In a real application, you must use a database (like Redis for caching/session data or PostgreSQL/MongoDB linked to user accounts) to store this information persistently and associate it with the user initiating the request. - /api/request-otp:
- Receives
phoneNumber
from the request body. - Performs basic E.164 format validation.
- Calls
vonageVerify.start()
with the number and a custombrand
name (this appears in the SMS message). - On success, stores the
requestId
locally (in memory for now) and sends it back to the client. - Includes robust
try...catch
error handling, logging Vonage errors and returning appropriate HTTP status codes (500 for server errors, 400 for bad input, 429 for rate limiting).
- Receives
- /api/verify-otp:
- Receives
requestId
andcode
from the request body. - Performs basic validation on inputs.
- Checks if the
requestId
exists in the local store (simulating checking a database). - Calls
vonageVerify.check()
with therequestId
andcode
. - Explicitly checks the
result.status
. Vonage uses'0'
for success. - Handles various Vonage failure statuses (
'16'
for wrong code,'6'
for expired/not found,'17'
for too many attempts) with user-friendly messages and appropriate HTTP status codes (400, 410, 429). - Updates the status in the local store (or database).
- Cleans up expired/invalid requests from the store.
- Includes robust
try...catch
for network or unexpected Vonage errors.
- Receives
- Note: The example uses
console.log
for simplicity. In production, replace these with a structured logger (like Winston or Pino, as mentioned in Section 5) for better log management and analysis.
3. Building the Frontend (React Example)
Now, let's create the user interface in the frontend
directory.
frontend/src/App.jsx
:
// frontend/src/App.jsx
import React, { useState } from 'react';
import './App.css'; // Optional: for basic styling
function App() {
const [phoneNumber, setPhoneNumber] = useState('');
const [code, setCode] = useState('');
const [requestId, setRequestId] = useState(null);
const [verificationStatus, setVerificationStatus] = useState(''); // '', 'pending', 'verified', 'error'
const [message, setMessage] = useState(''); // For success/error messages
const [isLoading, setIsLoading] = useState(false);
// Backend API URL - Make sure this matches your backend setup
const API_URL = 'http://localhost:3001/api'; // Adjust port if needed
const handleRequestOtp = async (e) => {
e.preventDefault();
setIsLoading(true);
setMessage('');
setVerificationStatus('');
try {
const response = await fetch(`${API_URL}/request-otp`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ phoneNumber }),
});
const data = await response.json();
if (response.ok) {
setRequestId(data.requestId);
setVerificationStatus('pending');
setMessage('OTP requested successfully. Check your phone.');
} else {
throw new Error(data.error || `Request failed with status ${response.status}`);
}
} catch (error) {
console.error('Request OTP Error:', error);
setMessage(`Error requesting OTP: ${error.message}`);
setVerificationStatus('error');
} finally {
setIsLoading(false);
}
};
const handleVerifyOtp = async (e) => {
e.preventDefault();
if (!requestId) {
setMessage('Error: No request ID found. Please request OTP first.');
return;
}
setIsLoading(true);
setMessage('');
try {
const response = await fetch(`${API_URL}/verify-otp`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ requestId, code }),
});
const data = await response.json();
if (response.ok && data.verified) {
setVerificationStatus('verified');
setMessage(data.message || 'Phone number verified successfully!');
// Reset form for potential next verification
// setPhoneNumber('');
// setCode('');
// setRequestId(null);
} else {
// Use the error message from the backend if available
throw new Error(data.error || `Verification failed with status ${response.status}`);
}
} catch (error) {
console.error('Verify OTP Error:', error);
setMessage(`Error verifying OTP: ${error.message}`);
setVerificationStatus('error');
} finally {
setIsLoading(false);
}
};
return (
<div className="App">
<h1>Phone Number Verification</h1>
{!verificationStatus || verificationStatus === 'error' ? (
<form onSubmit={handleRequestOtp}>
<h2>Step 1: Enter Phone Number</h2>
<p>Use E.164 format (e.g., +14155552671)</p>
<label htmlFor="phoneNumber">Phone Number:</label>
<input
type="tel"
id="phoneNumber"
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
placeholder="+14155552671"
required
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Sending...' : 'Request OTP'}
</button>
</form>
) : null}
{verificationStatus === 'pending' ? (
<form onSubmit={handleVerifyOtp}>
<h2>Step 2: Enter OTP Code</h2>
<label htmlFor="code">Verification Code:</label>
<input
type="text" // Use "text" with pattern for better mobile input
id="code"
value={code}
onChange={(e) => setCode(e.target.value)}
required
minLength="4"
maxLength="10"
pattern="\d*" // Allow only digits
inputMode="numeric" // Hint for numeric keyboard on mobile
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Verifying...' : 'Verify Code'}
</button>
</form>
) : null}
{message && (
<p className={`message ${verificationStatus}`}>
{message}
</p>
)}
{verificationStatus === 'verified' && (
<div>
<h2>_ Verification Complete!</h2>
{/* Optionally show verified number or allow next step */}
</div>
)}
</div>
);
}
export default App;
Note: Optional basic styling can be placed in src/App.css
(which is already imported by the component above) or a similar CSS file. Example CSS:
/* Example content for src/App.css */
.App { max-width: 400px; margin: 40px auto; padding: 20px; border: 1px solid #ccc; border-radius: 8px; text-align: center; }
label { display: block; margin-bottom: 5px; font-weight: bold; }
input[type="tel"], input[type="text"] { width: calc(100% - 22px); padding: 10px; margin-bottom: 15px; border: 1px solid #ccc; border-radius: 4px; }
button { padding: 10px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1em; }
button:disabled { background-color: #ccc; cursor: not-allowed; }
.message { margin-top: 20px; padding: 10px; border-radius: 4px; }
.message.pending { background-color: #e7f3fe; color: #0c5460; border: 1px solid #bee5eb; }
.message.verified { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.message.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
Explanation:
- State: Uses
useState
to manage the phone number input, OTP code input, therequestId
received from the backend, loading state, UI status (verificationStatus
), and feedback messages. - API URL: Defines the base URL for the backend API.
handleRequestOtp
:- Triggered by the first form submission.
- Sends a
POST
request to/api/request-otp
with the phone number. - Uses
fetch
for the API call. - Handles success by storing the
requestId
and updating the UI state to show the OTP input field. - Handles errors by displaying an error message.
- Includes loading state management.
handleVerifyOtp
:- Triggered by the second form submission.
- Sends a
POST
request to/api/verify-otp
with therequestId
and the enteredcode
. - Handles success by setting the status to
verified
and showing a success message. - Handles errors (including specific messages from the backend) by updating the message state.
- Includes loading state management.
- Conditional Rendering: The UI changes based on the
verificationStatus
:- Initially (or on error), shows the phone number input form.
- When
pending
, shows the OTP code input form. - When
verified
, shows a success confirmation. - Displays messages based on API responses.
4. Running the Application
-
Start the Backend:
cd backend npm start # Or node server.js
The API server should start, likely on
http://localhost:3001
. -
Start the Frontend:
cd ../frontend npm run dev
The Vite dev server should start, likely on
http://localhost:5173
. -
Test:
- Open your browser to the frontend URL (
http://localhost:5173
). - Enter your phone number in E.164 format (e.g.,
+14155552671
- use your actual number). - Click
Request OTP
. Check your phone for an SMS containing the code (it might take a few seconds). - Enter the received code into the second input field.
- Click
Verify Code
. - You should see the success message. Check the backend console logs for details.
- Open your browser to the frontend URL (
5. Error Handling and Logging
- Backend: The
server.js
includestry...catch
blocks for Vonage API calls. It logs errors to the console and attempts to return meaningful JSON error responses with appropriate HTTP status codes (400, 404, 410, 429, 500). Vonage-specific error statuses (like'6'
or'16'
) are mapped to clearer user messages. For production, integrate a dedicated logging library (like Winston or Pino) and an error tracking service (like Sentry). - Frontend: The
App.jsx
catches errors fromfetch
calls and displays error messages returned from the backend API.
6. Database Schema and Data Layer (Production Considerations)
The provided example uses an in-memory object (verificationRequests
) which is unsuitable for production. You must integrate a database.
- Schema: You'd need a table (e.g.,
phone_verifications
) to store:verification_id
(Primary Key)user_id
(Foreign Key to your users table, if applicable)phone_number
(The number being verified)vonage_request_id
(The ID from Vonage, indexed for lookups)status
('pending', 'verified', 'failed', 'expired', 'cancelled')created_at
(Timestamp)expires_at
(Timestamp, e.g., 5 minutes fromcreated_at
)attempts
(Integer, track incorrect code entries)
- Data Layer: Implement functions in your backend to:
- Create a new verification record when
/request-otp
is called. - Find a record by
vonage_request_id
during/verify-otp
. - Update the record's status and attempt count.
- Periodically clean up expired records.
- Create a new verification record when
- Technology: Redis is excellent for short-lived data like OTP requests due to its speed and TTL features. Alternatively, use your main application database (PostgreSQL, MySQL, MongoDB).
7. Security Features
- Input Validation: The backend validates the phone number format (E.164) and OTP code format. For production robustness, strongly consider using a dedicated library like
libphonenumber-js
for comprehensive phone number validation instead of basic regex. Sanitize all inputs. - Rate Limiting: Crucial to prevent abuse. Implement rate limiting on both
/request-otp
(e.g., 1 request per minute per number/IP) and/verify-otp
(e.g., 5 attempts perrequest_id
). Use libraries likeexpress-rate-limit
. Vonage also applies its own rate limits. - Secure API Keys: Never commit
.env
files or hardcode keys. Use environment variables in your deployment environment. - HTTPS: Always use HTTPS in production for both frontend and backend.
- Request ID Handling: Don't expose internal database IDs. Use the opaque
vonage_request_id
for verification checks. - CSRF Protection: If your frontend uses cookies/sessions for authentication with the backend, implement CSRF protection (e.g., using
csurf
middleware). This is less critical if using token-based authentication (like JWTs) stored in localStorage/sessionStorage, but still good practice.
8. Handling Special Cases
- Phone Number Formatting: Always normalize numbers to E.164 format (
+
followed by country code and number) before sending to Vonage. - Vonage Brand Name: Limited to 11 alphanumeric characters. Choose carefully.
- OTP Expiry: Vonage handles the expiry (typically 5 minutes). Your UI should ideally reflect this or allow requesting a new code after a timeout. The backend handles expired codes (
status: '6'
). - Concurrent Requests: Vonage prevents sending multiple codes too quickly to the same number (error status
10
). The backend example doesn't explicitly handle this, but a production implementation should catch this error (429 Too Many Requests
often), potentially inform the user, or manage the flow via the database state. - Network Errors: Implement retries with exponential backoff for transient network issues when calling the Vonage API. Since the frontend example uses
fetch
, you might need to implement this manually or use afetch
wrapper library that supports retries. The backend SDK might have some internal retry logic, but explicit handling can be beneficial.
9. Performance Optimizations
- Database Indexing: Index the
vonage_request_id
column in your database table for fast lookups during verification. - Asynchronous Operations: Node.js is inherently non-blocking. Ensure all I/O operations (database calls, Vonage API calls) use
async/await
or Promises correctly to avoid blocking the event loop. - Efficient Code: Keep API logic concise and avoid unnecessary computations.
- Caching: Caching isn't typically applied directly to the OTP flow itself, but ensure general API performance best practices are followed.
10. Monitoring, Observability, and Analytics
- Logging: Implement structured logging (JSON format) in the backend. Log key events like OTP request initiation, success, failure (with error details), and verification attempts. Include correlation IDs (like
requestId
) in logs. - Health Checks: The
/health
endpoint provides a basic check. Expand this to verify database connectivity and Vonage API reachability if possible. - Metrics: Track metrics like:
- Number of OTPs requested/verified/failed.
- API endpoint latency (
/request-otp
,/verify-otp
). - Error rates (HTTP 4xx/5xx).
- Vonage API error rates/types.
- Use tools like Prometheus/Grafana or Datadog.
- Error Tracking: Integrate services like Sentry or Bugsnag to capture and alert on backend exceptions and frontend errors.
- Dashboards: Create dashboards visualizing the key metrics to monitor the health and usage of the OTP system.
11. Troubleshooting and Caveats
- Vonage API Keys Invalid: (Error like
401 Unauthorized
) Double-check keys in.env
and ensure the file is loaded correctly (dotenv.config()
is called early). - Invalid Phone Number Format: (Vonage error status
3
or backend 400) Ensure E.164 format (+1...
). Use validation libraries. - Rate Limited / Throttled: (Vonage error status
1
or HTTP 429) You're sending requests too frequently. Implement backend rate limiting and inform the user. Check Vonage account limits. - Incorrect Code: (Vonage error status
16
or backend 400) User entered the wrong code. Handled in the/verify-otp
logic. - Expired Code/Request ID: (Vonage error status
6
or backend 410/404) The code expired (usually 5 mins), or therequestId
is wrong/not found. The user needs to request a new code. - Too Many Wrong Attempts: (Vonage error status
17
or backend 429) User tried the wrong code too many times for a givenrequestId
. Invalidate the request and force requesting a new one. - CORS Errors: (Browser console error) Ensure the
cors
middleware inserver.js
is configured correctly, especially theorigin
option (use*
for testing, but restrict it to your frontend domain in production). .env
Not Loading: Ensuredotenv.config()
is called at the very top ofserver.js
before accessingprocess.env
variables.- In-Memory Store Limitations: The demo store will lose data on server restarts. Do not use in production.
- Vonage Service Issues: Check the Vonage Status Page if you suspect an outage. Implement fallback mechanisms if high availability is critical (though complex for OTP).
12. Deployment and CI/CD
- Environment Variables: Configure
VONAGE_API_KEY
,VONAGE_API_SECRET
,PORT
, and the production frontendorigin
URL for CORS securely in your deployment environment (e.g., using platform secrets management). - Database: Provision and configure your chosen production database (e.g., managed Redis, PostgreSQL).
- Build Process:
- Frontend: Run
npm run build
in thefrontend
directory. Serve the static files from thedist
folder using a static file server (like Nginx, Vercel, Netlify, or integrated into your backend). - Backend: Ensure Node.js is installed on the server. You might use a process manager like PM2 to keep the Node.js server running.
- Frontend: Run
- HTTPS: Configure HTTPS for both frontend and backend traffic.
- CI/CD Pipeline: Set up a pipeline (e.g., using GitHub Actions, GitLab CI, Jenkins) to automate testing, building, and deploying the frontend and backend.