This guide provides a complete walkthrough for building a production-ready web application using Next.js and MessageBird to allow users to book appointments and automatically receive SMS reminders. We'll cover everything from project setup to deployment, focusing on best practices, security, and error handling.
The final application will feature a simple web form for booking appointments and leverage MessageBird's API for phone number validation and scheduling SMS messages to be sent at a specific time before the appointment. This solves the common problem of no-shows for appointment-based services by providing timely, automated nudges to customers.
Project Overview and Goals
- Goal: Create a web application where users can book appointments and receive automated SMS reminders via MessageBird.
- Problem Solved: Reduces appointment no-shows and improves customer communication for businesses.
- Technologies:
- Next.js: React framework for the frontend UI and API routes (backend logic). Chosen for its developer experience, performance features, and integrated API capabilities.
- Node.js: Runtime environment for Next.js.
- MessageBird: Communication Platform as a Service (CPaaS) used for:
- Lookup API: Validating user-provided phone numbers.
- SMS API: Sending and scheduling SMS reminders. Chosen for its direct scheduling feature and developer-friendly SDK.
- PostgreSQL: Relational database for storing appointment data. Chosen for its reliability and robustness.
- Prisma: Next-generation ORM for Node.js and TypeScript, used for database access and migrations. Chosen for its type safety and ease of use.
- Prerequisites:
- Node.js (v18 or later recommended) and npm/yarn.
- A MessageBird account with an API key.
- Access to a PostgreSQL database (local or cloud-hosted).
- Basic understanding of React, Next.js, and asynchronous JavaScript.
System Architecture
Here's a high-level overview of how the components interact:
+-------------+ +---------------------+ +---------------------+ +----------------+ +----------------+
| Browser |----->| Next.js Frontend |----->| Next.js API Route |----->| Prisma |----->| PostgreSQL |
| (User Form) | | (React Page) | | (/api/book) | | (ORM Client) | | (Database) |
+-------------+ +---------------------+ +----------+----------+ +----------------+ +----------------+
|
| Calls
v
+--------------------+
| MessageBird API |
| (Lookup & Messages)|
+--------------------+
- The user fills out the appointment booking form in their browser.
- The Next.js frontend page submits the form data to a Next.js API route (
/api/book
). - The API route validates the input, including using the MessageBird Lookup API to verify the phone number.
- If valid, the API route calculates the reminder time and uses the MessageBird Messages API to schedule the SMS reminder.
- The API route uses Prisma to save the appointment details to the PostgreSQL database.
- The API route returns a success or error response to the frontend.
- At the scheduled time, MessageBird automatically sends the SMS reminder to the user's phone.
1. Setting up the Project
Let's initialize our Next.js project and set up the necessary tools and configurations.
1. Create Next.js App: Open your terminal and run:
npx create-next-app@latest messagebird-reminders --typescript --eslint --tailwind --src-dir --app --import-alias ""@/*""
cd messagebird-reminders
We're using TypeScript, ESLint, Tailwind CSS, the src/
directory structure, and the App Router for this project.
2. Install Dependencies:
Install the MessageBird SDK, Prisma client, and Moment.js (for date/time manipulation). Next.js has built-in support for .env
files, so explicit dotenv
installation is usually not required.
npm install messagebird @prisma/client moment moment-timezone
npm install prisma --save-dev
messagebird
: Official SDK for interacting with the MessageBird API.@prisma/client
: Auto-generated database client based on your schema.prisma
: CLI tool for database migrations and schema management.moment
,moment-timezone
: Libraries for robust date, time, and timezone handling. Note: Moment.js is in maintenance mode. For new projects, consider alternatives like Day.js or date-fns for smaller bundle sizes and better immutability.
3. Initialize Prisma: Set up Prisma to manage our database schema and connections.
npx prisma init --datasource-provider postgresql
This creates:
prisma/schema.prisma
: Your database schema definition file..env
: A file for environment variables (automatically added to.gitignore
). Next.js automatically loads variables from this file.
4. Configure Environment Variables:
Open the .env
file created in the project root and add your MessageBird API key and database connection URL.
# .env
# Database Connection (Replace with your actual PostgreSQL connection string)
# Example format: postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public
DATABASE_URL=""postgresql://postgres:password@localhost:5432/reminders?schema=public""
# MessageBird API Key (Get from MessageBird Dashboard -> Developers -> API access)
# Use your LIVE key for actual sending, TEST key for development without sending SMS.
MESSAGEBIRD_API_KEY=""YOUR_MESSAGEBIRD_API_KEY""
# Default Sender ID (Alphanumeric or E.164 Number - Check country regulations)
MESSAGEBIRD_ORIGINATOR=""BeautyBird"" # Or your MessageBird phone number
# Default Country Code for Phone Lookup (Optional, helps with local numbers)
# Example: US, NL, GB
DEFAULT_COUNTRY_CODE=""US""
DATABASE_URL
: Get this from your PostgreSQL provider or local setup. Ensure the database (reminders
in the example) exists.MESSAGEBIRD_API_KEY
: Find this in your MessageBird Dashboard under Developers > API access > Live API Key.MESSAGEBIRD_ORIGINATOR
: This is the sender ID that appears on the recipient's phone. It can be an alphanumeric string (like""BeautyBird""
) or a phone number purchased through MessageBird. Important: Alphanumeric sender IDs are not supported in all countries (e.g., the US). Check MessageBird's country restrictions and use a valid number if needed.DEFAULT_COUNTRY_CODE
: An ISO 3166-1 alpha-2 country code (e.g.,""US""
,""GB""
,""NL""
). Providing this helps the MessageBird Lookup API parse phone numbers entered without an international prefix.
5. Define Database Schema:
Open prisma/schema.prisma
and define the model for storing appointments.
// prisma/schema.prisma
generator client {
provider = ""prisma-client-js""
}
datasource db {
provider = ""postgresql""
url = env(""DATABASE_URL"")
}
model Appointment {
id String @id @default(cuid())
name String
phoneNumber String // Store the normalized number from MessageBird Lookup
treatment String
appointmentTime DateTime // Store in UTC
timeZone String // User's selected timezone (e.g., ""America/New_York"")
reminderSentAt DateTime? // Optional: Store when the reminder was scheduled/sent
messageBirdId String? // Optional: Store the MessageBird message ID
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([phoneNumber]) // Add index for phone number lookups
@@index([appointmentTime]) // Add index for time-based queries
}
- We store
appointmentTime
asDateTime
(which Prisma maps to timestamp with timezone in Postgres, effectively storing it in UTC). - We store the user's
timeZone
explicitly to handle scheduling correctly across different regions. phoneNumber
stores the E.164 formatted number returned by MessageBird Lookup.- Optional fields
reminderSentAt
andmessageBirdId
can be useful for tracking. - Added indexes (
@@index
) for potentially common query fields.
6. Create Initial Database Migration:
Apply the schema to your database. Prisma will create the Appointment
table and indexes.
npx prisma migrate dev --name init
Follow the prompts. This creates SQL migration files in prisma/migrations
and updates your database.
7. Set up Prisma Client: Create a utility file to instantiate the Prisma client, ensuring we reuse the same instance across our application (important for Next.js API routes).
// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';
declare global {
// allow global `var` declarations
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined;
}
export const prisma =
global.prisma ||
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') global.prisma = prisma;
8. Configure MessageBird Client: Similarly, create a utility for the MessageBird client.
// src/lib/messagebird.ts
import MessageBird from 'messagebird';
const apiKey = process.env.MESSAGEBIRD_API_KEY;
if (!apiKey) {
throw new Error(""MESSAGEBIRD_API_KEY is not set in environment variables."");
}
export const messagebird = MessageBird(apiKey);
This setup provides a solid foundation for our application logic.
2. Implementing Core Functionality (Frontend Form)
Let's build the user interface for booking appointments. We'll use React state and Tailwind CSS within a Next.js page component.
Create a new page file src/app/book/page.tsx
.
// src/app/book/page.tsx
'use client';
import { useState, FormEvent } from 'react';
import moment from 'moment-timezone';
// Note: moment.tz.names() returns a very long list. Consider filtering
// this list for better UX (e.g., common timezones) or using a dedicated timezone picker component.
const timezones = moment.tz.names();
export default function BookAppointmentPage() {
const [name, setName] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');
const [treatment, setTreatment] = useState('Consultation'); // Default value
const [date, setDate] = useState('');
const [time, setTime] = useState('');
const [timeZone, setTimeZone] = useState(moment.tz.guess()); // Guess user's timezone
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsLoading(true);
setMessage(null);
// Basic client-side validation (more robust validation happens server-side)
if (!name || !phoneNumber || !date || !time || !timeZone) {
setMessage({ type: 'error', text: 'Please fill in all fields.' });
setIsLoading(false);
return;
}
// Combine date and time for the API
const appointmentTime = `${date} ${time}`;
try {
const response = await fetch('/api/book', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name,
phoneNumber,
treatment,
appointmentTime, // Send combined date/time string
timeZone,
}),
});
const result = await response.json();
if (!response.ok || !result.success) {
throw new Error(result.error || 'Failed to book appointment.');
}
setMessage({ type: 'success', text: 'Appointment booked successfully! A reminder will be sent.' });
// Optionally reset the form
// setName(''); setPhoneNumber(''); setDate(''); setTime('');
} catch (error: any) {
console.error('Booking error:', error);
setMessage({ type: 'error', text: error.message || 'An unexpected error occurred.' });
} finally {
setIsLoading(false);
}
};
// Get minimum date allowed (e.g., today)
const getMinDate = () => {
return moment().format('YYYY-MM-DD');
};
return (
<div className=""min-h-screen bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8"">
<div className=""max-w-md w-full space-y-8 bg-white p-10 rounded-lg shadow-md"">
<div>
<h2 className=""mt-6 text-center text-3xl font-extrabold text-gray-900"">
Book Your Appointment
</h2>
</div>
<form className=""mt-8 space-y-6"" onSubmit={handleSubmit}>
{message && (
<div className={`p-4 rounded-md ${message.type === 'success' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
{message.text}
</div>
)}
<div className=""rounded-md shadow-sm -space-y-px"">
<div>
<label htmlFor=""name"" className=""sr-only"">Name</label>
<input
id=""name""
name=""name""
type=""text""
required
className=""appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm""
placeholder=""Full Name""
value={name}
onChange={(e) => setName(e.target.value)}
disabled={isLoading}
/>
</div>
<div>
<label htmlFor=""phone-number"" className=""sr-only"">Phone Number</label>
<input
id=""phone-number""
name=""phoneNumber""
type=""tel"" // Use 'tel' type for better mobile usability
required
className=""appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm""
placeholder=""Phone Number (e.g._ +1 555-123-4567)""
value={phoneNumber}
onChange={(e) => setPhoneNumber(e.target.value)}
disabled={isLoading}
/>
</div>
<div>
<label htmlFor=""treatment"" className=""sr-only"">Treatment</label>
<select
id=""treatment""
name=""treatment""
required
className=""appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm""
value={treatment}
onChange={(e) => setTreatment(e.target.value)}
disabled={isLoading}
>
<option>Consultation</option>
<option>Haircut</option>
<option>Manicure</option>
<option>Facial</option>
</select>
</div>
<div className=""flex space-x-2"">
<div className=""w-1/2"">
<label htmlFor=""date"" className=""sr-only"">Date</label>
<input
id=""date""
name=""date""
type=""date""
required
min={getMinDate()} // Prevent booking past dates
className=""appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm""
value={date}
onChange={(e) => setDate(e.target.value)}
disabled={isLoading}
/>
</div>
<div className=""w-1/2"">
<label htmlFor=""time"" className=""sr-only"">Time</label>
<input
id=""time""
name=""time""
type=""time""
required
className=""appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm""
value={time}
onChange={(e) => setTime(e.target.value)}
disabled={isLoading}
/>
</div>
</div>
<div>
<label htmlFor=""timeZone"" className=""sr-only"">Time Zone</label>
<select
id=""timeZone""
name=""timeZone""
required
className=""appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm""
value={timeZone}
onChange={(e) => setTimeZone(e.target.value)}
disabled={isLoading}
>
{timezones.map(tz => (
<option key={tz} value={tz}>{tz}</option>
))}
</select>
</div>
</div>
<div>
<button
type=""submit""
className=""group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50""
disabled={isLoading}
>
{isLoading ? 'Booking...' : 'Book Appointment'}
</button>
</div>
</form>
</div>
</div>
);
}
This form collects the necessary details, performs basic client-side checks, sends the data to our yet-to-be-created API endpoint, and displays success or error messages.
3. Building the API Layer
Now, let's create the Next.js API route (src/app/api/book/route.ts
) that will handle the form submission, interact with MessageBird, and save data to the database.
// src/app/api/book/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { messagebird } from '@/lib/messagebird';
import moment from 'moment-timezone';
import { LookupResponse } from 'messagebird/types/lookup'; // Import specific types
import { MessagesResponse } from 'messagebird/types/messages'; // For response type
// Define expected request body structure
interface BookingRequestBody {
name: string;
phoneNumber: string;
treatment: string;
appointmentTime: string; // Expecting ""YYYY-MM-DD HH:mm"" or similar parsable format
timeZone: string;
}
// How many hours before the appointment to send the reminder
const REMINDER_HOURS_BEFORE = 3;
// Minimum notice required for booking (e.g., 3 hours + 5 minutes buffer)
const MIN_BOOKING_NOTICE_HOURS = REMINDER_HOURS_BEFORE + (5 / 60);
export async function POST(req: NextRequest) {
let messageBirdMessageId: string | undefined = undefined; // Store message ID for potential cancellation
try {
const body = await req.json() as BookingRequestBody;
// --- 1. Server-Side Validation ---
const { name, phoneNumber, treatment, appointmentTime, timeZone } = body;
if (!name || !phoneNumber || !treatment || !appointmentTime || !timeZone) {
return NextResponse.json({ success: false, error: 'Missing required fields.' }, { status: 400 });
}
if (!moment.tz.zone(timeZone)) {
return NextResponse.json({ success: false, error: 'Invalid time zone provided.' }, { status: 400 });
}
// Parse the appointment date/time using the provided timezone
const appointmentMoment = moment.tz(appointmentTime, timeZone);
if (!appointmentMoment.isValid()) {
return NextResponse.json({ success: false, error: 'Invalid date or time format.' }, { status: 400 });
}
// Check if appointment is sufficiently in the future
const earliestPossibleBooking = moment().add(MIN_BOOKING_NOTICE_HOURS, 'hours');
if (appointmentMoment.isBefore(earliestPossibleBooking)) {
return NextResponse.json({ success: false, error: `Booking must be at least ${MIN_BOOKING_NOTICE_HOURS} hours in the future.` }, { status: 400 });
}
// --- 2. Phone Number Validation (MessageBird Lookup) ---
let validatedPhoneNumber: string;
try {
// Provide default country code if user didn't include international prefix
const lookupParams = { countryCode: process.env.DEFAULT_COUNTRY_CODE || undefined };
// Use Promise-based syntax for clarity
const lookupResult = await new Promise<LookupResponse>((resolve, reject) => {
messagebird.lookup.read(phoneNumber, lookupParams, (err, response) => {
if (err) {
// Specific handling for common parse error
if (err.errors && err.errors[0].code === 21) {
return reject(new Error('Invalid phone number format. Please include country code if necessary.'));
}
console.error(""MessageBird Lookup Error:"", err);
return reject(new Error('Failed to validate phone number.'));
}
if (!response) {
return reject(new Error('Received empty response from phone number validation.'));
}
// Ensure it's a mobile number capable of receiving SMS
if (response.type !== 'mobile') {
return reject(new Error('Only mobile phone numbers are supported for SMS reminders.'));
}
resolve(response);
});
});
// Use the internationally formatted number returned by MessageBird
validatedPhoneNumber = lookupResult.phoneNumber;
} catch (error: any) {
console.error(""Lookup validation failed:"", error);
return NextResponse.json({ success: false, error: error.message || 'Phone number validation failed.' }, { status: 400 });
}
// --- 3. Calculate Reminder Time (in UTC for MessageBird) ---
const reminderMoment = appointmentMoment.clone().subtract(REMINDER_HOURS_BEFORE, 'hours');
// MessageBird expects ISO 8601 format (UTC implicitly handled by format())
const scheduledDateTime = reminderMoment.format(); // e.g., ""2025-12-31T17:00:00Z""
// --- 4. Schedule SMS via MessageBird ---
try {
const messageBody = `${name}, here's a reminder for your ${treatment} appointment at ${appointmentMoment.format('HH:mm')} (${timeZone}). See you soon!`;
const originator = process.env.MESSAGEBIRD_ORIGINATOR || 'Reminder'; // Fallback originator
const messageBirdResponse = await new Promise<MessagesResponse>((resolve, reject) => {
messagebird.messages.create({
originator: originator,
recipients: [validatedPhoneNumber],
scheduledDatetime: scheduledDateTime,
body: messageBody,
}, (err, response) => {
if (err) {
console.error(""MessageBird Scheduling Error:"", err);
// Provide more specific error if possible
const errMsg = err.errors?.[0]?.description || 'Failed to schedule SMS reminder.';
return reject(new Error(errMsg));
}
if (!response) {
return reject(new Error('Received empty response from MessageBird scheduling.'));
}
console.log('MessageBird Scheduling Response:', response);
resolve(response);
});
});
// Store the message ID in case we need to cancel it if DB save fails
messageBirdMessageId = messageBirdResponse?.id;
} catch (error: any) {
console.error(""Scheduling failed:"", error);
return NextResponse.json({ success: false, error: error.message || 'Failed to schedule reminder.' }, { status: 500 });
}
// --- 5. Store Appointment in Database ---
try {
const newAppointment = await prisma.appointment.create({
data: {
name,
phoneNumber: validatedPhoneNumber, // Store normalized number
treatment,
appointmentTime: appointmentMoment.toDate(), // Store as Date object (UTC)
timeZone: timeZone,
messageBirdId: messageBirdMessageId, // Store the MessageBird message ID
reminderSentAt: reminderMoment.toDate(), // Store scheduled time
},
});
console.log('Appointment saved:', newAppointment.id);
// --- 6. Send Success Response ---
return NextResponse.json({ success: true, appointmentId: newAppointment.id });
} catch (dbError) {
console.error('Database save error:', dbError);
// Important Consideration: If the database save fails *after* the MessageBird message was successfully scheduled,
// the user might receive a reminder for an appointment that doesn't exist in our system.
// Handling this requires a compensating action: attempting to cancel the scheduled message using the MessageBird API.
// This adds complexity, as you'd need the `messageBirdMessageId` (stored above) and make a
// `messagebird.messages.delete(id)` call within this catch block, potentially with its own error handling.
// For simplicity, this example doesn't implement cancellation, but it's crucial for production robustness.
console.error(`CRITICAL: DB save failed but MessageBird message ${messageBirdMessageId} might be scheduled! Manual cancellation might be needed.`);
return NextResponse.json({ success: false, error: 'Failed to save appointment details after scheduling reminder.' }, { status: 500 });
}
} catch (error: any) {
console.error('General API error:', error);
return NextResponse.json({ success: false, error: 'An unexpected server error occurred.' }, { status: 500 });
}
}
Key points in the API route:
- Validation: Rigorous checks on all inputs, including date/time validity, future booking constraints, and timezone existence.
- MessageBird Lookup: Uses
messagebird.lookup.read
to validate the phone number format and type (must be mobile). It handles specific errors (like code 21 for invalid format) and uses the normalizedphoneNumber
returned by the API for consistency. We wrap the callback in a Promise for cleaner async/await usage. - Time Calculation: Uses
moment-timezone
to correctly parse the input date/time in the user's specified timezone. It then calculates the reminder time, also considering the timezone, and formats it into the ISO 8601 string required by MessageBird'sscheduledDatetime
. Moment's.format()
without arguments outputs ISO 8601 in UTC, which is perfect. - MessageBird Scheduling: Calls
messagebird.messages.create
with theoriginator
,recipients
(using the validated number),scheduledDatetime
, and messagebody
. Again, a Promise wrapper is used. The MessageBird message ID is stored. - Database Storage: If scheduling succeeds, uses Prisma (
prisma.appointment.create
) to save the appointment details, including the UTCappointmentTime
, thetimeZone
, and themessageBirdId
. - Error Handling: Uses
try...catch
blocks extensively. Critically, it logs a warning if the database save fails after scheduling the message, highlighting the need for potential manual intervention or implementation of the cancellation logic. Returns informative JSON error responses with appropriate HTTP status codes.
4. Integrating with Third-Party Services (MessageBird Details)
We've already integrated the MessageBird SDK, but let's detail the configuration aspects:
-
API Key (
MESSAGEBIRD_API_KEY
):- Purpose: Authenticates your application's requests to the MessageBird API.
- How to Obtain:
- Log in to your MessageBird Dashboard.
- Navigate to the ""Developers"" section in the left-hand menu.
- Click on ""API access"".
- You'll see your Live API Key. Copy this key. (You can also create restricted keys for better security if needed).
- Paste this key as the value for
MESSAGEBIRD_API_KEY
in your.env
file.
- Security: Keep this key confidential. Do not commit it to version control. Use environment variables.
-
Originator (
MESSAGEBIRD_ORIGINATOR
):- Purpose: The sender ID displayed to the recipient of the SMS.
- Configuration: Set this in your
.env
file. - Options & Restrictions:
- Alphanumeric: (e.g.,
""BeautyBird""
,""MyClinic""
). Max 11 characters. Not supported in all countries (notably the US, Canada). If used where unsupported, messages may fail or be assigned a random shared number. - Numeric (E.164 Format): A phone number purchased through MessageBird (e.g.,
+12015550123
). Required for sending to countries like the US. Ensure the number is SMS-enabled.
- Alphanumeric: (e.g.,
- Recommendation: Check MessageBird's documentation for sender ID restrictions in your target countries. Use a purchased number for maximum compatibility, especially if targeting North America.
-
Default Country Code (
DEFAULT_COUNTRY_CODE
):- Purpose: Assists the Lookup API in parsing phone numbers entered without an international dialing prefix (e.g., if a US user enters
""555-123-4567""
instead of""+15551234567""
). - How to Obtain: Use the standard ISO 3166-1 alpha-2 code for your primary target country (e.g.,
""US""
,""GB""
,""DE""
,""NL""
). - Configuration: Set in
.env
. The API route code uses this value when callingmessagebird.lookup.read
.
- Purpose: Assists the Lookup API in parsing phone numbers entered without an international dialing prefix (e.g., if a US user enters
-
Fallback Mechanisms:
- The current implementation doesn't have explicit fallbacks for MessageBird outages. For critical systems, you might consider:
- Implementing retry logic (see next section) for transient API errors.
- Potentially integrating a secondary SMS provider as a backup (more complex).
- Monitoring MessageBird's status page and implementing alerting.
- The current implementation doesn't have explicit fallbacks for MessageBird outages. For critical systems, you might consider:
5. Error Handling, Logging, and Retry Mechanisms
Robust error handling is crucial for a production application.
- Consistent Strategy: Our API route uses
try...catch
blocks for different logical steps (validation, lookup, scheduling, DB save). Errors are caught, logged to the console (usingconsole.error
), and a standardized JSON response{ success: false, error: '...' }
is returned with an appropriate HTTP status code (400 for client errors, 500 for server errors). The critical failure point (DB save after scheduling) is specifically noted in logs. - Logging:
- We use
console.log
for informational messages (e.g., successful scheduling/saving) andconsole.error
for errors, including the error object itself for detailed stack traces during development. - Production Logging: For production, replace
console.log
/console.error
with a dedicated logging library (e.g.,pino
,winston
). Configure it to:- Output structured logs (JSON format).
- Set appropriate log levels (e.g., INFO, WARN, ERROR).
- Send logs to a persistent storage or log management service (e.g., Datadog, Logtail, AWS CloudWatch Logs).
- We use
- Retry Mechanisms:
- MessageBird API Calls: The MessageBird SDK itself does not automatically retry requests. For transient network issues or temporary MessageBird API hiccups (e.g., 5xx errors), you could implement a retry strategy around the
messagebird.lookup.read
andmessagebird.messages.create
calls.- Example (Conceptual): Wrap the Promise-based calls in a function that catches specific errors (like network errors or 503 Service Unavailable) and retries a few times with exponential backoff. Libraries like
async-retry
can simplify this.
- Example (Conceptual): Wrap the Promise-based calls in a function that catches specific errors (like network errors or 503 Service Unavailable) and retries a few times with exponential backoff. Libraries like
- MessageBird API Calls: The MessageBird SDK itself does not automatically retry requests. For transient network issues or temporary MessageBird API hiccups (e.g., 5xx errors), you could implement a retry strategy around the