Two-factor authentication (2FA), often implemented via One-Time Passwords (OTPs) sent over SMS, adds a critical layer of security to user accounts and transactions. It verifies user identity by requiring something they know (password) and something they have (access to their phone).
This guide provides a step-by-step walkthrough for building a secure and robust SMS OTP verification system using Node.js, Express, and the Vonage Verify API. We will create simple API endpoints capable of requesting an OTP code sent to a user's phone number and verifying the code entered by the user.
Project Goals:
- Build an Express application with two core API endpoints:
/request-otp
: Accepts a phone number and initiates an OTP verification request via Vonage./verify-otp
: Accepts a Vonage request ID and the user-submitted OTP code to verify the code's validity.
- Securely handle Vonage API credentials.
- Implement basic input validation and error handling.
- Provide a foundation for integrating SMS OTP into larger authentication systems.
Technology Stack:
- Node.js: A JavaScript runtime environment for building server-side applications.
- Express: A minimal and flexible Node.js web application framework.
- Vonage Verify API: A service that handles the generation and delivery of OTPs via SMS (and voice fallback) and manages the verification process.
@vonage/server-sdk
: The official Vonage Server SDK for Node.js to interact with the Vonage API.dotenv
: A module to load environment variables from a.env
file intoprocess.env
.body-parser
: Node.js body parsing middleware (included directly in Express v4.16.0+, but explicit usage can enhance clarity for all versions).
System Architecture:
The flow involves the following components:
- Client Application (e.g., Web Frontend, Mobile App): Initiates the OTP request with a phone number and later submits the received code for verification. (Not built in this guide, but interacts with our API).
- Node.js/Express API Server (This Guide):
- Receives requests from the client.
- Uses the Vonage SDK to interact with the Vonage Verify API.
- Sends OTP requests to Vonage.
- Sends verification checks to Vonage.
- Returns responses (success/failure, request ID) to the client.
- Vonage Verify API:
- Receives requests from our API server.
- Generates and sends the OTP SMS to the user's phone.
- Handles OTP lifecycle (expiry, retries via different channels if configured).
- Validates submitted codes against the specific request.
- Returns verification results to our API server.
A simplified interaction diagram:
Client App ---> Node.js/Express API ---> Vonage Verify API ---> User's Phone (SMS)
| | |
| <-- Request ID-| |
| | |
|--- OTP Code -->| |
| | |
| |--- Verify Code ------>|
| | |
| |<-- Verification Result-|
| | |
|<-- Final Result| |
Prerequisites:
- Node.js and npm (or yarn): Installed on your development machine. Download Node.js here.
- Vonage API Account: Sign up for a free Vonage account.
- Vonage API Key and Secret: Find these at the top of your Vonage API Dashboard after signing up.
- Vonage Brand Name: You'll use this in the SMS message (e.g._
""YourApp""
). This can often be set within the Vonage dashboard or passed via the API. See Section 3 and Section 7 for notes on configuration and potential restrictions. - (Optional but Recommended) Vonage Virtual Number: While the Verify API can often use an alphanumeric sender ID or a shared number pool_ having a dedicated Vonage number can be useful for consistency and testing. You can purchase one via Numbers > Buy Numbers in the dashboard.
1. Setting up the project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
mkdir vonage-otp-app cd vonage-otp-app
-
Initialize Node.js Project: Create a
package.json
file to manage project dependencies and scripts.npm init -y
-
Install Dependencies: Install Express, the Vonage Server SDK,
dotenv
for environment variable management, andbody-parser
for handling request bodies.npm install express @vonage/server-sdk dotenv body-parser
express
: The web framework.@vonage/server-sdk
: To interact with the Vonage APIs.dotenv
: To load environment variables from a.env
file.body-parser
: To parse incoming JSON request bodies.
-
Create
.env
File: Create a file named.env
in the root of your project. This file will store your sensitive API credentials and configuration. Never commit this file to version control.# .env VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET VONAGE_BRAND_NAME=""Your App Name"" # Replace with your app's name PORT=3000 # Optional: Define the port the server runs on
Replace
YOUR_VONAGE_API_KEY
andYOUR_VONAGE_API_SECRET
with the actual credentials from your Vonage dashboard. SetVONAGE_BRAND_NAME
to the name users will see in the SMS message (e.g.,""MyApp Security""
). -
Create
.gitignore
File: Create a.gitignore
file to prevent sensitive files and unnecessary directories from being committed to Git.# .gitignore node_modules .env npm-debug.log* yarn-debug.log* yarn-error.log*
-
Create
index.js
: Create the main application file,index.js
, in the project root.touch index.js
-
Basic Server Setup: Add the following initial setup code to
index.js
:// index.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const bodyParser = require('body-parser'); const { Vonage } = require('@vonage/server-sdk'); // --- Input Validation --- // Basic validation: Check if a string is a plausible phone number (E.164 format is ideal) // Production apps need more robust validation (e.g., using libraries like libphonenumber-js) const isValidPhoneNumber = (phoneNumber) => { // Very basic check: starts with '+', contains digits, reasonable length const phoneRegex = /^\+[1-9]\d{1,14}$/; return typeof phoneNumber === 'string' && phoneRegex.test(phoneNumber); }; // Basic validation: Check if the code is a string of 4 to 6 digits const isValidOtpCode = (code) => { const codeRegex = /^\d{4,6}$/; return typeof code === 'string' && codeRegex.test(code); }; // --- Vonage Initialization --- // Ensure API key and secret are loaded if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET) { console.error('Error: VONAGE_API_KEY or VONAGE_API_SECRET is not set in the environment variables.'); process.exit(1); // Exit if credentials are missing } const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET }); // --- Express App Setup --- const app = express(); const port = process.env.PORT || 3000; // Middleware to parse JSON request bodies (included in Express 4.16.0+, but explicit use is clear) app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); // --- API Endpoints (To be added in the next section) --- // --- Simple Health Check Endpoint --- app.get('/health', (req, res) => { res.status(200).send('OK'); }); // --- Start Server --- app.listen(port, () => { console.log(`Server listening on port ${port}`); });
This code:
- Loads environment variables using
dotenv
. - Imports necessary modules.
- Includes basic placeholder functions for phone number and OTP code validation (emphasizing that production requires more robust checks).
- Initializes the Vonage SDK with credentials from
.env
. Includes a check to ensure credentials are set. - Sets up the Express application and middleware.
- Adds a basic
/health
endpoint. - Starts the server.
- Loads environment variables using
2. Implementing Core Functionality & API Layer
Now, let's build the two main API endpoints for requesting and verifying OTPs.
Requesting an OTP
This endpoint will receive a phone number, trigger the Vonage Verify process, and return the request_id
needed for the verification step.
-
Add the
/request-otp
Endpoint: Add the following route handler to yourindex.js
file, typically placed before the/health
endpoint or server start logic.// index.js // ... (previous setup code) ... // --- API Endpoints --- // POST /request-otp // Initiates an OTP verification request app.post('/request-otp', async (req, res) => { const { phoneNumber } = req.body; // 1. Input Validation if (!isValidPhoneNumber(phoneNumber)) { console.warn(`Invalid phone number format received: ${phoneNumber}`); return res.status(400).json({ success: false, error: 'Invalid phone number format. Use E.164 format (e.g., +14155552671).' }); } const brand = process.env.VONAGE_BRAND_NAME || ""MyApp""; // Fallback brand name console.log(`Requesting OTP for phone number: ${phoneNumber} with brand: ${brand}`); try { // 2. Call Vonage Verify API const result = await vonage.verify.start({ number: phoneNumber, brand: brand, // code_length: '6' // Optional: Specify 6 digits (default is 4) // pin_expiry: 300 // Optional: Set expiry time in seconds (default 300 = 5 minutes) // workflow_id: 6 // Optional: Specify SMS -> TTS -> TTS workflow }); // 3. Handle Vonage Response if (result.status === '0') { // Success - Status '0' indicates the request was accepted console.log(`OTP request successful for ${phoneNumber}. Request ID: ${result.request_id}`); return res.status(200).json({ success: true, requestId: result.request_id }); } else { // Vonage returned an error status console.error(`Vonage Verify API error for ${phoneNumber}. Status: ${result.status}, Error: ${result.error_text}`); // Map common Vonage error statuses to user-friendly messages or appropriate HTTP status codes let statusCode = 500; let errorMessage = result.error_text || 'Failed to send OTP.'; if (result.status === '3') { // Invalid number format statusCode = 400; errorMessage = 'Invalid phone number format provided to Vonage.'; } else if (result.status === '10') { // Concurrent verifications to the same number statusCode = 429; // Too Many Requests errorMessage = 'Concurrent verification attempts detected. Please wait before trying again.'; } else if (result.status === '9') { // Partner quota exceeded statusCode = 503; // Service Unavailable errorMessage = 'Verification service quota exceeded. Please try again later.'; } return res.status(statusCode).json({ success: false, error: errorMessage, vonage_status: result.status }); } } catch (error) { // 4. Handle SDK/Network Errors console.error(`Error requesting OTP for ${phoneNumber}:`, error); return res.status(500).json({ success: false, error: 'An unexpected error occurred while requesting the OTP.' }); } }); // ... (rest of the code: /verify-otp, /health, app.listen) ...
Explanation:
- It extracts the
phoneNumber
from the JSON request body. - Input Validation: It uses the basic
isValidPhoneNumber
check. Returns a 400 Bad Request if invalid. Remember: Use a robust library likegoogle-libphonenumber
via a Node.js wrapper (e.g.,libphonenumber-js
) in production for proper E.164 validation and formatting. - It retrieves the brand name from environment variables.
- It calls
vonage.verify.start()
with the phone number and brand. Optional parameters likecode_length
,pin_expiry
, andworkflow_id
are commented out but show how to customize the process. - Vonage Response Handling:
- If
result.status
is'0'
, the request was successful. It returns a 200 OK response withsuccess: true
and the crucialrequestId
. The client must store thisrequestId
to use in the verification step. - If
result.status
is non-zero, an error occurred at the Vonage API level. We log the details and map common statuses (like '3' for invalid number, '10' for concurrent requests) to appropriate HTTP status codes (400, 429) and user-friendly error messages. A generic 500 error is returned for unmapped or unexpected Vonage errors.
- If
- SDK/Network Error Handling: A
try...catch
block handles potential errors during the SDK call itself (e.g., network issues, invalid credentials setup). It returns a 500 Internal Server Error.
- It extracts the
Verifying the OTP
This endpoint receives the request_id
(obtained from the previous step) and the OTP code
entered by the user. It asks Vonage to check if the code is correct for that specific request.
-
Add the
/verify-otp
Endpoint: Add the following route handler toindex.js
, after the/request-otp
endpoint.// index.js // ... (previous setup code including /request-otp) ... // POST /verify-otp // Verifies the OTP code submitted by the user app.post('/verify-otp', async (req, res) => { const { requestId, code } = req.body; // 1. Input Validation if (!requestId || typeof requestId !== 'string' || requestId.trim() === '') { return res.status(400).json({ success: false, error: 'Request ID is required.' }); } if (!isValidOtpCode(code)) { console.warn(`Invalid OTP code format received for request ID ${requestId}: ${code}`); return res.status(400).json({ success: false, error: 'Invalid OTP code format. Must be 4-6 digits.' }); } console.log(`Verifying OTP for request ID: ${requestId} with code: ${code}`); try { // 2. Call Vonage Verify Check API const result = await vonage.verify.check(requestId, code); // 3. Handle Vonage Response if (result.status === '0') { // Success - Status '0' means the code was correct console.log(`OTP verification successful for request ID: ${requestId}`); return res.status(200).json({ success: true, message: 'OTP verified successfully.' }); } else { // Vonage returned an error status (e.g., wrong code, expired request) console.warn(`Vonage Verify Check API error for request ID ${requestId}. Status: ${result.status}, Error: ${result.error_text}`); let statusCode = 401; // Unauthorized (or Bad Request 400) is often suitable for failed verification let errorMessage = result.error_text || 'OTP verification failed.'; if (result.status === '16') { // The code provided was incorrect errorMessage = 'Incorrect OTP code provided.'; } else if (result.status === '6') { // The verify request associated with this request_id has expired errorMessage = 'OTP request has expired. Please request a new code.'; statusCode = 410; // Gone (or 400 Bad Request) } else if (result.status === '17') { // A wrong code was provided too many times errorMessage = 'Too many incorrect attempts. Please request a new code.'; statusCode = 429; // Too Many Requests } else if (result.status === '5') { // Invalid request ID format errorMessage = 'Internal error: Invalid request ID format.'; // More likely a server issue statusCode = 500; } return res.status(statusCode).json({ success: false, error: errorMessage, vonage_status: result.status }); } } catch (error) { // 4. Handle SDK/Network Errors console.error(`Error verifying OTP for request ID ${requestId}:`, error); // Check if the error indicates the request ID doesn't exist // Note: The exact error structure (like status '101') can vary based on SDK updates or Vonage API changes. // Always consult the official Vonage documentation and test error responses. // This is an example check: if (error.body && error.body.status === '101' && error.body.error_text === 'No matching request found') { console.warn(`Attempted to verify non-existent or already verified request ID: ${requestId}`); return res.status(404).json({ success: false, error: 'Verification request not found or already completed.' }); } return res.status(500).json({ success: false, error: 'An unexpected error occurred during OTP verification.' }); } }); // ... (/health, app.listen) ...
Explanation:
- It extracts
requestId
andcode
from the request body. - Input Validation: Checks if both
requestId
andcode
are present and use the basicisValidOtpCode
check. Returns 400 Bad Request if invalid. - It calls
vonage.verify.check()
with therequestId
andcode
. - Vonage Response Handling:
- If
result.status
is'0'
, the code was correct. It returns 200 OK withsuccess: true
. - If non-zero, the code was incorrect, the request expired, or another error occurred. We log the details and map common statuses ('16' for wrong code, '6' for expired, '17' for too many attempts) to appropriate HTTP status codes (often 401 Unauthorized or 400 Bad Request, but 410 Gone for expired or 429 for rate-limited attempts can be more specific) and user-friendly messages.
- If
- SDK/Network Error Handling: The
try...catch
handles SDK or network errors. It includes an example check for a specific error structure (like status '101' - No matching request found) which might indicate therequestId
was invalid, already used, or expired, returning a 404 Not Found in that case. This specific check might need adjustment based on observed errors from the SDK. Otherwise, it returns a 500 Internal Server Error.
- It extracts
3. Integrating with Vonage (Configuration Details)
Properly configuring the Vonage integration involves obtaining credentials and understanding the environment variables used.
-
Obtain API Key and Secret:
- Log in to your Vonage API Dashboard.
- Your API Key and API Secret are displayed prominently on the main dashboard page.
- Copy these values carefully.
-
Set Environment Variables:
- Open the
.env
file you created earlier. - Paste your API Key as the value for
VONAGE_API_KEY
. - Paste your API Secret as the value for
VONAGE_API_SECRET
. - Set
VONAGE_BRAND_NAME
to the name you want displayed in the SMS message (e.g.,VONAGE_BRAND_NAME=""TechCorp Security""
). This helps users identify the source of the OTP. Note that Vonage may have character limits or other restrictions on brand names depending on the destination country and regulations; consult the Vonage documentation for details. Also, see the note on Alphanumeric Sender IDs in Section 7.
# .env - Example filled values VONAGE_API_KEY=a1b2c3d4 VONAGE_API_SECRET=s3cr3tK3yF0rV0n4g3 VONAGE_BRAND_NAME=""TechCorp Security"" PORT=3000
Security: Remember, the
.env
file contains sensitive credentials.- Ensure it's listed in your
.gitignore
file. - Do not commit it to version control (GitHub, GitLab, etc.).
- Use secure methods for managing environment variables in production environments (e.g., platform-specific configuration services like AWS Secrets Manager, Azure Key Vault, or system environment variables).
- Open the
4. Error Handling and Logging
We've already incorporated basic error handling, but let's refine the strategy.
- Consistent Error Responses: Our endpoints return JSON objects with a
success
flag (boolean) and either a relevant payload (likerequestId
) on success or anerror
message (string) on failure. Including thevonage_status
in error responses can aid debugging. - HTTP Status Codes: Use appropriate HTTP status codes to signal the nature of the error (400 for bad input, 401/403 for auth failures, 404 for not found, 429 for rate limits, 500 for server errors, 503 for service unavailable).
- Logging:
- We use
console.log
for informational messages (request received, success) andconsole.warn
orconsole.error
for issues. - Production Logging: For production, use a dedicated logging library like
winston
orpino
. These offer features like:- Different log levels (debug, info, warn, error).
- Structured logging (JSON format) for easier parsing by log analysis tools.
- Log rotation and multiple transport options (file, console, external services).
- Log Sensitivity: Be careful not to log sensitive user data like the OTP code itself, unless strictly necessary for debugging under controlled conditions. Log
requestId
and phone number (potentially masked) for correlation.
- We use
Example of enhancing logging (conceptual):
// Conceptual enhancement with a logger library (e.g., Winston)
// const winston = require('winston');
// const logger = winston.createLogger({ /* ... configuration ... */ });
// Inside /request-otp error handler:
// logger.error(`Vonage Verify API error for ${phoneNumber}. Status: ${result.status}, Error: ${result.error_text}`, { phoneNumber, vonageStatus: result.status });
// Inside /verify-otp catch block:
// logger.error(`Error verifying OTP for request ID ${requestId}:`, { error, requestId });
5. Security Features
Implementing OTP is a security feature itself, but the implementation needs protection.
-
Input Validation:
- Phone Numbers: As mentioned, use a robust library (e.g.,
libphonenumber-js
) to validate and potentially format phone numbers into the E.164 standard (+14155552671
) before sending them to Vonage. This prevents errors and potential injection issues. - OTP Codes: Validate that the code format matches expectations (e.g., 4-6 digits).
- Request IDs: Validate that
requestId
looks like a valid identifier (though Vonage handles the actual validation).
- Phone Numbers: As mentioned, use a robust library (e.g.,
-
Secure Credential Storage: Use environment variables and secure management practices, never hardcode secrets.
-
Rate Limiting: This is critical for OTP endpoints:
- Prevents Abuse: Stops attackers from spamming users with OTP messages (toll fraud).
- Brute-Force Protection: Limits how many times a user (or attacker) can attempt to verify a code for a given
requestId
or from a specific IP address. - Implementation: Use middleware like
express-rate-limit
. Apply separate limits for requesting OTPs (e.g., 5 requests per phone number per hour) and verifying codes (e.g., 5 attempts perrequestId
, 10 attempts per IP per minute).
# Install rate limiter npm install express-rate-limit
// index.js - Add near the top after `app = express()` const rateLimit = require('express-rate-limit'); // Rate limiter for requesting OTPs (e.g., per IP) const requestOtpLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 10, // Limit each IP to 10 OTP requests per windowMs message: { success: false, error: 'Too many OTP requests created from this IP, please try again after 15 minutes' }, standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); // Rate limiter for verifying OTPs (e.g., per IP) const verifyOtpLimiter = rateLimit({ windowMs: 5 * 60 * 1000, // 5 minutes max: 5, // Limit each IP to 5 verification attempts per windowMs (adjust based on Vonage's attempt limits) message: { success: false, error: 'Too many verification attempts from this IP, please try again after 5 minutes' }, standardHeaders: true, legacyHeaders: false, }); // Apply limiters to specific routes app.use('/request-otp', requestOtpLimiter); app.use('/verify-otp', verifyOtpLimiter); // ... (rest of the app setup and routes)
Note: IP-based limiting is basic. More sophisticated limiting might involve user accounts or phone numbers, requiring session management or a database.
-
HTTPS: Always serve your application over HTTPS in production to encrypt communication between the client and your server. Use a reverse proxy like Nginx or Caddy, or platform services (Heroku, AWS Load Balancer) to handle TLS termination.
6. Database Schema and Data Layer (Considerations)
While this guide doesn't implement a database, a real-world application almost certainly needs one:
- Storing
request_id
: You need to associate therequestId
returned by/request-otp
with the user's session or account attempting verification. When the user submits the code to/verify-otp
, you retrieve the correctrequestId
from their session/database record. - Tracking Verification Status: Store whether a user's phone number has been successfully verified.
- Rate Limiting by User/Phone: Implement more granular rate limiting based on user ID or phone number stored in the database, rather than just IP address.
- Audit Trails: Log verification attempts (success/failure, timestamp, user ID, IP address) for security monitoring.
Example Schema Snippets (Conceptual - using Prisma as an example):
// schema.prisma (Conceptual Example)
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
phoneNumber String? @unique
phoneVerified Boolean @default(false)
// ... other fields
otpRequests OtpRequest[]
}
model OtpRequest {
id String @id @default(cuid())
vonageRequestId String @unique // The ID from Vonage
userId String
user User @relation(fields: [userId], references: [id])
expiresAt DateTime // Calculate based on Vonage pin_expiry
createdAt DateTime @default(now())
verifiedAt DateTime?
attempts Int @default(0) // Track verification attempts for this request
}
This requires setting up a database, an ORM like Prisma or Sequelize, and integrating data access logic into your route handlers.
7. Troubleshooting and Caveats
- Invalid API Credentials: Ensure
VONAGE_API_KEY
andVONAGE_API_SECRET
in your.env
file are correct and have no extra spaces. Error messages often include ""authentication failed"" or similar. - Incorrect Phone Number Format: Vonage Verify generally expects E.164 format (
+14155552671
). Using local formats might work for some regions but can be unreliable. Use a library for robust validation/formatting. Vonage status3
often indicates this. - Concurrent Verifications: Vonage typically prevents sending multiple OTPs to the same number within a short period (e.g., 30 seconds). This results in Vonage status
10
. Implement logic to prevent users from rapidly clicking ""Resend Code"" or handle this error gracefully. Consider adding cancellation logic if needed (usingvonage.verify.cancel(REQUEST_ID)
). - Expired Request ID: OTP codes expire (default 5 minutes). Trying to verify an expired code results in Vonage status
6
. Inform the user the code expired and prompt them to request a new one. - Incorrect Code Entered: Results in Vonage status
16
. After too many attempts (Vonage default might be 5), status17
occurs. Implement attempt tracking on your side or rely on Vonage's limits, informing the user clearly. - Rate Limits Hit: If you implement
express-rate-limit
or hit Vonage's own internal limits, requests will fail (often with HTTP 429). Ensure your limits are reasonable. Vonage status9
indicates hitting their platform quota. - Non-Existent Request ID: Trying to verify a
requestId
that was never generated, already verified, or cancelled might result in Vonage status101
(No matching request found - often surfaced as an SDK error rather than a non-zero status in thecheck
result) or potentially status5
. Return a 404 or 400 error. - Vonage Service Issues: Occasionally, the Vonage API might experience downtime or delays. Implement appropriate timeouts and consider fallback mechanisms if critical. Monitor Vonage's status page.
- Alphanumeric Sender ID (
VONAGE_BRAND_NAME
Behavior): Using a brand name as the sender ID depends heavily on carrier support and regulations in the destination country. In some regions (like the US), carriers might replace the alphanumeric sender ID with a shared short code or long code number pool managed by Vonage for compliance reasons. This means theVONAGE_BRAND_NAME
you set might not always appear as the sender. Check Vonage documentation for country-specific sender ID behavior and potential registration requirements for Alphanumeric Sender IDs where supported.
8. Deployment and CI/CD
- Environment Variables: Use your deployment platform's mechanism for setting environment variables (e.g., Heroku Config Vars, AWS Parameter Store, Docker environment files). Do not commit
.env
files to your production repository. - Build Process: If using TypeScript or a build step, ensure your
Dockerfile
or deployment script includes compiling the code. For plain JavaScript, usually just need to copy files and install production dependencies (npm install --omit=dev
). PORT
Binding: Ensure your application listens on the port specified by the environment (oftenprocess.env.PORT
), not a hardcoded port like3000
. Our code already does this (process.env.PORT || 3000
).- HTTPS: Configure HTTPS termination via a load balancer or reverse proxy.
- CI/CD Pipeline (Conceptual):
- Trigger: On push to
main
branch or tag creation. - Checkout Code: Get the latest source.
- Install Dependencies:
npm ci
(usespackage-lock.json
for deterministic installs). - (Optional) Linting/Testing: Run
eslint
,npm test
. - (Optional) Build:
npm run build
(if applicable). - Package: Create a Docker image or deployment artifact.
- Deploy: Push the image to a registry and deploy to your platform (Heroku, AWS ECS, Kubernetes, etc.).
- Smoke Tests: Run basic checks against the deployed application (e.g., hit the
/health
endpoint).
- Trigger: On push to
- Rollback: Have a plan to quickly revert to a previous working version if a deployment fails.
Example Dockerfile
(Simple):
# Dockerfile
# Use an official Node.js runtime as a parent image
# Using 'slim' variant for smaller image size
FROM node:18-slim
# Set the working directory in the container
WORKDIR /usr/src/app
# Copy package.json and package-lock.json (or yarn.lock)
# Copying these first leverages Docker layer caching
COPY package*.json ./
# Install only production dependencies using 'npm ci'
# Use 'npm ci' for faster, more reliable installs in CI/CD environments
# It requires a package-lock.json or npm-shrinkwrap.json
RUN npm ci --omit=dev
# Bundle app source
COPY . .
# Your app binds to port 3000, but PORT env var can override it
# Expose the port the app runs on (useful for documentation, less critical for runtime)
EXPOSE 3000
# Define the command to run your app
CMD [ ""node"", ""index.js"" ]