Sending SMS with Twilio in RedwoodJS: A Developer Guide
This guide provides a step-by-step walkthrough for integrating Twilio's Programmable Messaging API into a RedwoodJS application to send basic SMS messages using a Node.js backend function. We'll cover everything from project setup and configuration to implementation, error handling, testing, and deployment considerations.
By the end of this guide, you will have a secure RedwoodJS function capable of accepting a phone number and message body, and using Twilio to send an SMS message to that number. This functionality is crucial for applications needing to send notifications, alerts, verification codes, or other transactional messages programmatically.
Project Overview and Goals
Goal: To build a RedwoodJS API function that sends an SMS message via the Twilio API.
Problem Solved: Enables programmatic SMS communication directly from your RedwoodJS backend, facilitating features like user notifications, appointment reminders, or two-factor authentication prompts.
Technologies:
- RedwoodJS: A full-stack JavaScript/TypeScript framework for building modern web applications. We leverage its API-side functions for backend logic.
- Node.js: The runtime environment for RedwoodJS's backend.
- Twilio Programmable Messaging: The third-party service used to send SMS messages via its REST API.
- Yarn: The package manager used by RedwoodJS.
System Architecture:
+-----------------+ +-------------------------+ +-------------+
| RedwoodJS | ----> | RedwoodJS API Function | ----> | Twilio API | ----> SMS
| (Frontend Call) | | (api/src/functions/...) | | |
+-----------------+ +-------------------------+ +-------------+
(Optional) (Node.js + Twilio SDK)
- (Optional) A frontend component or external service triggers the RedwoodJS API function.
- The RedwoodJS API function (
sendSms
in this guide) receives the request (containing recipient number and message body). - The function uses the Twilio Node.js SDK, configured with your credentials, to make an API call to Twilio.
- Twilio processes the request and delivers the SMS to the recipient's phone.
Prerequisites:
- Node.js: Version 18.x or later installed. (Download Node.js)
- Yarn: Version 1.x (Classic) installed (
npm install --global yarn
). - Twilio Account: A free or paid Twilio account. (Sign up for Twilio)
- Twilio Phone Number: An SMS-enabled phone number purchased within your Twilio account.
- Verified Phone Number (for Trial Accounts): If using a Twilio trial account, you must verify the recipient phone number(s) in your Twilio Console.
Final Outcome: A RedwoodJS project with a functional /api/sendSms
endpoint that securely sends SMS messages using Twilio.
1. Setting up the RedwoodJS Project
Let's start by creating a new RedwoodJS project and installing the necessary dependencies.
-
Create a New RedwoodJS App: Open your terminal and run:
yarn create redwood-app ./redwood-twilio-sms
Follow the prompts (you can choose JavaScript or TypeScript - this guide uses JavaScript examples, but the concepts are identical).
-
Navigate to Project Directory:
cd redwood-twilio-sms
-
Install Twilio Node.js Helper Library: RedwoodJS uses Yarn workspaces. We need to install the Twilio library specifically in the
api
workspace.yarn workspace api add twilio
This command adds the
twilio
package to theapi/package.json
file and installs it. -
Configure Environment Variables: Sensitive credentials like API keys should never be hardcoded. RedwoodJS uses
.env
files for environment variables.- Create a
.env
file in the root of your project (redwood-twilio-sms/.env
). - Add the following lines to your
.env
file:
# .env TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token_xxxxxxxxxxxxxx TWILIO_PHONE_NUMBER=+15551234567
-
Obtaining Credentials:
TWILIO_ACCOUNT_SID
andTWILIO_AUTH_TOKEN
: Find these on your main Twilio Console dashboard (https://www.twilio.com/console).TWILIO_PHONE_NUMBER
: This is the SMS-enabled Twilio phone number you purchased. Find it under ""Phone Numbers"" > ""Manage"" > ""Active numbers"" in your Twilio Console. Ensure it's in E.164 format (e.g.,+15551234567
).
-
Security: Ensure your
.env
file is listed in your.gitignore
file (RedwoodJS includes this by default) to prevent accidentally committing secrets.
- Create a
2. Implementing Core Functionality: The SMS Sending Function
We'll create a RedwoodJS API function to handle the SMS sending logic.
-
Generate the API Function: Use the RedwoodJS CLI to scaffold a new function:
yarn rw g function sendSms
This creates the file
api/src/functions/sendSms.js
(or.ts
) with some boilerplate code. -
Implement the Sending Logic: Replace the contents of
api/src/functions/sendSms.js
with the following code:// api/src/functions/sendSms.js import { logger } from 'src/lib/logger' import twilio from 'twilio' // Import the Twilio library // Initialize Twilio Client // Ensure TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN are set in your .env file const accountSid = process.env.TWILIO_ACCOUNT_SID const authToken = process.env.TWILIO_AUTH_TOKEN const twilioPhoneNumber = process.env.TWILIO_PHONE_NUMBER // Validate environment variables if (!accountSid || !authToken || !twilioPhoneNumber) { logger.error( 'Twilio environment variables not set. Ensure TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER are in your .env file.' ) // In a real app, you might want to prevent the function from running // or throw a more specific error here during startup if possible. } // Only initialize client if credentials are available const client = accountSid && authToken ? twilio(accountSid, authToken) : null /** * Sends an SMS message using Twilio. * Expects a POST request with a JSON body containing: * { * ""to"": ""+15559876543"", // Recipient phone number in E.164 format * ""body"": ""Your message here"" * } */ export const handler = async (event, context) => { // 1. Check HTTP Method (optional but good practice) if (event.httpMethod !== 'POST') { logger.warn('Received non-POST request to sendSms function') return { statusCode: 405, // Method Not Allowed headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'Method Not Allowed' }), } } // 2. Check if Twilio client is initialized if (!client) { logger.error('Twilio client failed to initialize. Check credentials.') return { statusCode: 500, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'SMS service configuration error.' }), } } // 3. Parse Request Body and Validate Input let requestBody try { requestBody = JSON.parse(event.body) } catch (error) { logger.error('Failed to parse request body:', error) return { statusCode: 400, // Bad Request headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'Invalid JSON body' }), } } const { to, body } = requestBody // Basic validation if (!to || typeof to !== 'string' || !to.startsWith('+')) { logger.warn('Invalid ""to"" phone number provided:', to) return { statusCode: 400, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'Invalid recipient phone number. Use E.164 format (e.g., +15551234567).', }), } } if (!body || typeof body !== 'string' || body.trim().length === 0) { logger.warn('Invalid or empty ""body"" provided') return { statusCode: 400, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ error: 'Message body cannot be empty.' }), } } // 4. Send the SMS using Twilio try { logger.info(`Attempting to send SMS to ${to}`) const message = await client.messages.create({ body: body, from: twilioPhoneNumber, // Your Twilio number from .env to: to, // Recipient number from request body }) logger.info(`SMS sent successfully. Message SID: ${message.sid}`) return { statusCode: 200, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ success: true, messageSid: message.sid, status: message.status, // e.g., 'queued', 'sending', 'sent' }), } } catch (error) { // Log Twilio-specific errors or generic errors logger.error('Failed to send SMS via Twilio:', error) // Determine appropriate status code based on error if possible const statusCode = error.status || 500 // Use Twilio's status or default to 500 return { statusCode: statusCode, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ success: false, error: 'Failed to send SMS.', // Optionally include more detail in non-production environments // details: process.env.NODE_ENV !== 'production' ? error.message : undefined, twilioErrorCode: error.code, // Include Twilio's error code if available twilioMoreInfo: error.moreInfo, // Include link to Twilio docs if available }), } } }
Code Explanation:
- Imports: We import Redwood's
logger
for logging and thetwilio
library. - Twilio Client Initialization: We retrieve credentials from
process.env
and initialize the Twilio client. Crucially, we check if the environment variables exist before initializing. - Handler Function: This is the main entry point for the API function (
handler
). - HTTP Method Check: Ensures the function only responds to
POST
requests. - Client Initialization Check: Verifies the Twilio client was successfully created.
- Request Parsing & Validation: Parses the JSON body from the incoming
event
and validates the presence and basic format of theto
phone number (must start with+
) and thebody
. - SMS Sending: Uses
client.messages.create
with thebody
,from
(your Twilio number), andto
parameters. This is an asynchronous operation, so we useawait
. - Success Response: If the API call succeeds, it returns a
200 OK
status with the message SID and status. - Error Handling: A
try...catch
block handles potential errors during the API call (e.g., invalid credentials, network issues, Twilio errors). It logs the error and returns an appropriate error status code (using Twilio's status if available, otherwise 500) and a JSON error message, potentially including Twilio-specific error codes (error.code
) and documentation links (error.moreInfo
) for easier debugging.
- Imports: We import Redwood's
3. Building the API Layer
The RedwoodJS function is our API layer for this specific task.
-
Endpoint:
/api/sendSms
-
Method:
POST
-
Request Body (JSON):
{ ""to"": ""+1recipient_phone_number"", ""body"": ""Your message content here."" }
-
Success Response (JSON - 200 OK):
{ ""success"": true, ""messageSid"": ""SMxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"", ""status"": ""queued"" }
-
Error Response (JSON - 4xx/5xx):
{ ""success"": false, ""error"": ""Specific error message."", ""twilioErrorCode"": 60001, ""twilioMoreInfo"": ""https://www.twilio.com/docs/errors/60001"" }
-
Testing with
curl
: Make sure your RedwoodJS development server is running (yarn rw dev
). Open a new terminal window and run:curl -X POST http://localhost:8910/api/sendSms \ -H ""Content-Type: application/json"" \ -d '{ ""to"": ""+1YOUR_VERIFIED_PHONE_NUMBER"", ""body"": ""Hello from RedwoodJS and Twilio!"" }'
- Replace
+1YOUR_VERIFIED_PHONE_NUMBER
with your actual mobile number (which must be verified in Twilio if using a trial account). - You should see a success JSON response, and shortly after, receive the SMS on your phone. Check the terminal running
yarn rw dev
for logs.
- Replace
4. Integrating with Twilio
This was covered in steps 1 (installation, environment variables) and 2 (client initialization, API call).
Key Configuration Points:
TWILIO_ACCOUNT_SID
: Your unique account identifier. Found on the Twilio Console dashboard. Purpose: Identifies your account to Twilio. Format:ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
.TWILIO_AUTH_TOKEN
: Your secret API key. Found on the Twilio Console dashboard. Treat this like a password. Purpose: Authenticates your API requests. Format: Alphanumeric string.TWILIO_PHONE_NUMBER
: The Twilio number messages will be sent from. Found in the Phone Numbers section of the Twilio Console. Purpose: Sets thefrom
number for outgoing SMS. Format: E.164 (+1...
).
Dashboard Navigation:
- Log in to https://www.twilio.com/console.
- Account SID & Auth Token: Visible directly on the main dashboard (""Account Info"" section).
- Phone Number: Navigate to ""Develop"" (left sidebar) -> ""Phone Numbers"" -> ""Manage"" -> ""Active numbers"". Copy the desired number in E.164 format.
- (Trial Only) Verified Numbers: Navigate to ""Develop"" (left sidebar) -> ""Phone Numbers"" -> ""Manage"" -> ""Verified caller IDs"". Add any phone numbers you want to send messages to during your trial.
5. Error Handling, Logging, and Retry Mechanisms
- Error Handling: Implemented within the
try...catch
block inapi/src/functions/sendSms.js
. It catches errors from theclient.messages.create
call, logs them using Redwood's logger, and returns structured JSON error responses with appropriate HTTP status codes. It includes Twilio-specific error details when available (error.code
,error.moreInfo
). - Logging: RedwoodJS's built-in
logger
(api/src/lib/logger.js
) is used.logger.info
: Logs successful operations and key steps.logger.error
: Logs exceptions and failure conditions.logger.warn
: Logs non-critical issues like invalid input.- Logs appear in the console where
yarn rw dev
is running. In production deployments, logs are typically routed to your hosting provider's logging service (e.g., Vercel Logs, Netlify Functions Logs).
- Retry Mechanisms:
- This basic implementation does not include automatic retries. Network issues or temporary Twilio outages might cause failures.
- For Production: Implement retries with exponential backoff for transient errors (like network timeouts or 5xx errors from Twilio). Consider using a background job queue (e.g., using packages like
bullmq
and Redis) to handle sending and retries more robustly, preventing HTTP request timeouts for long-running processes. This is beyond the scope of this basic guide.
Testing Error Scenarios:
- Invalid Credentials: Temporarily change
TWILIO_AUTH_TOKEN
in.env
to an incorrect value, restartyarn rw dev
, and send acurl
request. Expect a 401 or similar error. - Invalid
to
Number: Send acurl
request with an improperly formattedto
number (e.g.,5551234
,+1
). Expect a 400 error from our validation. Send a validly formatted but non-existent number – expect an error from Twilio (potentially logged, response might vary). - Empty Body: Send a
curl
request with""body"": """"
. Expect a 400 error from our validation. - Trial Account Unverified Number: If using a trial account, send to a number not listed in your Verified Caller IDs. Expect a Twilio error (likely code 21608).
6. Database Schema and Data Layer
Not Applicable: This specific function does not require database interaction. It receives data via the API request and interacts directly with the external Twilio service. If you needed to store message history, associate messages with users, or manage opt-outs, you would define Prisma schemas in api/db/schema.prisma
, generate migrations (yarn rw prisma migrate dev
), and use Redwood's services (yarn rw g service ...
) to interact with the database before or after calling the Twilio API.
7. Security Features
- Input Validation: The
api/src/functions/sendSms.js
function includes basic validation for theto
andbody
parameters to prevent malformed requests or trivial injection attempts (though Twilio's API handles its own sanitization). More robust validation (e.g., usingyup
orzod
) is recommended for complex inputs. - Secret Management: API credentials (
TWILIO_ACCOUNT_SID
,TWILIO_AUTH_TOKEN
) are stored securely in environment variables (.env
) and are not checked into version control (.gitignore
). In production, these are configured via the hosting provider's environment variable settings. - Authentication/Authorization:
- Crucially, by default, RedwoodJS API functions are publicly accessible. Anyone who knows the URL (
/api/sendSms
) can potentially call it. - For Production: You MUST secure this endpoint. Use RedwoodJS's built-in authentication (
yarn rw setup auth ...
) and add the@requireAuth
directive or checkisAuthenticated
/currentUser
within thehandler
function to ensure only authorized users or services can trigger SMS sending. It is critical to understand that the function as presented without authentication is insecure and unsuitable for production environments. See RedwoodJS Auth Docs.
- Crucially, by default, RedwoodJS API functions are publicly accessible. Anyone who knows the URL (
- Rate Limiting:
- Twilio: Twilio enforces its own rate limits based on your account, number type, and destination country. Exceeding these limits results in
429 Too Many Requests
errors (Twilio error code 20429). - Application-Level: For high-traffic applications or to prevent abuse, implement rate limiting on the function itself (e.g., using an in-memory store for simple cases or Redis/database for distributed environments). This is not included in this basic guide.
- Twilio: Twilio enforces its own rate limits based on your account, number type, and destination country. Exceeding these limits results in
- Common Vulnerabilities: Basic input validation helps, but securing the endpoint via authentication is the primary defense against unauthorized access and potential abuse (e.g., exhausting your Twilio balance).
8. Handling Special Cases
- E.164 Phone Number Formatting: The primary special case is ensuring the
to
number is always in E.164 format (+
followed by country code and number, no spaces or dashes). The validation enforces the leading+
, but more comprehensive E.164 validation libraries exist if needed. - Internationalization (i18n): The message
body
is sent as-is. If you need to send messages in different languages, your application logic (potentially in a RedwoodJS service calling this function) would need to determine the correct language and provide the translatedbody
text. - Character Limits & Encoding: Standard SMS messages have character limits (160 GSM-7 characters, fewer for UCS-2 encoding used for non-Latin characters). Longer messages are automatically segmented by Twilio (and billed as multiple messages). Be mindful of message length in the
body
you provide. Twilio handles the encoding details. - MMS: To send MMS (images, etc.), you would add a
mediaUrl
parameter to theclient.messages.create
call, pointing to a publicly accessible URL of the media file. MMS is typically only supported in the US and Canada.
9. Performance Optimizations
- Twilio Client Initialization: The Twilio client is initialized once when the function cold starts and reused for subsequent invocations within the same container instance. This is efficient.
- Asynchronous Operations: The
client.messages.create
call is asynchronous (await
), preventing the Node.js event loop from being blocked during the API request. - High Volume:
- Rate Limits: As mentioned, Twilio imposes rate limits. Distribute sending across multiple Twilio numbers or use a Messaging Service if needed.
- Queuing: For sending bulk messages or ensuring resilience, use a background job queue (like BullMQ with Redis). The API function would add a job to the queue, and a separate worker process would handle sending and retries. This prevents API gateway timeouts (typically 10-30 seconds) if Twilio takes longer to respond or if retries are needed.
- Twilio Messaging Services: For scaling, opt-in/opt-out management, and intelligent routing, consider using a Twilio Messaging Service instead of a single
from
number. You pass themessagingServiceSid
instead offrom
toclient.messages.create
.
10. Monitoring, Observability, and Analytics
- RedwoodJS Logging: Logs provide basic observability into function executions and errors. Ensure logs are captured by your hosting provider (Vercel, Netlify, Render, etc.).
- Twilio Console Logs: The Twilio Console provides detailed logs for every message attempt:
- Navigate to ""Monitor"" (left sidebar) -> ""Logs"" -> ""Messaging"".
- Filter by date, number, status, etc.
- See delivery status (
queued
,sent
,delivered
,undelivered
,failed
), error codes, message segments, and cost. This is invaluable for debugging delivery issues.
- Error Tracking Services: Integrate services like Sentry or BugSnag with your RedwoodJS API side to capture, aggregate, and alert on errors proactively. RedwoodJS has recipes for integrating some of these.
- Health Checks: While this specific function doesn't maintain state, you could create a simple health check function (
/api/health
) that returns200 OK
to ensure the API side is generally responsive. - Metrics/Dashboards: For business analytics (e.g., number of notifications sent), you would typically log events to a dedicated analytics platform or database from your application logic before calling the
sendSms
function.
11. Troubleshooting and Caveats
- Check Redwood API Server Logs First: Before diving into Twilio logs, always check the console output where
yarn rw dev
is running. This often shows immediate errors from your function code (like parsing errors, invalid input, or exceptions before the Twilio call is even made). - Invalid Credentials (Twilio Error 20003): Double-check
TWILIO_ACCOUNT_SID
andTWILIO_AUTH_TOKEN
in your.env
file and ensure they match the Twilio Console. Restartyarn rw dev
after changes. - Trial Account Restrictions (Twilio Error 21608 - Unverified Number): Trial accounts can only send SMS to phone numbers verified in the Twilio Console (""Verified Caller IDs""). Upgrade your account or verify the recipient number.
- Trial Account Restrictions (SMS Prefix): Messages sent from trial accounts will be prefixed with ""Sent from a Twilio trial account - "". Upgrade to remove this.
- Invalid
From
Number (Twilio Error 21212): EnsureTWILIO_PHONE_NUMBER
in.env
is a valid Twilio number associated with your account and enabled for SMS. - Invalid
To
Number (Twilio Error 21211): Ensure theto
number provided in the request body is a valid phone number in E.164 format. Check Twilio logs for details if the format is correct but delivery fails. - Rate Limits Exceeded (Twilio Error 20429 / HTTP 429): Slow down your sending rate or use a Twilio Messaging Service for better throughput management. Implement application-side queuing/throttling.
- Function Timeout: If the Twilio API is slow to respond, your serverless function might time out (typically 10-30 seconds depending on the provider). Use background queues for long-running/high-volume tasks.
.env
Not Loaded: Ensure the.env
file is in the project root andyarn rw dev
was restarted after creating/modifying it. Check that the environment variables are correctly named.- Publicly Accessible Endpoint: Re-iterate that without adding authentication (
@requireAuth
), this endpoint is open. Secure it before deploying to production. - JSON Parsing Errors: Ensure the
curl
command or frontend request sends a valid JSON body with the correctContent-Type: application/json
header.
12. Deployment and CI/CD
- Deployment Targets: RedwoodJS supports various deployment targets like Vercel, Netlify, Render, AWS Serverless, etc. Follow the specific deployment guide for your chosen provider in the RedwoodJS Deployment Docs.
- Environment Variables: This is critical. You must configure the
TWILIO_ACCOUNT_SID
,TWILIO_AUTH_TOKEN
, andTWILIO_PHONE_NUMBER
environment variables within your hosting provider's settings dashboard (e.g., Vercel Environment Variables, Netlify Build environment variables). Do not commit your.env
file. - CI/CD: Set up a CI/CD pipeline (e.g., using GitHub Actions, Vercel/Netlify Git integration) to automatically build, test, and deploy your application upon code changes. Your pipeline should install dependencies (
yarn install
), potentially run tests (yarn rw test
), build the app (yarn rw build
), and deploy (vercel deploy --prod
,netlify deploy --prod
, etc.). - Rollback: Most hosting providers offer mechanisms to roll back to previous deployments if a new deployment introduces issues. Familiarize yourself with your provider's rollback procedure.
13. Verification and Testing
-
Manual Verification:
- Run
yarn rw dev
. - Use
curl
(as shown in Section 3) or a tool like Postman/Insomnia to send aPOST
request tohttp://localhost:8910/api/sendSms
with a valid JSON body. - Check the
curl
/tool response for a200 OK
status and JSON body with""success"": true
. - Check the recipient phone for the incoming SMS message.
- Check the
yarn rw dev
console output for logs. - Check the Twilio Console logs (""Monitor"" -> ""Logs"" -> ""Messaging"") for the message record and its status.
- Run
-
Automated Testing (Jest): RedwoodJS uses Jest for testing. Let's write a basic test for our function.
- Create/Edit the test file
api/src/functions/sendSms.test.js
:
// api/src/functions/sendSms.test.js import { mockHttpEvent } from '@redwoodjs/testing/api' import { handler } from './sendSms' // Import the handler import twilio from 'twilio' // Import the original library // Mock the Twilio library jest.mock('twilio', () => { // Mock the messages.create function const create = jest.fn().mockResolvedValue({ sid: 'SMmockedsid1234567890abcdef', status: 'queued', }) // Mock the client constructor return jest.fn().mockImplementation(() => ({ messages: { create, }, })) }) // Mock the logger to prevent console noise during tests jest.mock('src/lib/logger', () => ({ logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), }, })) describe('sendSms function', () => { // Define mock environment variables before tests const OLD_ENV = process.env beforeAll(() => { process.env = { ...OLD_ENV, TWILIO_ACCOUNT_SID: 'ACtestmockaccount', TWILIO_AUTH_TOKEN: 'testmocktoken', TWILIO_PHONE_NUMBER: '+15550001111', } // Note: These mocked environment variables apply *only* during the execution // of this test suite (`yarn rw test api`). You still need the actual `.env` // file for local development (`yarn rw dev`) and correctly configured // environment variables in your production deployment. }) afterAll(() => { process.env = OLD_ENV // Restore old environment }) beforeEach(() => { // Reset mocks before each test twilio().messages.create.mockClear() twilio.mockClear() // Clear the constructor mock if needed }) it('should return 405 for non-POST requests', async () => { const response = await handler( mockHttpEvent({ httpMethod: 'GET' }), null ) expect(response.statusCode).toBe(405) expect(JSON.parse(response.body).error).toBe('Method Not Allowed') }) it('should return 400 if ""to"" number is missing or invalid', async () => { const event = mockHttpEvent({ httpMethod: 'POST', body: JSON.stringify({ body: 'Test message' }), // Missing 'to' }) const response = await handler(event, null) expect(response.statusCode).toBe(400) expect(JSON.parse(response.body).error).toContain( 'Invalid recipient phone number' ) }) it('should return 400 if ""body"" is missing or empty', async () => { const event = mockHttpEvent({ httpMethod: 'POST', body: JSON.stringify({ to: '+15551234567', body: ' ' }), // Empty 'body' }) const response = await handler(event, null) expect(response.statusCode).toBe(400) expect(JSON.parse(response.body).error).toBe( 'Message body cannot be empty.' ) }) it('should call Twilio client and return 200 on success', async () => { const recipient = '+15559876543' const messageBody = 'Hello from test!' const mockTwilioNumber = process.env.TWILIO_PHONE_NUMBER const event = mockHttpEvent({ httpMethod: 'POST', body: JSON.stringify({ to: recipient, body: messageBody }), }) const response = await handler(event, null) // Check response expect(response.statusCode).toBe(200) const responseBody = JSON.parse(response.body) expect(responseBody.success).toBe(true) expect(responseBody.messageSid).toBe('SMmockedsid1234567890abcdef') expect(responseBody.status).toBe('queued') // Check if twilio.messages.create was called correctly const mockCreate = twilio().messages.create expect(mockCreate).toHaveBeenCalledTimes(1) expect(mockCreate).toHaveBeenCalledWith({ to: recipient, body: messageBody, from: mockTwilioNumber, }) }) it('should return error status code if Twilio API call fails', async () => { // Configure the mock to reject this time const errorMessage = 'Twilio API error' const errorCode = 20003 // Example: Authentication error const mockError = new Error(errorMessage) mockError.status = 401 // Unauthorized mockError.code = errorCode mockError.moreInfo = `https://www.twilio.com/docs/errors/${errorCode}` twilio().messages.create.mockRejectedValue(mockError) const event = mockHttpEvent({ httpMethod: 'POST', body: JSON.stringify({ to: '+15559876543', body: 'This will fail', }), }) const response = await handler(event, null) expect(response.statusCode).toBe(401) // Should use error.status const responseBody = JSON.parse(response.body) expect(responseBody.success).toBe(false) expect(responseBody.error).toBe('Failed to send SMS.') expect(responseBody.twilioErrorCode).toBe(errorCode) expect(responseBody.twilioMoreInfo).toContain(errorCode.toString()) }) })
- Explanation:
- We use
jest.mock('twilio', ...)
to replace the actual Twilio library with a mock. - The mock simulates the
client.messages.create
function, allowing us to control its return value (mockResolvedValue
) or make it throw an error (mockRejectedValue
). - We mock
src/lib/logger
to avoid log output during tests. - We mock
process.env
variables needed by the function within the test suite usingbeforeAll
and restore them withafterAll
. - Each
it(...)
block tests a specific scenario: wrong HTTP method, invalid input, successful call, and failed API call. - We use
mockHttpEvent
from@redwoodjs/testing/api
to simulate the event object passed to the handler. - We assert the
statusCode
andbody
of the response. - For the success case, we also assert that the mocked
twilio().messages.create
function was called with the correct arguments. - For the failure case, we assert that the error details (status code, Twilio error code) are correctly propagated to the response.
- We use
- Create/Edit the test file