This guide provides a step-by-step walkthrough for building a production-ready Node.js application using the Express framework to send SMS messages via the Vonage Messages API. We will cover project setup, core implementation, API endpoint creation, configuration, error handling, security considerations, and deployment.
By the end of this tutorial, you will have a functional Express API endpoint capable of accepting a phone number and message, and using Vonage to deliver that message as an SMS. This enables applications to programmatically send notifications, alerts, verification codes, or other communications directly to users' mobile devices.
Project Overview and Goals
What We'll Build: A simple REST API built with Node.js and Express. This API will expose a single endpoint (/send
) that accepts a POST request containing a destination phone number and a message text. It will then use the Vonage Messages API (via the @vonage/server-sdk
) to send the message as an SMS.
Problem Solved: Provides a backend service layer to abstract the complexities of interacting directly with an SMS provider API, enabling frontend applications or other services to easily trigger SMS sends through a simple HTTP request.
Technologies:
- Node.js: A JavaScript runtime environment enabling server-side execution. Chosen for its asynchronous nature, large ecosystem (npm), and suitability for I/O-bound tasks like API interactions.
- Express: A minimal and flexible Node.js web application framework. Chosen for its simplicity in setting up routes, middleware, and handling HTTP requests/responses.
- Vonage Messages API: A unified API from Vonage for sending messages across various channels (SMS, MMS, WhatsApp, etc.). Chosen for its robustness, global reach, and developer-friendly SDK. We will use the
@vonage/server-sdk
Node.js library. - dotenv: A module to load environment variables from a
.env
file intoprocess.env
. Chosen for securely managing API keys and configuration outside the codebase.
System Architecture: (Simplified text representation)
+-------------+ +-----------------------+ +-----------------+ +--------------+
| User/Client | ----> | Node.js/Express API | ----> | Vonage SDK | ----> | Vonage API |
| (e.g., Web | POST | (Listens on Port XXXX)| | (@vonage/server | | (Sends SMS) |
| App, Mobile)| /send | - Validates Request | | -sdk) | | |
| | | - Calls Vonage SDK | | - Authenticates | | |
| | | - Returns Response | | - Formats Request| | |
+-------------+ +-----------------------+ +-----------------+ +--------------+
| | |
| +-------------------<------------------------------------+
| SMS Delivery
+------------------------------------<--------------------------------------------+
User Receives SMS
Prerequisites:
- Node.js and npm (or yarn): Installed on your system. Download from nodejs.org.
- Vonage API Account: Sign up for free at Vonage API Dashboard. You'll receive some free credits for testing.
- Vonage Application: You need to create a Vonage Application and generate a private key.
- Vonage Phone Number: You need a Vonage virtual number capable of sending SMS_ linked to your Vonage Application.
- Basic JavaScript/Node.js Knowledge: Familiarity with JavaScript syntax and Node.js concepts.
- Terminal/Command Line Access: For running commands.
- (Optional) ngrok: Useful for testing webhooks if you extend this to receive SMS_ but not strictly required for sending only. Download from ngrok.com.
- (Optional) Postman or curl: For testing the API endpoint.
Final Outcome: A running Node.js Express application with a /send
endpoint that successfully sends an SMS via Vonage when called with a valid phone number and message. The application will include basic error handling and secure configuration management.
1. Setting up the project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal or command prompt and create a new directory for your project_ then navigate into it.
mkdir vonage-sms-sender cd vonage-sms-sender
-
Initialize Node.js Project: This command creates a
package.json
file_ which tracks project details and dependencies. The-y
flag accepts default settings.npm init -y
Optional: Add
"type": "module"
to yourpackage.json
if you prefer using ES Moduleimport
/export
syntax. If you do_ replacerequire
withimport
(e.g._import express from 'express';
)_module.exports
withexport default
or named exports_ and be mindful of differences like needing file extensions in imports and usingimport.meta.url
for path resolution instead of__dirname
. This guide uses the default CommonJSrequire
syntax. -
Install Dependencies: We need Express for the web server_ the Vonage Server SDK to interact with the API_ and
dotenv
for environment variable management.npm install express @vonage/server-sdk dotenv --save
express
: Web framework.@vonage/server-sdk
: Official Vonage library for Node.js (version 3+ uses Application ID/Private Key for Messages API).dotenv
: Loads environment variables from a.env
file.
-
Create Project Structure: Create the necessary files for our application.
touch index.js .env .gitignore # Optional: mkdir middleware logger services utils
index.js
: The main entry point for our application logic..env
: Stores sensitive credentials and configuration (API keys_ phone numbers). Never commit this file to version control..gitignore
: Specifies files and directories that Git should ignore.
-
Configure
.gitignore
: Addnode_modules
and.env
to your.gitignore
file to prevent committing dependencies and secrets.# .gitignore node_modules/ .env *.log private.key
We also add
private.key
as we'll download this file from Vonage later and should not commit it. -
Configure
.env
File: Open the.env
file and add the following placeholders. We will fill these in later.# .env # Server Configuration PORT=3000 # Vonage API Credentials & Configuration VONAGE_APPLICATION_ID=YOUR_VONAGE_APPLICATION_ID VONAGE_APPLICATION_PRIVATE_KEY_PATH=./private.key # Alternative: Store key content directly if file path is not feasible (e.g._ some PaaS) # VONAGE_PRIVATE_KEY_CONTENT="-----BEGIN PRIVATE KEY-----\nYOUR_KEY_CONTENT...\n-----END PRIVATE KEY-----" # Optional: API Key/Secret - Useful for Vonage CLI_ Account/Number management APIs_ or older APIs. # Not used for Messages API authentication with Application ID/Private Key. VONAGE_API_KEY=YOUR_VONAGE_API_KEY VONAGE_API_SECRET=YOUR_VONAGE_API_SECRET VONAGE_NUMBER=YOUR_VONAGE_VIRTUAL_NUMBER # Your Vonage sending number in E.164 format (e.g._ +15551234567) # Test Recipient (Optional_ needed for Trial Accounts) TEST_RECIPIENT_NUMBER=RECIPIENT_PHONE_NUMBER # Recipient number in E.164 format (e.g._ +15559876543) # Optional: API Key for securing your own endpoint # INTERNAL_API_KEY=your-secret-api-key-here
- Purpose: Separating configuration from code enhances security and makes deployment to different environments easier.
VONAGE_APPLICATION_ID
/VONAGE_APPLICATION_PRIVATE_KEY_PATH
(or_CONTENT
): Used by the@vonage/server-sdk
(v3+) to authenticate Messages API calls.VONAGE_API_KEY
/VONAGE_API_SECRET
: Your main Vonage account credentials. Included here as they are often useful for other Vonage interactions (like CLI setup or using other Vonage APIs) even if not used for authenticating this specificsend
request.VONAGE_NUMBER
: The Vonage virtual number messages will be sent from.PORT
: The port your Express server will listen on.
2. Implementing core functionality
Now_ let's write the code to initialize the Vonage SDK and create the function to send SMS messages.
-
Load Environment Variables: At the very top of
index.js
_ require and configuredotenv
to load the variables from your.env
file intoprocess.env
.// index.js require('dotenv').config(); // Load environment variables from .env file
-
Require Dependencies: Import the necessary modules:
express
and theVonage
class from the SDK.// index.js require('dotenv').config(); const express = require('express'); const { Vonage } = require('@vonage/server-sdk'); const path = require('path'); // To resolve the private key path if used
-
Initialize Vonage SDK: Create an instance of the Vonage client using your Application ID and private key. Prioritize key content from environment variable if available_ otherwise use the path.
// index.js // ... (requires) // --- Vonage SDK Initialization --- // Determine private key source: prefer content from env var_ fallback to path const privateKeySource = process.env.VONAGE_PRIVATE_KEY_CONTENT ? process.env.VONAGE_PRIVATE_KEY_CONTENT.replace(/\\n/g_ '\n') // Handle escaped newlines if needed : path.resolve(process.env.VONAGE_APPLICATION_PRIVATE_KEY_PATH); const vonage = new Vonage({ applicationId: process.env.VONAGE_APPLICATION_ID_ privateKey: privateKeySource }); // --- Helper Function to Send SMS --- async function sendSms(recipient_ messageText) { const fromNumber = process.env.VONAGE_NUMBER; const toNumber = recipient; // Expecting E.164 format try { console.log(`Attempting to send SMS from ${fromNumber} to ${toNumber}`); const response = await vonage.messages.send({ message_type: ""text""_ // Specify message type as text text: messageText_ // The content of the SMS to: toNumber_ // Recipient phone number from: fromNumber_ // Your Vonage virtual number channel: ""sms"" // Specify the channel as SMS }); console.log(`SMS submitted successfully to ${recipient}. Message UUID: ${response.message_uuid}`); return { success: true_ message_uuid: response.message_uuid }; } catch (error) { // Log detailed error information_ especially Vonage API errors let errorMessage = `Error sending SMS to ${recipient}: ${error.message}`; let vonageErrorDetails = null; if (error.response && error.response.data) { vonageErrorDetails = error.response.data; errorMessage = `Vonage API Error sending SMS to ${recipient}: ${vonageErrorDetails.title || 'Unknown error'} - ${vonageErrorDetails.detail || JSON.stringify(vonageErrorDetails)}`; console.error(errorMessage_ vonageErrorDetails); } else { console.error(errorMessage_ error); // Log the full error object for other types } // Rethrow a structured error or a more generic one for the API layer const apiError = new Error(errorMessage); apiError.vonageDetails = vonageErrorDetails; // Attach details if available apiError.statusCode = error.response?.status || 500; // Preserve status code if possible throw apiError; } } // Export the function if needed elsewhere (e.g._ for testing or separation) // module.exports = { sendSms }; // CommonJS export
- Why
async/await
? Thevonage.messages.send
method returns a Promise. Usingasync/await
makes handling asynchronous operations cleaner. - Private Key Handling: The code now checks for
VONAGE_PRIVATE_KEY_CONTENT
first_ allowing deployment environments to provide the key content directly. It falls back to using the file path specified byVONAGE_APPLICATION_PRIVATE_KEY_PATH
. Note the.replace(/\\n/g_ '\n')
which might be needed if your environment variable mechanism escapes newline characters. - Payload Structure: Standard JSON format with double quotes for keys and string values.
- Error Handling: The
try...catch
block handles errors. It logs detailed info_ especially parsing Vonage's structured error response (error.response.data
). It then rethrows an error_ potentially enriching it with Vonage details and status code_ for the calling layer (API endpoint) to handle.
- Why
3. Building a complete API layer
Let's create the Express server and the API endpoint to trigger the sendSms
function.
-
Initialize Express App: Set up the basic Express application and configure middleware to parse JSON request bodies.
// index.js // ... (requires_ Vonage init_ sendSms function) // --- Express App Setup --- const app = express(); const port = process.env.PORT || 3000; // Use PORT from .env or default to 3000 // Middleware to parse JSON bodies app.use(express.json()); // Middleware to parse URL-encoded bodies (optional but good practice) app.use(express.urlencoded({ extended: true }));
-
Create
/send
Endpoint: Define a POST route at/send
. This endpoint will expect a JSON body withto
andmessage
properties.// index.js // ... (Express app setup) // --- API Endpoint --- app.post('/send'_ async (req_ res) => { // 1. Basic Input Validation const { to, message } = req.body; if (!to || !message) { console.error('Validation Error: Missing "to" or "message" in request body'); // Use return to stop execution after sending response return res.status(400).json({ success: false, message: 'Missing required fields: "to" and "message".' }); } // Basic E.164 format check (starts with '+', followed by digits) // Consider using a library like 'libphonenumber-js' for robust validation if (!/^\+[1-9]\d{1,14}$/.test(to)) { console.error(`Validation Error: Invalid phone number format for "to": ${to}`); return res.status(400).json({ success: false, message: 'Invalid "to" phone number format. Use E.164 format (e.g., +15551234567).' }); } try { // 2. Call the Core SMS Sending Function const result = await sendSms(to, message); console.log(`API call successful for sending SMS to ${to}`); // 3. Send Success Response (Flatter structure as suggested) return res.status(200).json({ success: true, message: 'SMS submitted successfully.', message_uuid: result.message_uuid // Directly include the UUID }); // Alternative nested structure: // return res.status(200).json({ success: true, message: 'SMS submitted successfully.', data: result }); } catch (error) { // 4. Send Error Response console.error(`API Error: Failed to process /send request for ${to}:`, error.message); // Determine appropriate status code - use error's statusCode if available, else 500 const statusCode = error.statusCode || 500; // Return a generic error message to the client, log details server-side return res.status(statusCode).json({ success: false, message: 'Failed to send SMS.' // Generic message // Optionally include a non-sensitive error code or type here // error_code: 'SEND_FAILED' }); // Avoid leaking internal details like error.message directly in production responses: // return res.status(500).json({ success: false, message: 'Failed to send SMS.', error: error.message }); // <-- Leaks details } });
- Request Validation: Basic checks for presence and E.164 format. Production apps should use validation libraries (
express-validator
_joi
_zod
). - Authentication/Authorization: This endpoint is open. Secure it using middleware (see Section 7).
- Response Format: Returns consistent JSON. The success response uses the suggested flatter structure. The error response returns a generic message and logs details server-side_ avoiding leakage of internal
error.message
.
- Request Validation: Basic checks for presence and E.164 format. Production apps should use validation libraries (
-
Start the Server: Make the application listen on the configured port.
// index.js // ... (API endpoint) // --- Start Server --- // Check if the script is being run directly (and not required by a test runner) if (require.main === module) { app.listen(port_ () => { console.log(`Server listening at http://localhost:${port}`); console.log(`Vonage Application ID configured: ${process.env.VONAGE_APPLICATION_ID ? 'Yes' : 'No'}`); console.log(`Attempting to send SMS from: ${process.env.VONAGE_NUMBER}`); }); } // Export the app instance for potential use in integration tests module.exports = { app, sendSms }; // Export app and sendSms
- Testability: The
if (require.main === module)
block prevents the server from auto-starting when the file isrequire
d by test files. We also exportapp
andsendSms
to make them accessible for testing.
- Testability: The
-
Testing with
curl
: Once the server is running (node index.js
), test the endpoint:curl -X POST http://localhost:3000/send \ -H "Content-Type: application/json" \ -d '{ "to": "YOUR_RECIPIENT_NUMBER_E164", "message": "Hello from Vonage and Node.js!" }'
- Expected Success Response (200 OK):
{ "success": true, "message": "SMS submitted successfully.", "message_uuid": "some-unique-message-identifier" }
- Expected Validation Error Response (400 Bad Request):
{ "success": false, "message": "Missing required fields: \"to\" and \"message\"." }
or
{ "success": false, "message": "Invalid \"to\" phone number format. Use E.164 format (e.g., +15551234567)." }
- Expected Server/Vonage Error Response (e.g., 500 Internal Server Error):
(Check server logs for the detailed error like
{ "success": false, "message": "Failed to send SMS." }
Vonage API Error: The requested resource does not exist...
)
- Expected Success Response (200 OK):
4. Integrating with necessary third-party services (Vonage)
This section details how to get the required credentials from Vonage and configure your application.
-
Sign Up/Log In: Go to the Vonage API Dashboard and either sign up for a new account or log in.
-
Get API Key and Secret:
- Navigate to the main Dashboard page after logging in.
- Your API Key and API Secret are displayed near the top.
- Copy these values and paste them into your
.env
file forVONAGE_API_KEY
andVONAGE_API_SECRET
. Remember, these are useful for other Vonage tools/APIs but not for authenticating Messages API calls when using Application ID/Private Key.
-
Create a Vonage Application: The Messages API requires authentication via an Application ID and a private key.
- In the left-hand navigation menu, go to Applications > + Create a new application.
- Enter an Application name (e.g.,
""Node SMS Sender App""
). - Click Generate public and private key. This will automatically download a
private.key
file. Save this file securely in your project's root directory (or note its content). Ensure.gitignore
includesprivate.key
. - Your Application ID will be displayed on this page. Copy it and paste it into your
.env
file forVONAGE_APPLICATION_ID
. - Scroll down to Capabilities.
- Toggle on the Messages capability.
- Provide Inbound URL and Status URL. Even if unused in this guide, they are required. Use placeholders like
https://example.com/webhooks/inbound
andhttps://example.com/webhooks/status
, or use anngrok
URL if testing webhooks. EnsurePOST
is selected for the HTTP Method. - Click Generate application.
-
Purchase/Link a Vonage Number: You need a Vonage virtual number to send SMS from.
- In the left-hand navigation menu, go to Numbers > Buy numbers.
- Search for numbers by country and features (ensure SMS is selected). Choose a number and click Buy.
- Alternatively, go to Numbers > Your numbers.
- Find the number you want to use. Click the Gear icon (Manage) or Link button.
- Select the Vonage Application you just created (
""Node SMS Sender App""
) from the dropdown menu to link the number to the application. This is crucial. Messages sent using Application ID/Private Key authentication must originate from a number linked to that application. - Copy the full number (E.164 format, e.g.,
+15551234567
) and paste it into your.env
file forVONAGE_NUMBER
.
-
Verify
.env
Variables: Double-check that allVONAGE_
variables in your.env
file are correctly filled:PORT
VONAGE_APPLICATION_ID
VONAGE_APPLICATION_PRIVATE_KEY_PATH
(orVONAGE_PRIVATE_KEY_CONTENT
)VONAGE_API_KEY
,VONAGE_API_SECRET
(optional but recommended)VONAGE_NUMBER
TEST_RECIPIENT_NUMBER
(especially for trial accounts)
-
(Trial Accounts Only) Add Test Numbers: If using a free trial Vonage account, you can typically only send SMS to verified numbers.
- Go to the Vonage Dashboard > Sandbox & Test Numbers (or similar section).
- Add the
TEST_RECIPIENT_NUMBER
(your personal phone) to the list. Verify it using the code sent via SMS/call. - Failure results in a
""Non-Whitelisted Destination""
error.
5. Implementing proper error handling, logging, and retry mechanisms
Enhance basic try...catch
with better logging and resilience.
-
Consistent Error Handling Strategy:
- API Layer (
/send
): Catch errors fromsendSms
. Log internal details. Return generic, user-friendly errors with appropriate HTTP status codes (4xx for client issues, 5xx for server/Vonage issues). (Implemented in Section 3). - Core Function (
sendSms
): Catch specific Vonage errors. Log detailed info. Rethrow standardized errors for the API layer. (Improved in Section 2).
- API Layer (
-
Enhanced Logging (Example: Winston): Use a dedicated library for structured, leveled logging.
-
Install:
npm install winston --save
-
Configure (
logger.js
):// logger.js const winston = require('winston'); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', // Control level via env var format: winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), // Log stack traces winston.format.splat(), winston.format.json() // Log in JSON format ), defaultMeta: { service: 'vonage-sms-sender' }, // Add service context transports: [ // Log errors to error.log new winston.transports.File({ filename: 'error.log', level: 'error' }), // Log all levels to combined.log new winston.transports.File({ filename: 'combined.log' }), ], }); // Log to console in non-production environments with colorization if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() // Simple format for console ), })); } module.exports = logger;
-
Use Logger in
index.js
: Replaceconsole.log/error
withlogger.info/warn/error
.// index.js // ... requires ... const logger = require('./logger'); // Assuming logger.js is created // ... inside sendSms function ... async function sendSms(recipient, messageText) { // ... try { logger.info(`Attempting to send SMS from ${fromNumber} to ${toNumber}`, { recipient, from: fromNumber }); const response = await vonage.messages.send({ /* ... payload ... */ }); logger.info(`SMS submitted successfully to ${recipient}. Message UUID: ${response.message_uuid}`, { recipient, message_uuid: response.message_uuid }); return { success: true, message_uuid: response.message_uuid }; } catch (error) { const vonageErrorDetails = error.response?.data; const errorMessage = `Failed sending SMS to ${recipient}: ${error.message}`; logger.error(errorMessage, { recipient, error: error.message, // Main error message vonageDetails: vonageErrorDetails, // Include Vonage specific error if available stack: error.stack // Include stack trace }); // Rethrow error for API layer (as implemented in Section 2) const apiError = new Error(`Failed to send SMS to ${recipient}.`); // Keep rethrown message generic apiError.vonageDetails = vonageErrorDetails; apiError.statusCode = error.response?.status || 500; throw apiError; } } // ... inside /send endpoint ... app.post('/send', async (req, res) => { // ... validation ... if (!to || !message) { logger.warn('Validation Error: Missing fields in request body', { body: req.body }); return res.status(400).json({ /* ... */ }); } if (!/^\+[1-9]\d{1,14}$/.test(to)) { logger.warn(`Validation Error: Invalid phone number format`, { to }); return res.status(400).json({ /* ... */ }); } // ... etc ... try { const result = await sendSms(to, message); logger.info(`API call successful for sending SMS`, { recipient: to, message_uuid: result.message_uuid }); return res.status(200).json({ /* ... */ }); } catch (error) { // Log the detailed error caught from sendSms logger.error(`API Error: Failed to process /send request`, { recipient: to, error: error.message, vonageDetails: error.vonageDetails, // Include details if attached statusCode: error.statusCode, stack: error.stack }); // Return generic response (as implemented in Section 3) return res.status(error.statusCode || 500).json({ success: false, message: 'Failed to send SMS.' }); } }); // ... starting server ... if (require.main === module) { app.listen(port, () => { logger.info(`Server listening at http://localhost:${port}`); // ... other startup logs ... }); }
-
-
Retry Mechanisms (Example:
async-retry
): Handle transient network or API issues.-
Install:
npm install async-retry --save
-
Implement Retry in
sendSms
:// index.js // ... requires ... const retry = require('async-retry'); const logger = require('./logger'); // ... Vonage init ... async function sendSms(recipient, messageText) { const fromNumber = process.env.VONAGE_NUMBER; const toNumber = recipient; try { // Wrap the Vonage call in retry logic const response = await retry(async (bail, attemptNumber) => { logger.info(`Attempt ${attemptNumber} to send SMS to ${recipient}`); try { const result = await vonage.messages.send({ message_type: "text", text: messageText, to: toNumber, from: fromNumber, channel: "sms" }); // Success on this attempt return result; } catch (error) { const statusCode = error.response?.status; const vonageError = error.response?.data; logger.warn(`Attempt ${attemptNumber} failed for ${recipient}: ${error.message}`, { attempt: attemptNumber, recipient, error: error.message, statusCode, vonageError }); // Decide whether to retry or bail (stop retrying) // DO NOT retry non-recoverable errors (4xx client errors like bad number, auth error) if (statusCode && statusCode >= 400 && statusCode < 500) { logger.error(`Non-retryable error (status ${statusCode}) encountered for ${recipient}. Bailing out.`_ { recipient_ statusCode_ vonageError }); // Use bail(error) to stop retrying and make retry() reject with this error bail(error); return; // Exit async function after bailing } // For other errors (network_ 5xx server errors)_ throw to trigger retry throw error; } }_ { retries: 3_ // Try initial call + 3 retries (total 4 attempts) factor: 2_ // Exponential backoff factor (waits 1s_ 2s_ 4s) minTimeout: 1000_ // Minimum wait time: 1 second maxTimeout: 5000_ // Maximum wait time between retries: 5 seconds onRetry: (error_ attemptNumber) => { // This is called before making the next attempt logger.warn(`Retrying SMS send to ${recipient} (attempt ${attemptNumber}) due to error: ${error.message}`, { attempt: attemptNumber, recipient }); } }); // End of retry block // If retry block succeeds (doesn't bail or throw after max retries) logger.info(`SMS submitted successfully to ${recipient} after attempt(s). Message UUID: ${response.message_uuid}`, { recipient, message_uuid: response.message_uuid }); return { success: true, message_uuid: response.message_uuid }; } catch (error) { // This catch block executes if retry() ultimately fails (bailed or max retries exceeded) const finalStatusCode = error.response?.status; const finalVonageError = error.response?.data; logger.error(`Failed to send SMS to ${recipient} after all retries: ${error.message}`, { recipient, finalError: error.message, finalStatusCode, finalVonageError, stack: error.stack }); // Rethrow a final error for the API layer const apiError = new Error(`Failed to send SMS to ${recipient} after multiple attempts.`); apiError.vonageDetails = finalVonageError; apiError.statusCode = finalStatusCode || 500; throw apiError; } } // ... rest of index.js ...
-
Considerations: Carefully tune retry counts, timeouts, and the logic for identifying retryable errors (network errors, HTTP 5xx status codes) vs. non-retryable errors (HTTP 4xx).
-
6. Creating a database schema and data layer (Optional)
While not essential just for sending an SMS via API, storing message logs, status updates (via webhooks), or user data would require a database. This section is intentionally omitted as it significantly expands the scope beyond the core task of sending an SMS. If needed, consider technologies like PostgreSQL, MongoDB, or MySQL, along with an ORM/ODM like Sequelize or Mongoose.