This guide provides a comprehensive walkthrough for building a robust application capable of handling both inbound and outbound SMS messages using Twilio's Programmable Messaging API, the Node.js runtime, and the high-performance Fastify web framework.
We'll cover everything from initial project setup and core messaging logic to essential production considerations like security, error handling, deployment, and testing. By the end, you'll have a solid foundation for integrating two-way SMS communication into your services. While this guide covers core production considerations, true ""production readiness"" depends heavily on the specific application's scale, risk profile, and requirements, potentially needing deeper dives into databases, advanced monitoring, and scaling strategies beyond the scope of this foundational guide.
Project Overview and Goals
What We're Building:
We will create a Node.js application using the Fastify framework that:
- Receives Inbound SMS: Listens for incoming SMS messages sent to a designated Twilio phone number via a webhook.
- Processes and Replies: Parses the incoming message and sends an automated reply back to the sender using Twilio's TwiML.
- Sends Outbound SMS: Exposes a secure API endpoint to programmatically send SMS messages to specified recipients via the Twilio REST API.
Problem Solved:
This application provides the core infrastructure needed for various SMS-based features, such as:
- Automated customer support bots.
- Two-factor authentication (2FA) code delivery.
- Appointment reminders and notifications.
- Marketing alerts (ensure compliance).
- Interactive SMS campaigns.
Technologies Used:
- Node.js: A JavaScript runtime environment ideal for building scalable, event-driven network applications.
- Fastify: A high-performance, low-overhead web framework for Node.js focused on developer experience and speed.
- Twilio Programmable Messaging: A cloud communications platform providing REST APIs and tools for sending and receiving SMS messages globally.
twilio
Node.js Helper Library: Simplifies interaction with the Twilio API.dotenv
/@fastify/env
: Manages environment variables securely and efficiently.ngrok
(for development): A tool to expose local development servers to the internet for webhook testing.
System Architecture:
<!-- The following block describes the system architecture, originally presented as a Mermaid diagram. -->
graph LR
subgraph ""User's Phone""
U(User)
end
subgraph ""Twilio Cloud""
T_API(Twilio API)
T_NUM(Twilio Phone Number)
T_WH(Twilio Webhook)
end
subgraph ""Your Infrastructure""
subgraph ""Development (Local Machine)""
NG(ngrok Tunnel)
APP_DEV(Fastify App - Dev)
end
subgraph ""Production (Cloud/Server)""
APP_PROD(Fastify App - Prod)
LB(Load Balancer/Firewall)
end
end
subgraph ""API Client""
API_C(Your Service/App)
end
%% Inbound Flow
U -- Sends SMS --> T_NUM
T_NUM -- Triggers Webhook --> T_WH
%% Development Inbound
T_WH -- Forwards Request --> NG
NG -- Relays Request --> APP_DEV
%% Production Inbound
T_WH -- Sends Request --> LB
LB -- Forwards Request --> APP_PROD
%% Reply Flow (Common)
subgraph ""Fastify Application (Dev or Prod)""
FASTIFY(Fastify Instance)
IN_ROUTE(Inbound Route /webhooks/sms/twilio)
OUT_ROUTE(Outbound Route /api/send-sms)
TW_CLIENT(Twilio Client)
end
APP_DEV --> IN_ROUTE
APP_PROD --> IN_ROUTE
IN_ROUTE -- Generates TwiML --> T_WH
T_WH -- Sends Reply SMS --> U
%% Outbound Flow
API_C -- POST Request --> OUT_ROUTE
OUT_ROUTE -- Uses Twilio Client --> TW_CLIENT
TW_CLIENT -- Calls API --> T_API
T_API -- Sends SMS --> U
%% Link components within Fastify App
IN_ROUTE -- Processes --> FASTIFY
OUT_ROUTE -- Processes --> FASTIFY
FASTIFY -- Loads Config --> TW_CLIENT
Prerequisites:
- Node.js and npm (or yarn): Installed on your system. (LTS version recommended).
- Twilio Account: A free or paid Twilio account (Sign up for Twilio here).
- Twilio Phone Number: An SMS-capable phone number purchased within your Twilio account.
- Twilio Account SID and Auth Token: Found on your main Twilio Console dashboard.
ngrok
(Required for local webhook testing): Installed and authenticated (ngrok website).- Basic Terminal/Command Line Knowledge.
- Text Editor or IDE: Such as VS Code.
Expected Outcome:
A functional Fastify application running locally (exposed via ngrok
) or deployed, capable of receiving SMS messages, replying automatically, and sending messages via an API endpoint, complete with basic security and error handling.
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 fastify-twilio-sms cd fastify-twilio-sms
-
Initialize Node.js Project: This creates a
package.json
file.npm init -y
-
Install Dependencies: We need Fastify, its environment variable handler, the Twilio helper library,
dotenv
(as a fallback and for clarity),raw-body
for request validation, andpino-pretty
for development logging.npm install fastify @fastify/env twilio dotenv raw-body pino-pretty
fastify
: The core web framework.@fastify/env
: Loads and validates environment variables based on a schema.twilio
: Official Node.js library for the Twilio API.dotenv
: Loads environment variables from a.env
file intoprocess.env
.@fastify/env
can use this too.raw-body
: Needed to capture the raw request body for Twilio signature validation.pino-pretty
: Development dependency to format Fastify's default Pino logs nicely.
-
Set up Environment Variables: Create a file named
.env
in the root of your project. Never commit this file to version control. Populate it with your Twilio credentials and application settings:# .env # Twilio Credentials - Get from https://www.twilio.com/console TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token_xxxxxxxxxxxxxx TWILIO_PHONE_NUMBER=+15551234567 # Your purchased Twilio number # Application Settings PORT=3000 HOST=0.0.0.0 # Listen on all available network interfaces NODE_ENV=development # Set to 'production' when deployed LOG_LEVEL=info # Pino log level (trace, debug, info, warn, error, fatal) # Add this later when using ngrok for webhook validation (Section 7) # Make sure this matches EXACTLY the URL configured in Twilio Console # TWILIO_WEBHOOK_URL=https://<your-ngrok-subdomain>.ngrok.io/webhooks/sms/twilio
- Replace placeholders with your actual Account SID, Auth Token, and Twilio phone number.
HOST=0.0.0.0
is important for running inside containers or VMs.
-
Configure
.gitignore
: Create a.gitignore
file in the project root to prevent sensitive information and unnecessary files from being committed to Git.# .gitignore # Dependencies node_modules/ # Environment Variables .env .env.* !.env.example # Logs logs/ *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Build output dist/ build/ # OS generated files .DS_Store Thumbs.db
-
Create Server File (
server.js
): This file will define how to build the Fastify app and optionally start it.// server.js 'use strict'; const Fastify = require('fastify'); const fastifyEnv = require('@fastify/env'); const twilio = require('twilio'); const getRawBody = require('raw-body'); const { validateRequest } = require('twilio'); // Define schema for environment variables const envSchema = { type: 'object', required: [ 'PORT', 'HOST', 'TWILIO_ACCOUNT_SID', 'TWILIO_AUTH_TOKEN', 'TWILIO_PHONE_NUMBER', ], properties: { PORT: { type: 'string', default: 3000 }, HOST: { type: 'string', default: '0.0.0.0' }, NODE_ENV: { type: 'string', default: 'development' }, // Default is sufficient LOG_LEVEL: { type: 'string', default: 'info' }, TWILIO_ACCOUNT_SID: { type: 'string' }, TWILIO_AUTH_TOKEN: { type: 'string' }, TWILIO_PHONE_NUMBER: { type: 'string' }, TWILIO_WEBHOOK_URL: { type: 'string' } // Required for reliable webhook validation }, }; // Function to build the Fastify app instance async function buildApp (opts = {}) { // Configure Fastify logger based on environment const loggerConfig = process.env.NODE_ENV === 'development' ? { transport: { target: 'pino-pretty', options: { translateTime: 'HH:MM:ss Z', ignore: 'pid,hostname', }, }, level: process.env.LOG_LEVEL || 'info', } : { level: process.env.LOG_LEVEL || 'info' }; // Basic JSON logs in production const fastify = Fastify({ logger: loggerConfig, ...opts // Pass any additional options for testing, etc. }); // Register @fastify/env to load and validate environment variables await fastify.register(fastifyEnv, { confKey: 'config', // Access environment variables via `fastify.config` schema: envSchema, dotenv: true, // Load .env file using dotenv }); // Add hook to capture raw body *before* Fastify parses it for the webhook fastify.addHook('preParsing', async (request, reply, payload) => { if (request.routerPath === '/webhooks/sms/twilio') { const contentType = request.headers['content-type']; if (contentType) { try { // Capture raw body as a string for validation request.rawBodyString = await getRawBody(payload, { length: request.headers['content-length'], limit: '1mb', // Adjust limit as needed encoding: true // Let raw-body detect encoding (should be utf-8 from Twilio) }); } catch (err) { fastify.log.error({ err }, 'Failed to capture raw body'); // Let Fastify handle the error downstream, maybe return error here? // For now, just log it. Validation will likely fail. } } } // IMPORTANT: Return the original payload stream for Fastify to continue parsing return payload; }); // Decorate Fastify instance with Twilio client (initialized onReady) let twilioClient; fastify.decorate('twilioClient', null); fastify.addHook('onReady', async () => { twilioClient = twilio(fastify.config.TWILIO_ACCOUNT_SID, fastify.config.TWILIO_AUTH_TOKEN); fastify.twilioClient = twilioClient; // Make client available via fastify.twilioClient fastify.log.info('Twilio client initialized.'); }); // --- Register Routes --- registerRoutes(fastify); return fastify; } // --- Route Definitions --- function registerRoutes(fastify) { // === Inbound SMS Webhook === fastify.post('/webhooks/sms/twilio', async (request, reply) => { fastify.log.info( { body: request.body, headers: request.headers }, 'Received inbound SMS webhook from Twilio' ); // --- Security Validation (See Section 7) --- const twilioSignature = request.headers['x-twilio-signature']; const webhookUrl = fastify.config.TWILIO_WEBHOOK_URL; // Must be set in config for reliable validation if (!webhookUrl) { fastify.log.error('TWILIO_WEBHOOK_URL environment variable is not set. Cannot validate request.'); return reply.status(500).send('Webhook URL configuration error.'); } if (!twilioSignature) { fastify.log.warn('Request received without X-Twilio-Signature.'); return reply.status(400).send('Missing Twilio Signature'); } if (!request.rawBodyString) { fastify.log.error('Raw request body was not captured. Cannot validate request.'); return reply.status(500).send('Internal server error processing request body.'); } const isValid = validateRequest( fastify.config.TWILIO_AUTH_TOKEN, twilioSignature, webhookUrl, request.rawBodyString // Pass the raw body STRING to the validator ); if (!isValid) { fastify.log.warn({ signature: twilioSignature, url: webhookUrl }, 'Invalid Twilio signature received.'); return reply.status(403).send('Invalid Twilio Signature'); } fastify.log.info('Twilio signature validated successfully.'); // --- End Security Validation --- // Extract details from Twilio's request payload const sender = request.body.From; const recipient = request.body.To; // Your Twilio Number const messageBody = request.body.Body; const messageSid = request.body.MessageSid; fastify.log.info( `Message SID ${messageSid} from ${sender}: ""${messageBody}""` ); // Create a TwiML response using the Twilio helper library const twiml = new twilio.twiml.MessagingResponse(); // Simple auto-reply logic const replyText = `Thanks for messaging! You said: ""${messageBody}""`; twiml.message(replyText); // Send the TwiML response back to Twilio reply.type('text/xml').send(twiml.toString()); // Fastify handles sending the response when reply.send() is called. No explicit return needed. }); // === Outbound SMS API === const sendSmsSchema = { body: { type: 'object', required: ['to', 'body'], properties: { to: { type: 'string', description: 'E.164 formatted phone number', pattern: '^\\+[1-9]\\d{1,14}$' }, // Basic E.164 pattern (fixed end anchor) body: { type: 'string', minLength: 1, maxLength: 1600 }, }, }, response: { // Optional: Define expected response structure 200: { type: 'object', properties: { success: { type: 'boolean' }, messageSid: { type: 'string' }, status: { type: 'string' } } }, // Add error responses if needed (e.g., 4xx, 5xx) } }; // Clarification on maxLength: 1600: While Twilio supports sending messages up to 1600 characters // via automatic concatenation, this means the message will be split into multiple SMS segments. // Each segment (typically 160 GSM-7 characters or 70 UCS-2 characters) is billed individually. // Endpoint to send an outbound SMS fastify.post('/api/send-sms', { schema: sendSmsSchema }, async (request, reply) => { const { to, body } = request.body; fastify.log.info(`Attempting to send SMS to ${to}: ""${body}""`); try { // Use the Twilio client decorated onto the fastify instance const message = await fastify.twilioClient.messages.create({ body: body, from: fastify.config.TWILIO_PHONE_NUMBER, // Your Twilio number to: to, // Recipient's number }); fastify.log.info( `SMS sent successfully! SID: ${message.sid}, Status: ${message.status}` ); return reply.status(200).send({ success: true, messageSid: message.sid, status: message.status, // e.g., 'queued', 'sending', 'sent' }); } catch (error) { fastify.log.error( { err: { message: error.message, code: error.code, status: error.status } }, // Log Twilio error details `Failed to send SMS to ${to}` ); // Customize error response based on Twilio error codes if needed return reply.status(error.status || 500).send({ success: false, message: error.message || 'Failed to send SMS', code: error.code || null, // Twilio specific error code }); } }); // === Health Check Endpoint === fastify.get('/health', async (request, reply) => { // Add checks for DB connection, Twilio client status, etc. if needed return { status: 'ok', timestamp: new Date().toISOString() }; }); } // End registerRoutes // --- Server Start Logic --- // Only run the server start if this script is executed directly if (require.main === module) { buildApp().then(fastify => { fastify.listen({ port: fastify.config.PORT, host: fastify.config.HOST, }, (err, address) => { if (err) { fastify.log.error(err); process.exit(1); } // Logger already logs listening info if configured correctly fastify.log.info( `Twilio configured with SID: ${fastify.config.TWILIO_ACCOUNT_SID.substring(0, 5)}... and Phone: ${fastify.config.TWILIO_PHONE_NUMBER}` ); }); }).catch(err => { console.error('Error building or starting the application:', err); process.exit(1); }); } // Export the build function for testing or programmatic use module.exports = buildApp;
- Refactoring: The code is now wrapped in
buildApp
which returns thefastify
instance. The server only starts if the file is run directly (node server.js
).buildApp
is exported. envSchema
:NODE_ENV
removed fromrequired
.- Webhook Validation: Uses
request.rawBodyString
captured viapreParsing
andgetRawBody
. Removed fallback forwebhookUrl
and added checks for its presence and the signature/raw body. - Twilio Client: Now decorated onto
fastify
instance inonReady
hook for better access. - Health Check: Added
/health
route (from Section 10) here for completeness. - Regex Fix: Corrected regex pattern in
sendSmsSchema
to include$
anchor:^\\+[1-9]\\d{1,14}$
. - Logging Fix: Replaced triple quotes
""""""
with standard double quotes""
in log messages.
- Refactoring: The code is now wrapped in
-
Add Run Script to
package.json
: Modify yourpackage.json
to include convenient scripts for running the server:// package.json (add/update scripts section) ""scripts"": { ""start"": ""node server.js"", ""dev"": ""node server.js"" },
-
Initial Run: You should now be able to start your basic server:
npm run dev
If successful, you'll see log output indicating the server is listening and confirming your Twilio SID/Number were loaded. Press
Ctrl+C
to stop it.
2. Implementing Core Functionality: Handling Inbound SMS
The webhook endpoint /webhooks/sms/twilio
is now defined within server.js
(in the registerRoutes
function). It includes:
- Listening for POST requests.
- Logging incoming data.
- Security Validation: Implemented directly within the route handler (using
validateRequest
with the raw body string). - Parsing sender, recipient, and message body.
- Generating a TwiML response using
twilio.twiml.MessagingResponse
. - Sending the
text/xml
response back to Twilio.
Testing with ngrok
and Twilio Console:
-
Start
ngrok
: Open a new terminal window and run:ngrok http 3000 # Use the PORT defined in your .env
Copy the
https
forwarding URL (e.g.,https://<unique-subdomain>.ngrok.io
). -
Set
TWILIO_WEBHOOK_URL
: Open your.env
file, uncomment theTWILIO_WEBHOOK_URL
line, and paste your fullngrok
https
URL including the path:# .env (update this line) TWILIO_WEBHOOK_URL=https://<unique-subdomain>.ngrok.io/webhooks/sms/twilio
Important: Restart your Fastify server after changing
.env
for the new value to be loaded. -
Start your Fastify server: In your original terminal:
npm run dev
-
Configure Twilio Webhook:
- Go to your Twilio Console.
- Navigate to Phone Numbers > Manage > Active Numbers.
- Click on your Twilio phone number.
- Scroll to ""Messaging"".
- Under ""A MESSAGE COMES IN"", select ""Webhook"".
- Paste your exact
ngrok
https
URL (including/webhooks/sms/twilio
) into the box. - Ensure the method is
HTTP POST
. - Click ""Save"".
-
Send a Test SMS: Send an SMS from your phone to your Twilio number.
-
Check Logs & Reply: Observe your Fastify server logs. You should see the incoming request logged, the ""Twilio signature validated successfully"" message, and details of the message. You should receive the auto-reply on your phone. If validation fails, check the logs for errors (e.g., URL mismatch, missing signature, incorrect Auth Token).
3. Building an API Layer: Sending Outbound SMS
The API endpoint /api/send-sms
is also defined in server.js
(within registerRoutes
). It includes:
- Listening for POST requests.
- Schema Validation: Uses Fastify's schema (
sendSmsSchema
) to validate the request body (to
,body
). Invalid requests get a 400 response automatically. - Using the initialized
fastify.twilioClient
to callmessages.create
. - Sending the SMS via the Twilio API.
- Handling success and errors, returning JSON responses with appropriate status codes and details (including Twilio error codes on failure).
- SMS Length Clarification: A comment near the schema explains the implications of the 1600 character limit (concatenation and billing).
Testing the Outbound API:
Use curl
or Postman (ensure your server is running):
curl -X POST http://localhost:3000/api/send-sms \
-H ""Content-Type: application/json"" \
-d '{
""to"": ""+15559876543"",
""body"": ""Hello from Fastify and Twilio!""
}'
Expected Output (Success):
{
""success"": true,
""messageSid"": ""SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"",
""status"": ""queued""
}
Check the target phone for the SMS and your server logs for details.
4. Integrating with Twilio (Configuration Deep Dive)
- API Keys (Account SID & Auth Token): Used for authenticating requests to Twilio (sending SMS). Stored in
.env
. - Twilio Phone Number: Used as
From
for outbound and target for inbound. Stored in.env
. - Webhook URL Configuration: Tells Twilio where to send inbound SMS events. Configured in Twilio Console. Must match
TWILIO_WEBHOOK_URL
in your.env
for validation to work. Usengrok
URL for development, public deployed URL for production. Method:HTTP POST
.
5. Error Handling, Logging, and Retries
- Error Handling:
- Fastify handles schema validation errors (400).
try...catch
blocks handle Twilio API errors (outbound). Logerror.code
,error.status
,error.message
.- Webhook security validation returns 403/500 on failure.
- Graceful handling of TwiML generation/sending errors (ensure valid XML or error response).
- Consider a Fastify global error handler (
fastify.setErrorHandler
) for unhandled exceptions.
- Logging:
pino-pretty
in development, JSON in production.- Controlled by
LOG_LEVEL
. - Log relevant context (SIDs, numbers, errors).
- Retries:
- Twilio retries failed webhooks. Ensure your endpoint is idempotent if actions aren't safe to repeat.
- Implement custom retries for outbound API calls only if necessary (e.g., for critical messages and transient errors), potentially using a queue.
6. Database Schema and Data Layer (Considerations)
While this basic guide doesn't implement a database, real-world applications often need to store message history, user data, or conversation state.
- When Needed: Storing logs, tracking conversations, linking to users.
- Schema Ideas (Example using Prisma):
// schema.prisma datasource db { provider = ""postgresql"" url = env(""DATABASE_URL"") } generator client { provider = ""prisma-client-js"" } model MessageLog { id String @id @default(cuid()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt direction String // ""inbound"" or ""outbound"" twilioSid String @unique // Twilio's Message SID accountSid String fromNumber String toNumber String body String? @db.Text status String? // Twilio status errorCode Int? // Twilio error code errorMessage String? // userId String? // user User? @relation(fields: [userId], references: [id]) }
- Implementation: Choose DB, use ORM (Prisma, Sequelize), install drivers, define schema, run migrations, integrate client into Fastify (e.g., via
fastify-plugin
).
7. Adding Security Features
Security is critical.
-
Twilio Request Validation (Implemented in Section 1/2):
- Uses
raw-body
viapreParsing
hook to capture the raw request body string. - Requires
TWILIO_WEBHOOK_URL
to be correctly set in.env
and loaded via@fastify/env
. Reminder: Ensure this variable is uncommented and set to your correctngrok
(dev) or public (prod) URL. - Uses
twilio.validateRequest
with Auth Token, signature header, exact URL, and the raw body string. - Returns 403 Forbidden if validation fails. Returns 500 if configuration (URL) or body capture fails.
- Uses
-
API Key / Token Authentication (for
/api/send-sms
):- Recommendation: Protect the outbound API.
- Options: Simple static API key (check in
preHandler
), JWT (@fastify/jwt
), OAuth 2.0 (fastify-oauth2
). Implementation not shown in this guide.
-
Rate Limiting:
- Recommendation: Prevent abuse of the outbound API.
- Implementation: Use
@fastify/rate-limit
.npm install @fastify/rate-limit
// server.js (register the plugin within buildApp) await fastify.register(require('@fastify/rate-limit'), { max: 100, // Max requests per time window per IP timeWindow: '1 minute' });
-
Input Validation:
- Handled for
/api/send-sms
via Fastify schema. Ensure schemas are strict.
- Handled for
-
Helmet (Security Headers):
- Recommendation: Add standard security headers.
- Implementation: Use
@fastify/helmet
.npm install @fastify/helmet
// server.js (register the plugin within buildApp) await fastify.register(require('@fastify/helmet'));
8. Handling Special Cases
- Message Concatenation: Longer messages (>160 GSM-7 / 70 UCS-2 chars) are split by Twilio but billed as multiple segments. Inbound arrive as one payload.
- Character Encoding: Use UTF-8. Twilio handles GSM-7/UCS-2 detection.
- International Numbers: Use E.164 format (
+1...
,+44...
). - Opt-Outs (STOP/HELP): Twilio handles standard English keywords by default on long codes/Toll-Free. Respect opt-outs.
- Non-SMS Channels: Payloads/responses differ for WhatsApp, etc.
9. Implementing Performance Optimizations
- Asynchronous Operations: Use
async/await
for all I/O (Twilio calls, DB). - Payload Size: Keep TwiML/API responses concise.
- Caching: Consider Redis (
@fastify/redis
) for frequent lookups if needed. - Load Testing: Use
k6
,autocannon
. - Node.js Version: Use recent LTS.
- Profiling: Use Node.js profiler or
0x
.
10. Adding Monitoring, Observability, and Analytics
- Health Checks:
/health
endpoint implemented (seeserver.js
). - Twilio Debugger & Logs: Check Twilio Console Debugger and Message Logs.
- Application Logging: Aggregate production JSON logs (Datadog, ELK). Set up alerts.
- Metrics: Track message counts, latency, errors using
prom-client
/fastify-metrics
(for Prometheus) or push to Datadog. - Error Tracking: Use Sentry (
@sentry/node
) or Bugsnag.
11. Troubleshooting and Caveats
- Webhook Failures (Twilio Debugger):
11200
(unreachable/timeout),12100
/12200
(bad TwiML),12300
(bad Content-Type),11205
(SSL issue). ngrok
Issues: Tunnel expired, HTTP vs HTTPS mismatch, URL typo.- Request Validation Failure (
403 Invalid Twilio Signature
): Incorrect Auth Token, incorrectTWILIO_WEBHOOK_URL
(must match exactly), body modified before validation (check raw body capture), validating non-Twilio request. - Outbound Sending Failures (Check
error.code
):21211
(bad 'To'),21610
(STOP),21614
(not SMS capable),3xxxx
(carrier/delivery issues),20003
(low balance). Full List. - Environment Variables: Ensure
.env
loaded or variables set in production. - Firewalls: Allow traffic from Twilio IPs if restricting.
12. Deployment and CI/CD
- Platform: PaaS (Heroku, Render), IaaS (EC2, GCE), Containers (Docker, Kubernetes).
- Production Config:
NODE_ENV=production
, secure environment variables (use platform's secrets management),HOST=0.0.0.0
, update Twilio webhook URL. - Build Step: If needed (
tsc
, Babel). - Process Manager:
pm2
recommended (pm2 start server.js -i max
). - CI/CD: GitHub Actions, GitLab CI, etc. (Install -> Lint -> Test -> Build -> Deploy).
- Rollback: Have a plan.
13. Verification and Testing
Ensure correctness.
- Manual Verification Checklist:
-
- Server starts dev/prod.
-
-
ngrok
active,TWILIO_WEBHOOK_URL
set, Twilio Console URL matches.
-
-
- Inbound SMS -> Check logs (valid request, signature OK), check reply received.
-
- Outbound API (
/api/send-sms
) -> Check success response, check SMS received.
- Outbound API (
-
- Outbound API (invalid body) -> Check 400 error.
-
- Outbound API (invalid number) -> Check Twilio error logged/returned.
-
- Check Twilio Debugger.
-
- Check
/health
endpoint.
- Check
-
- Automated Testing:
- Unit Tests: Test functions in isolation. Mock
twilio
client (jest.fn()
,sinon
). - Integration Tests: Test route handlers interacting with services (mock externals like Twilio). Use
fastify.inject()
. Example (Conceptual Jest with refactoredserver.js
):// tests/outbound.test.js const buildApp = require('../server'); // Import the app builder const twilio = require('twilio'); // Mock the Twilio client constructor and methods const mockMessagesCreate = jest.fn(); jest.mock('twilio', () => { // Mock the constructor to return an object with a messages mock return jest.fn().mockImplementation(() => ({ messages: { create: mockMessagesCreate } })); }); let app; // Build a new app instance for tests beforeAll(async () => { // Optionally override config for tests if needed // process.env.SOME_VAR = 'test_value'; app = await buildApp(); await app.ready(); // Ensure plugins, hooks (like onReady) are loaded }); afterAll(async () => { await app.close(); }); beforeEach(() => { // Reset mocks before each test mockMessagesCreate.mockClear(); // Reset Twilio constructor mock if needed twilio.mockClear(); }); test('POST /api/send-sms should send SMS successfully', async () => { const mockResponse = { sid: 'SMtest', status: 'queued' }; mockMessagesCreate.mockResolvedValue(mockResponse); const response = await app.inject({ method: 'POST', url: '/api/send-sms', payload: { to: '+15551112222', body: 'Test message' } }); expect(response.statusCode).toBe(200); expect(JSON.parse(response.payload)).toEqual({ success: true, messageSid: mockResponse.sid, status: mockResponse.status }); expect(twilio).toHaveBeenCalledTimes(1); // Check constructor called expect(mockMessagesCreate).toHaveBeenCalledTimes(1); expect(mockMessagesCreate).toHaveBeenCalledWith({ to: '+15551112222', body: 'Test message', from: app.config.TWILIO_PHONE_NUMBER // Check correct 'from' number used }); }); test('POST /api/send-sms should handle Twilio API errors', async () => { const mockError = new Error('Twilio Test Error'); mockError.code = 21211; // Example error code mockError.status = 400; mockMessagesCreate.mockRejectedValue(mockError); const response = await app.inject({ method: 'POST', url: '/api/send-sms', payload: { to: '+15551112222', // Invalid number for this test case body: 'Test message causing error' } }); expect(response.statusCode).toBe(400); expect(JSON.parse(response.payload)).toEqual({ success: false, message: 'Twilio Test Error', code: 21211 }); expect(mockMessagesCreate).toHaveBeenCalledTimes(1); }); test('POST /api/send-sms should return 400 for invalid payload', async () => { const response = await app.inject({ method: 'POST', url: '/api/send-sms', payload: { // Missing 'to' field body: 'Test message' } }); expect(response.statusCode).toBe(400); // Check for Fastify's validation error structure const payload = JSON.parse(response.payload); expect(payload.message).toContain(""body must have required property 'to'""); expect(mockMessagesCreate).not.toHaveBeenCalled(); }); // Add tests for the inbound webhook (/webhooks/sms/twilio) // - Mock validateRequest // - Inject POST request with valid/invalid signature // - Check TwiML response
- Unit Tests: Test functions in isolation. Mock