Send SMS with Twilio in Next.js: Complete Guide with Node.js
Step-by-step tutorial for building a Next.js application that sends SMS messages via Twilio API using Node.js helper library, with secure credential management and production-ready error handling.
Build a robust application that sends SMS messages using a Next.js frontend and backend (API Routes) integrated with the Twilio Programmable Messaging API via their Node.js helper library.
Create a simple web form that accepts a phone number and message, submitting this data to a Next.js API route that securely interacts with the Twilio API to dispatch the SMS. This approach keeps your sensitive API credentials off the client-side and leverages Next.js for both UI and backend logic.
Project Overview and Goals
Goal: Create a functional web application that enables users to send SMS messages via Twilio through a secure Next.js backend API.
Problem Solved: Build a foundational structure for applications needing programmatic SMS capabilities (e.g., notifications, alerts, communication tools) while adhering to best practices for credential management and API interaction.
Technologies:
Next.js: React framework providing server-side rendering, static site generation, and API routes for backend functionality. Works with Next.js 13, 14, and 15 using App Router.
Node.js: Runtime environment for Next.js and the Twilio helper library. Recommended: Node.js 20.x LTS or 22.x LTS (as of January 2025). Node.js 18.x reaches End-of-Life on April 30, 2025 – not recommended for new projects.
Twilio Programmable Messaging API: Third-party service for sending SMS messages.
Twilio Node.js Helper Library: Simplifies Twilio REST API interaction. Current version: 5.x (released 2024). Compatible with SDK v4.x and v5.x.
Purchase an SMS-enabled Twilio Phone Number via the Buy a Number page. Ensure the number supports SMS capabilities.
(Trial Accounts Only) Verify recipient phone numbers in your Twilio Console under Verified Caller IDs. Trial accounts add a "Sent from a Twilio trial account" prefix to messages.
Trial Account Limits:
Concurrent calls: 5 maximum
Call duration: 10 minutes maximum per call
Calls per second (CPS): 1 maximum
SMS/calls: Only to verified phone numbers
Verification method: SMS only (no voice verification)
Basic understanding of React and Next.js concepts.
1. Set Up the Project
Initialize a new Next.js project and install dependencies.
Create Next.js App: Open your terminal and run the following command (TypeScript recommended but optional):
Bash
npx create-next-app@latest nextjs-twilio-sms --typescript
# Or for JavaScript:# npx create-next-app@latest nextjs-twilio-sms
Choose defaults or customize as needed (Tailwind CSS: No, src/ directory: Yes, App Router: Yes, Default import alias: @/*).
Navigate to Project Directory:
Bash
cd nextjs-twilio-sms
Install Twilio Helper Library:
Bash
npm install twilio
# or
yarn add twilio
Set Up Environment Variables: Create .env.local in your project root. This file stores sensitive credentials and must never be committed to version control.
Bash
touch .env.local
Add these lines to .env.local, replacing placeholder values with your actual Twilio credentials:
Text
# .env.local
# Found on your Twilio Console Dashboard: https://www.twilio.com/console
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token_xxxxxxxxxxxxxx
# Your purchased Twilio phone number with SMS capabilities
# Must be in E.164 format: +[country code][number]
TWILIO_PHONE_NUMBER=+15551234567
TWILIO_ACCOUNT_SID: Your unique account identifier from Twilio.
TWILIO_AUTH_TOKEN: Your secret token for authenticating API requests. Keep this secure.
TWILIO_PHONE_NUMBER: The Twilio phone number that appears as the SMS sender.
Add .env.local to .gitignore: Ensure this line exists in your .gitignore file (create one if needed). This prevents accidental commits of your secrets.
Text
# .gitignore (ensure this line exists)
.env*.local
Project Structure: Your relevant project structure should look like this (assuming src directory and App Router):
API Routes within the app/api directory create backend endpoints directly within your Next.js project.
2. Implement Core Functionality: The API Route
Create an API route that receives the recipient number and message body, then uses the Twilio library to send the SMS.
Create the API Route File: Create the directory src/app/api/send-sms/ and inside it, create route.ts (or route.js for JavaScript).
Implement the API Logic: Add this code to src/app/api/send-sms/route.ts:
TypeScript
// src/app/api/send-sms/route.tsimport { NextResponse } from'next/server';
import twilio from'twilio';
// Retrieve Twilio credentials from environment variablesconst accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const twilioPhoneNumber = process.env.TWILIO_PHONE_NUMBER;
// Validate essential environment variablesif (!accountSid || !authToken || !twilioPhoneNumber) {
console.error("FATAL ERROR: Twilio environment variables are not set properly.");
// Stop execution and return an error response if configuration is missingreturnNextResponse.json(
{ success: false, error: "Server configuration error: Twilio credentials missing." },
{ status: 500 } // Internal Server Error
);
}
// Initialize Twilio client (only if credentials are valid)const client = twilio(accountSid, authToken);
exportasyncfunctionPOST(request: Request) {
letto: string;
letbody: string;
try {
const payload = await request.json();
to = payload.to;
body = payload.body;
} catch (parseError) {
console.error("Error parsing request body:", parseError);
returnNextResponse.json(
{ success: false, error: "Invalid request body." },
{ status: 400 } // Bad Request
);
}
// Basic input validationif (!to || !body) {
returnNextResponse.json(
{ success: false, error: "Missing 'to' or 'body' parameter" },
{ status: 400 } // Bad Request
);
}
// More specific validation (recommended)const phoneRegex = /^\+[1-9]\d{1,14}$/; // Basic E.164 format regexif (!phoneRegex.test(to)) {
returnNextResponse.json(
{ success: false, error: "Invalid 'to' phone number format. Use E.164 format (e.g., +15551234567)." },
{ status: 400 }
);
}
if (typeof body !== 'string' || body.trim().length === 0) {
returnNextResponse.json(
{ success: false, error: "Message body cannot be empty." },
{ status: 400 }
);
}
// Consider adding length limits for the body if necessarytry {
console.log(`Attempting to send SMS via Twilio to: ${to}, Body starts with: "${body.substring(0, 30)}..."`); // Log responsiblyconst message = await client.messages.create({
body: body,
from: twilioPhoneNumber, // Your Twilio number from env varsto: to, // The recipient number from the request
});
console.log(`Twilio message initiated successfully. SID: ${message.sid}`); // SID confirms Twilio accepted the request// Return success response to the clientreturnNextResponse.json({ success: true, messageSid: message.sid });
} catch (error: any) {
console.error("Twilio API call failed:", error); // Log the full error server-side// Provide a generic error message to the client for security// Log the specific error internally for debugging// Use Twilio's status code if available, otherwise default to 500// Note: More specific mapping of Twilio error codes (e.g., 21211 -> 400) could be implementedreturnNextResponse.json(
{ success: false, error: "An error occurred while sending the SMS." },
{ status: error.status || 500 }
);
}
}
Explanation:
Import NextResponse and the twilio library.
Retrieve Twilio credentials from process.env. Next.js automatically loads .env.local server-side.
Environment Variable Check: Verify essential variables exist. If not, log a fatal error and return 500 Internal Server Error immediately, preventing the Twilio client from initializing without credentials.
Initialize the Twilio client after the credential check.
The POST handler parses to and body from the JSON request. Include basic JSON parsing error handling.
Input Validation: Check for presence, E.164 format for to, and non-empty body, returning 400 Bad Request on failure.
Twilio API Call: Use client.messages.create() within a try...catch block.
Error Handling: The catch block logs the specific error server-side but returns a generic error message ("An error occurred while sending the SMS.") to the client to avoid exposing internal details. Use error.status if provided by Twilio, otherwise default to 500 Internal Server Error.
3. Build a Simple Frontend
Create a basic form on the homepage to interact with your API route.
Modify the Homepage: Open src/app/page.tsx (or page.js) and replace its contents:
TypeScript
// src/app/page.tsx'use client'; // Required for useState and event handlersimport { useState, FormEvent } from'react';
// Import your CSS module if you move styles there// import styles from './page.module.css';exportdefaultfunctionHomePage() {
const [toNumber, setToNumber] = useState('');
const [messageBody, setMessageBody] = useState('');
const [status, setStatus] = useState('');
const [isLoading, setIsLoading] = useState(false);
consthandleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsLoading(true);
setStatus('Sending…');
try {
const response = awaitfetch('/api/send-sms', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ to: toNumber, body: messageBody }),
});
const data = await response.json();
if (response.ok && data.success) {
setStatus(`Message sent successfully! SID: ${data.messageSid}`);
// Optionally clear the form on success// setToNumber('');// setMessageBody('');
} else {
// Use the error message from the API response, or a defaultsetStatus(`Failed to send message: ${data.error || 'Unknown server error'}`);
}
} catch (error: any) {
console.error('Network or fetch error:', error);
setStatus(`Error: ${error.message || 'Could not connect to the API. Check network connection.'}`);
} finally {
setIsLoading(false);
}
};
// NOTE: The inline <style jsx> block has been removed for better compatibility.// Move styles to a separate CSS file (e.g., `src/app/page.module.css` or `src/app/globals.css`)// and import/apply classes as needed. The structure below assumes basic HTML elements.return (
<main /* className={styles.main} */ ><h1>Send SMS via Twilio</h1><formonSubmit={handleSubmit} /* className={styles.form} */ ><div /* className={styles.formGroup} */ ><labelhtmlFor="toNumber">To Number:</label><inputtype="tel"id="toNumber"value={toNumber}onChange={(e) => setToNumber(e.target.value)}
placeholder="+15551234567 (E.164 format)"
required
disabled={isLoading}
aria-label="Recipient Phone Number"
/>
</div><div /* className={styles.formGroup} */ ><labelhtmlFor="messageBody">Message:</label><textareaid="messageBody"value={messageBody}onChange={(e) => setMessageBody(e.target.value)}
rows={4}
required
disabled={isLoading}
aria-label="SMS Message Body"
/>
</div><buttontype="submit"disabled={isLoading}>
{isLoading ? 'Sending…' : 'Send SMS'}
</button></form>
{status && <p /* className={styles.status} */ role="status">{status}</p>}
</main>
);
}
Explanation:
'use client': Marks this as a Client Component for interactivity.
State: Manages input values (toNumber, messageBody), loading state (isLoading), and status message (status).
handleSubmit: Handles form submission, makes a POST request to /api/send-sms with form data, and updates status based on the API response or fetch errors.
Form: Includes label elements with htmlFor attributes matching the id attributes of input/textarea elements for accessibility. Inputs are disabled during loading.
Status Display: Shows feedback to the user.
Styling: Inline JSX styles removed. Define styles in a separate CSS file (CSS Modules like page.module.css or global CSS like globals.css) and apply corresponding class names to elements (commented placeholders like /* className={styles.main} */ included as a guide).
4. Error Handling and Logging
The application includes error handling at multiple levels:
Frontend (page.tsx):
Uses try...catch block around the fetch call to handle network errors or unreachable API endpoints.
Checks response.ok status and data.success flag returned by the API.
Displays user-friendly status messages, including errors reported by the API (data.error).
Disables the form during submission to prevent duplicate requests.
API Route (route.ts):
Environment Variable Check: Fails fast with 500 error if essential Twilio configuration is missing.
Input Validation: Returns 400 Bad Request for missing or invalid to number or body.
Twilio API Call: Uses try...catch block around client.messages.create.
Server-Side Logging: Logs detailed errors (including the original error from Twilio) using console.error. In production, replace console.error with a proper logging library/service (e.g., Winston, Pino, Sentry, Datadog).
Client-Side Error Response: Returns a generic error message ("An error occurred…") to the client upon Twilio API failure, hiding potentially sensitive details. Includes an appropriate HTTP status code (error.status or 500).
Test Error Scenarios:
Invalid Input: Enter an incorrectly formatted phone number (e.g., "12345") or leave the message blank. The API should return a 400 error, reflected on the frontend.
Missing/Invalid Credentials: Temporarily modify or remove values in .env.local and restart the server (npm run dev). Attempting to send should result in a server error (likely the 500 error from the initial check, or a 401/500 from Twilio if the check passes but creds are wrong), logged server-side and shown generically on the frontend. Restore correct credentials afterward.
Trial Account Restrictions: Send to a non-verified number using a trial account. Expect a Twilio error (e.g., 21608), logged server-side, generic error on frontend.
Network Error: Stop the development server (Ctrl+C) and try submitting the form. Expect a fetch error message on the frontend.
5. Troubleshooting and Caveats
Environment Variables Not Loaded: Ensure .env.local is in the project root, correctly named, and contains the right keys/values. Restart your Next.js dev server (npm run dev) after any changes to .env.local. Verify variables in the API route with temporary console.log statements if needed (remove before production).
Invalid Credentials (Twilio Error 20003 / HTTP 401): Double-check TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN in .env.local against the Twilio Console. Ensure they are correctly set in your deployment environment variables. See Twilio Error 20003 documentation for details.
Invalid 'To' Number (Twilio Error 21211 / HTTP 400): Ensure the recipient number is in E.164 format (+[country code][number], e.g., +15551234567) and is valid. See Twilio Error 21211 documentation.
Invalid 'From' Number (Twilio Error 21212 / HTTP 400): Ensure TWILIO_PHONE_NUMBER in .env.local (and deployment env vars) is a valid Twilio number you own, has SMS capabilities, and is in E.164 format. See Twilio Error 21212 documentation.
Trial Account Restrictions (Twilio Error 21608 / HTTP 400): Trial accounts can only send SMS to numbers verified in your Twilio Console (Verified Caller IDs). Error 21608 means "The To phone number provided is not yet verified for this account." See Twilio Error 21608 documentation.
API Route Not Found (404): Check the file path (src/app/api/send-sms/route.ts) and the fetch URL (/api/send-sms) match exactly. Case sensitivity matters.
JSON Parsing Errors: Ensure the frontend fetch sends Content-Type: application/json and a valid JSON string in the body. Ensure the API route correctly uses await request.json().
Rate Limits: Twilio implements several rate limits to prevent abuse:
API Concurrency Limit: Requests that exceed your account's REST API concurrency limit will receive HTTP 429 (Error 20429). Implement exponential backoff retry logic.
Message Queue: Each Twilio phone number has a separate queue that can hold up to 10 hours worth of message segments based on the sending rate for that phone number type. Messages beyond this limit may be rejected.
Per-Number Throughput: Varies by number type (long code, short code, toll-free). High-frequency sending may require Twilio Messaging Services for sender pools and higher throughput.
MMS Limits: Long code MMS is limited to 1 MMS/second per phone number, with account-level cap at 50 MMS/second. Toll-free MMS: 3 MMS/second per number, 25 MMS/second account-level cap.
Check Twilio Debugger: The Twilio Console Error Logs/Debugger is invaluable for diagnosing Twilio API errors and message delivery issues. Access the Message Logs to track message status (Queued, Sent, Delivered, Failed, etc.).
6. Deployment and CI/CD
Deploying this Next.js app is typically straightforward.
Deploying to Vercel (Example):
Push to Git: Commit your code to a Git provider (GitHub, GitLab, Bitbucket). Ensure .env.local is in .gitignore.
Import to Vercel: Connect your Git repository to Vercel. Vercel auto-detects Next.js.
TWILIO_PHONE_NUMBER
Set their values matching your .env.local. Apply them to Production, Preview, and Development environments. Vercel securely injects these into your API routes.
Deploy: Vercel usually deploys automatically on pushes to the main branch.
Test: Use the Vercel deployment URL to test SMS sending.
CI/CD: Platforms like Vercel, Netlify, AWS Amplify, or GitHub Actions handle CI/CD. Pushing code triggers builds and deployments. The key is correctly configuring runtime environment variables on the hosting platform.
7. Verification and Testing
Run Development Server:
Bash
npm run dev
# or
yarn dev
Access http://localhost:3000.
Manual Frontend Test (Happy Path):
Enter a valid, verified recipient number (E.164).
Enter a message. Click ""Send SMS"".
Expected: Status shows ""Sending..."", then ""Message sent successfully! SID: SM..."". SMS received. Server logs show SID.
Manual Frontend Test (Validation Errors):
Enter invalid phone format. Click Send.
Expected: Status shows ""Failed to send message: Invalid 'to' phone number format..."". Server logs show 400 error.
Leave message empty. Click Send.
Expected: Status shows ""Failed to send message: Message body cannot be empty."". Server logs show 400 error.
API Endpoint Test (using curl or Postman):
Success Case: (Ensure dev server running)
Bash
curl -X POST http://localhost:3000/api/send-sms \
-H "Content-Type: application/json" \
-d '{""to"": ""+15551234567"", ""body"": ""Test via curl""}'# Replace number with a valid, verified one
Create a Next.js API route that interacts with the Twilio API using their Node.js helper library. This API route will handle sending messages securely via Twilio's Programmable Messaging API. A frontend form in your Next.js app will collect recipient details and the message, then send it to the API route for processing.
It's a library that simplifies interaction with the Twilio REST API from your Next.js backend. It handles the complexities of making API calls and managing responses, making SMS integration easier.
Using API routes keeps your Twilio API credentials secure on the server-side, away from the client-side code. API routes provide a convenient way to handle backend logic like sending SMS messages within your Next.js application, reducing complexity.
Verification is required for trial Twilio accounts. If using a trial account, you'll need to verify recipient numbers within the Twilio Console under Verified Caller IDs before sending them messages.
Yes, this provides a solid foundation for production. For enhanced security and scalability, add authentication, more robust logging, and consider Twilio Messaging Services for managing sender identities and higher message throughput.
Create a .env.local file in your project's root directory and add your TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER. Make sure to add .env.local to your .gitignore to protect your credentials. Next.js reads environment variables automatically, both server-side and for API routes.
Use the E.164 format, which includes a plus sign followed by the country code and the phone number. For example, a US number would be +15551234567. This format is crucial for the Twilio API to correctly process the number.
Implement a try-catch block around your Twilio API call within your API route. Log detailed errors server-side for debugging purposes and return a generic, non-revealing error message to the user for security.
Twilio Messaging Services are a feature that helps manage multiple sender identities, including phone numbers and shortcodes. They also provide tools for scalability, such as high-throughput message sending and delivery insights, making them useful for larger applications.
These credentials are found in your Twilio Console Dashboard. Log into your Twilio account and navigate to the dashboard to locate your unique Account SID and Auth Token. Keep these secure!
Connect your Git repository to Vercel and configure the TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_PHONE_NUMBER environment variables in your Vercel project settings. After deploying, Vercel will automatically inject these variables into your API routes.
Organize your project with API routes inside src/app/api/send-sms/route.ts to handle the backend SMS logic. Your frontend code in src/app/page.tsx will manage user interaction.
The provided code includes input validation that checks for the correct E.164 format using a regular expression. If an invalid number is entered, the API returns a 400 Bad Request error, and the frontend displays a specific error message, guiding the user to use the correct format.
Build a robust application that sends SMS messages using a Next.js frontend and backend (API Routes) integrated with the Twilio Programmable Messaging API via their Node.js helper library.
Create a simple web form that accepts a phone number and message, submitting this data to a Next.js API route that securely interacts with the Twilio API to dispatch the SMS. This approach keeps your sensitive API credentials off the client-side and leverages Next.js for both UI and backend logic.
Project Overview and Goals
Goal: Create a functional web application that enables users to send SMS messages via Twilio through a secure Next.js backend API.
Problem Solved: Build a foundational structure for applications needing programmatic SMS capabilities (e.g., notifications, alerts, communication tools) while adhering to best practices for credential management and API interaction.
Technologies:
System Architecture:
Outcome: A working Next.js application with a simple UI to send SMS messages. The backend API route securely handles Twilio credentials and API calls.
Prerequisites:
node --version. Download from nodejs.org.1. Set Up the Project
Initialize a new Next.js project and install dependencies.
Create Next.js App: Open your terminal and run the following command (TypeScript recommended but optional):
npx create-next-app@latest nextjs-twilio-sms --typescript # Or for JavaScript: # npx create-next-app@latest nextjs-twilio-smsChoose defaults or customize as needed (Tailwind CSS: No,
src/directory: Yes, App Router: Yes, Default import alias:@/*).Navigate to Project Directory:
cd nextjs-twilio-smsInstall Twilio Helper Library:
npm install twilio # or yarn add twilioSet Up Environment Variables: Create
.env.localin your project root. This file stores sensitive credentials and must never be committed to version control.touch .env.localAdd these lines to
.env.local, replacing placeholder values with your actual Twilio credentials:TWILIO_ACCOUNT_SID: Your unique account identifier from Twilio.TWILIO_AUTH_TOKEN: Your secret token for authenticating API requests. Keep this secure.TWILIO_PHONE_NUMBER: The Twilio phone number that appears as the SMS sender.Add
.env.localto.gitignore: Ensure this line exists in your.gitignorefile (create one if needed). This prevents accidental commits of your secrets.Project Structure: Your relevant project structure should look like this (assuming
srcdirectory and App Router):API Routes within the
app/apidirectory create backend endpoints directly within your Next.js project.2. Implement Core Functionality: The API Route
Create an API route that receives the recipient number and message body, then uses the Twilio library to send the SMS.
Create the API Route File: Create the directory
src/app/api/send-sms/and inside it, createroute.ts(orroute.jsfor JavaScript).Implement the API Logic: Add this code to
src/app/api/send-sms/route.ts:// src/app/api/send-sms/route.ts import { NextResponse } from 'next/server'; import twilio from 'twilio'; // Retrieve Twilio credentials from environment variables const accountSid = process.env.TWILIO_ACCOUNT_SID; const authToken = process.env.TWILIO_AUTH_TOKEN; const twilioPhoneNumber = process.env.TWILIO_PHONE_NUMBER; // Validate essential environment variables if (!accountSid || !authToken || !twilioPhoneNumber) { console.error("FATAL ERROR: Twilio environment variables are not set properly."); // Stop execution and return an error response if configuration is missing return NextResponse.json( { success: false, error: "Server configuration error: Twilio credentials missing." }, { status: 500 } // Internal Server Error ); } // Initialize Twilio client (only if credentials are valid) const client = twilio(accountSid, authToken); export async function POST(request: Request) { let to: string; let body: string; try { const payload = await request.json(); to = payload.to; body = payload.body; } catch (parseError) { console.error("Error parsing request body:", parseError); return NextResponse.json( { success: false, error: "Invalid request body." }, { status: 400 } // Bad Request ); } // Basic input validation if (!to || !body) { return NextResponse.json( { success: false, error: "Missing 'to' or 'body' parameter" }, { status: 400 } // Bad Request ); } // More specific validation (recommended) const phoneRegex = /^\+[1-9]\d{1,14}$/; // Basic E.164 format regex if (!phoneRegex.test(to)) { return NextResponse.json( { success: false, error: "Invalid 'to' phone number format. Use E.164 format (e.g., +15551234567)." }, { status: 400 } ); } if (typeof body !== 'string' || body.trim().length === 0) { return NextResponse.json( { success: false, error: "Message body cannot be empty." }, { status: 400 } ); } // Consider adding length limits for the body if necessary try { console.log(`Attempting to send SMS via Twilio to: ${to}, Body starts with: "${body.substring(0, 30)}..."`); // Log responsibly const message = await client.messages.create({ body: body, from: twilioPhoneNumber, // Your Twilio number from env vars to: to, // The recipient number from the request }); console.log(`Twilio message initiated successfully. SID: ${message.sid}`); // SID confirms Twilio accepted the request // Return success response to the client return NextResponse.json({ success: true, messageSid: message.sid }); } catch (error: any) { console.error("Twilio API call failed:", error); // Log the full error server-side // Provide a generic error message to the client for security // Log the specific error internally for debugging // Use Twilio's status code if available, otherwise default to 500 // Note: More specific mapping of Twilio error codes (e.g., 21211 -> 400) could be implemented return NextResponse.json( { success: false, error: "An error occurred while sending the SMS." }, { status: error.status || 500 } ); } }Explanation:
NextResponseand thetwiliolibrary.process.env. Next.js automatically loads.env.localserver-side.500 Internal Server Errorimmediately, preventing the Twilio client from initializing without credentials.POSThandler parsestoandbodyfrom the JSON request. Include basic JSON parsing error handling.to, and non-emptybody, returning400 Bad Requeston failure.client.messages.create()within atry...catchblock.{ success: true, messageSid: ... }.catchblock logs the specificerrorserver-side but returns a generic error message ("An error occurred while sending the SMS.") to the client to avoid exposing internal details. Useerror.statusif provided by Twilio, otherwise default to500 Internal Server Error.3. Build a Simple Frontend
Create a basic form on the homepage to interact with your API route.
Modify the Homepage: Open
src/app/page.tsx(orpage.js) and replace its contents:// src/app/page.tsx 'use client'; // Required for useState and event handlers import { useState, FormEvent } from 'react'; // Import your CSS module if you move styles there // import styles from './page.module.css'; export default function HomePage() { const [toNumber, setToNumber] = useState(''); const [messageBody, setMessageBody] = useState(''); const [status, setStatus] = useState(''); const [isLoading, setIsLoading] = useState(false); const handleSubmit = async (event: FormEvent<HTMLFormElement>) => { event.preventDefault(); setIsLoading(true); setStatus('Sending…'); try { const response = await fetch('/api/send-sms', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ to: toNumber, body: messageBody }), }); const data = await response.json(); if (response.ok && data.success) { setStatus(`Message sent successfully! SID: ${data.messageSid}`); // Optionally clear the form on success // setToNumber(''); // setMessageBody(''); } else { // Use the error message from the API response, or a default setStatus(`Failed to send message: ${data.error || 'Unknown server error'}`); } } catch (error: any) { console.error('Network or fetch error:', error); setStatus(`Error: ${error.message || 'Could not connect to the API. Check network connection.'}`); } finally { setIsLoading(false); } }; // NOTE: The inline <style jsx> block has been removed for better compatibility. // Move styles to a separate CSS file (e.g., `src/app/page.module.css` or `src/app/globals.css`) // and import/apply classes as needed. The structure below assumes basic HTML elements. return ( <main /* className={styles.main} */ > <h1>Send SMS via Twilio</h1> <form onSubmit={handleSubmit} /* className={styles.form} */ > <div /* className={styles.formGroup} */ > <label htmlFor="toNumber">To Number:</label> <input type="tel" id="toNumber" value={toNumber} onChange={(e) => setToNumber(e.target.value)} placeholder="+15551234567 (E.164 format)" required disabled={isLoading} aria-label="Recipient Phone Number" /> </div> <div /* className={styles.formGroup} */ > <label htmlFor="messageBody">Message:</label> <textarea id="messageBody" value={messageBody} onChange={(e) => setMessageBody(e.target.value)} rows={4} required disabled={isLoading} aria-label="SMS Message Body" /> </div> <button type="submit" disabled={isLoading}> {isLoading ? 'Sending…' : 'Send SMS'} </button> </form> {status && <p /* className={styles.status} */ role="status">{status}</p>} </main> ); }Explanation:
'use client': Marks this as a Client Component for interactivity.toNumber,messageBody), loading state (isLoading), and status message (status).handleSubmit: Handles form submission, makes aPOSTrequest to/api/send-smswith form data, and updates status based on the API response or fetch errors.labelelements withhtmlForattributes matching theidattributes ofinput/textareaelements for accessibility. Inputs are disabled during loading.page.module.cssor global CSS likeglobals.css) and apply corresponding class names to elements (commented placeholders like/* className={styles.main} */included as a guide).4. Error Handling and Logging
The application includes error handling at multiple levels:
page.tsx):try...catchblock around thefetchcall to handle network errors or unreachable API endpoints.response.okstatus anddata.successflag returned by the API.data.error).route.ts):500error if essential Twilio configuration is missing.400 Bad Requestfor missing or invalidtonumber orbody.try...catchblock aroundclient.messages.create.console.error. In production, replaceconsole.errorwith a proper logging library/service (e.g., Winston, Pino, Sentry, Datadog).error.statusor500).Test Error Scenarios:
"12345") or leave the message blank. The API should return a 400 error, reflected on the frontend..env.localand restart the server (npm run dev). Attempting to send should result in a server error (likely the 500 error from the initial check, or a 401/500 from Twilio if the check passes but creds are wrong), logged server-side and shown generically on the frontend. Restore correct credentials afterward.Ctrl+C) and try submitting the form. Expect a fetch error message on the frontend.5. Troubleshooting and Caveats
.env.localis in the project root, correctly named, and contains the right keys/values. Restart your Next.js dev server (npm run dev) after any changes to.env.local. Verify variables in the API route with temporaryconsole.logstatements if needed (remove before production).TWILIO_ACCOUNT_SIDandTWILIO_AUTH_TOKENin.env.localagainst the Twilio Console. Ensure they are correctly set in your deployment environment variables. See Twilio Error 20003 documentation for details.+[country code][number], e.g.,+15551234567) and is valid. See Twilio Error 21211 documentation.TWILIO_PHONE_NUMBERin.env.local(and deployment env vars) is a valid Twilio number you own, has SMS capabilities, and is in E.164 format. See Twilio Error 21212 documentation.src/app/api/send-sms/route.ts) and thefetchURL (/api/send-sms) match exactly. Case sensitivity matters.fetchsendsContent-Type: application/jsonand a valid JSON string in the body. Ensure the API route correctly usesawait request.json().6. Deployment and CI/CD
Deploying this Next.js app is typically straightforward.
Deploying to Vercel (Example):
.env.localis in.gitignore.TWILIO_ACCOUNT_SIDTWILIO_AUTH_TOKENTWILIO_PHONE_NUMBERSet their values matching your.env.local. Apply them to Production, Preview, and Development environments. Vercel securely injects these into your API routes.CI/CD: Platforms like Vercel, Netlify, AWS Amplify, or GitHub Actions handle CI/CD. Pushing code triggers builds and deployments. The key is correctly configuring runtime environment variables on the hosting platform.
7. Verification and Testing
Run Development Server:
npm run dev # or yarn devAccess
http://localhost:3000.Manual Frontend Test (Happy Path):
Manual Frontend Test (Validation Errors):
API Endpoint Test (using
curlor Postman):curl -X POST http://localhost:3000/api/send-sms \ -H "Content-Type: application/json" \ -d '{""to"": ""+15551234567"", ""body"": ""Test via curl""}' # Replace number with a valid, verified one{""success"":true,""messageSid"":""SMxxxxxxxx...""}curl -X POST http://localhost:3000/api/send-sms \ -H "Content-Type: application/json" \ -d '{""to"": ""+15551234567""}'{""success"":false,""error"":""Missing 'to' or 'body' parameter""}Check Twilio Logs: Review the Twilio Console Message Logs and Error Logs for message status (Queued, Sent, Failed, etc.) and detailed error info.
Verification Checklist:
npm run build).npm run dev)..env.localconfigured and gitignored./api/send-sms) logic correct.curl.Complete Code Repository
A complete, working example of this project can be found on GitHub. (Note: Link removed as per instructions, as the actual URL was not provided.)
Next Steps
Extend this foundation:
mediaUrlparameter toclient.messages.create.Happy building!