Build a Node.js Fastify app for bulk SMS messaging with MessageBird
This guide provides a complete walkthrough for building a production-ready Node.js application using the Fastify framework to send bulk or broadcast SMS messages via the MessageBird API. We'll cover everything from project setup and core logic to asynchronous processing, error handling, security, and deployment.
You'll learn how to create a system that can efficiently handle sending potentially large volumes of SMS messages without blocking your main application thread, incorporating best practices for reliability and scalability.
Technologies Used:
- Node.js: The JavaScript runtime environment.
- Fastify: A high-performance, low-overhead web framework for Node.js. Chosen for its speed, extensibility, and developer-friendly features like built-in validation and logging.
- MessageBird: The communication platform-as-a-service (CPaaS) provider used for sending SMS messages via their API.
- MessageBird Node.js SDK (@messagebird/api): Simplifies interaction with the MessageBird REST API.
- Redis: An in-memory data structure store, used here as a message broker for background job processing.
- BullMQ: A robust Node.js library for creating and processing background jobs using Redis. Essential for handling bulk operations asynchronously.
- Prisma: A next-generation ORM for Node.js and TypeScript, used for database interaction (optional but recommended for tracking).
- PostgreSQL: A powerful, open-source relational database (optional, for use with Prisma).
- dotenv: A module to load environment variables from a
.env
file.
System Architecture:
Here’s a high-level overview of how the components interact:
+-----------------+ +-----------------+ +-----------------+ +-------------------+ +------------------+
| Client (e.g. UI)| ---> | Fastify API | ---> | Redis (BullMQ) | ---> | Background Worker | ---> | MessageBird API |
| | | (Accepts Request)| | (Job Queue) | | (Processes Jobs) | | (Sends SMS) |
+-----------------+ +-----------------+ +-----------------+ +-------------------+ +------------------+
| | | ^
| | Optional: Write Job Status | | Optional: Read Job Status
| V V |
+----------------------|-------------------------------------------------|---------+
| +-----------------+
+----------------> | Database (PG) |
| (Tracks Messages)|
+-----------------+
Final Outcome:
By the end of this guide, you will have:
- A Fastify API endpoint capable of accepting bulk SMS requests.
- An asynchronous job queue system (BullMQ + Redis) to process messages in the background.
- A background worker process that uses the MessageBird SDK to send SMS messages reliably.
- (Optional) Database integration with Prisma to track the status of each message.
- Configuration for error handling, retries, security, and deployment.
Prerequisites:
- Node.js: v18 or later installed.
- npm or yarn: Package manager for Node.js.
- MessageBird Account: Sign up for free at MessageBird.
- MessageBird API Access Key: Obtainable from your MessageBird Dashboard (Developers -> API access).
- Redis Server: Running locally or accessible remotely (e.g., via cloud provider or Docker).
- (Optional) PostgreSQL Database: Running locally or accessible remotely.
- Basic understanding of Node.js, APIs, and asynchronous programming.
- A verified phone number or purchased virtual number in MessageBird to use as the
originator
.
1. Setting up the project
Let's initialize our Node.js project and install the necessary dependencies.
1.1 Create Project Directory and Initialize
Open your terminal and create a new directory for your project, then navigate into it.
mkdir fastify-messagebird-bulk
cd fastify-messagebird-bulk
Initialize the Node.js project using npm (or yarn):
npm init -y
This creates a package.json
file.
1.2 Install Dependencies
Install Fastify, the MessageBird SDK, BullMQ, Redis client, dotenv, and Prisma (if using the database tracking).
# Core dependencies
npm install fastify @messagebird/api bullmq ioredis dotenv
# Optional: Prisma for database tracking
npm install prisma @prisma/client pg --save-dev
# Development dependencies (optional but recommended)
npm install -D nodemon concurrently
fastify
: The web framework.@messagebird/api
: The official MessageBird Node.js SDK.bullmq
: The job queue library.ioredis
: A robust Redis client needed by BullMQ.dotenv
: Loads environment variables from a.env
file.prisma
,@prisma/client
,pg
: For database interaction (ORM, client, PostgreSQL driver).nodemon
: Automatically restarts the server during development on file changes.concurrently
: Runs multiple commands concurrently (useful for running the API and worker).
1.3 Project Structure
Create the following directory structure within your project root:
fastify-messagebird-bulk/
├── prisma/ # (Optional) Prisma schema and migrations
├── src/
│ ├── config/ # Configuration files (e.g., MessageBird, Redis)
│ ├── jobs/ # BullMQ job definitions and processors
│ ├── routes/ # Fastify API route definitions
│ ├── services/ # Business logic (e.g., MessageBird service wrapper)
│ ├── utils/ # Utility functions
│ ├── api.js # Fastify server entry point
│ └── worker.js # BullMQ worker entry point
├── .env # Environment variables (DO NOT COMMIT)
├── .env.example # Example environment variables
├── .gitignore
└── package.json
1.4 Configure Environment Variables
Create a .env
file in the project root. This file will hold your sensitive credentials and configuration. Never commit this file to version control.
Also, create a .env.example
file to show the required variables.
.env.example
:
# MessageBird Configuration
MESSAGEBIRD_API_KEY=YOUR_MESSAGEBIRD_LIVE_ACCESS_KEY
MESSAGEBIRD_ORIGINATOR=YOUR_SENDER_ID_OR_NUMBER # e.g., +12025550147 or "MyCompany"
# Redis Configuration (for BullMQ)
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
# REDIS_PASSWORD=your_redis_password # Uncomment if your Redis requires auth
# REDIS_URL=redis://user:password@host:port # Alternative connection string
# Database Configuration (Optional - for Prisma)
# Example for PostgreSQL
DATABASE_URL="postgresql://user:password@host:port/database_name?schema=public"
# Application Configuration
API_PORT=3000
API_HOST=0.0.0.0 # Listen on all available network interfaces
QUEUE_NAME=sms_sending_queue
Copy .env.example
to .env
and fill in your actual credentials:
cp .env.example .env
MESSAGEBIRD_API_KEY
: Your live API access key from the MessageBird dashboard (Developers -> API access -> Show key).MESSAGEBIRD_ORIGINATOR
: The sender ID for your SMS messages. This can be:- A purchased virtual mobile number (VMN) from MessageBird (e.g.,
+12025550147
). - An Alphanumeric Sender ID (e.g.,
MyCompany
, max 11 chars). Note: Alphanumeric IDs are not supported in all countries (e.g., the US) and cannot receive replies. Check MessageBird's country restrictions. Use a number for broader compatibility.
- A purchased virtual mobile number (VMN) from MessageBird (e.g.,
REDIS_HOST
,REDIS_PORT
,REDIS_PASSWORD
/REDIS_URL
: Connection details for your Redis server.DATABASE_URL
: (Optional) Connection string for your PostgreSQL database if using Prisma. Follow the format specified in the Prisma documentation for your specific database.API_PORT
,API_HOST
: Network configuration for the Fastify server.QUEUE_NAME
: A name for the BullMQ job queue.
.gitignore
1.5 Configure Create a .gitignore
file in the root to prevent sensitive files and unnecessary folders from being committed:
.gitignore
:
# Dependencies
/node_modules
# Environment Variables
.env
# Prisma
# Prisma migration files (*.sql within migration folders) define schema changes and should be committed.
# Ignore SQLite files if used during development
*.db
*.db-journal
# Logs
*.log
# Build Output (if any)
/dist
# OS generated files
.DS_Store
Thumbs.db
1.6 (Optional) Initialize Prisma
If you're using the database for tracking, initialize Prisma:
npx prisma init --datasource-provider postgresql
This creates the prisma/
directory and a schema.prisma
file. Update prisma/schema.prisma
(covered later in Section 6).
package.json
Scripts
1.7 Configure Add scripts to your package.json
for easier development and execution:
package.json
(add/update the scripts
section):
{
// ... other package.json content
"scripts": {
"start:api": "node src/api.js",
"start:worker": "node src/worker.js",
"dev:api": "nodemon src/api.js",
"dev:worker": "nodemon src/worker.js",
"dev": "concurrently \"npm:dev:api\" \"npm:dev:worker\"",
"db:migrate": "npx prisma migrate dev --name init",
"db:studio": "npx prisma studio"
}
// ... rest of package.json
}
start:api
/start:worker
: Run the API server and worker in production.dev:api
/dev:worker
: Run the API server and worker usingnodemon
for development (auto-restarts on changes).dev
: Runs both the API and worker concurrently for development usingconcurrently
.db:migrate
,db:studio
: (Optional) Prisma commands for database migrations and GUI.
Your basic project structure and configuration are now complete.
2. Implementing core functionality
Now, let's implement the core logic for sending messages via MessageBird and setting up the asynchronous queue.
2.1 Configure MessageBird Service
Create a service file to encapsulate MessageBird interactions.
src/services/messagebirdService.js
:
const messagebirdClient = require('@messagebird/api');
const dotenv = require('dotenv');
dotenv.config(); // Load .env variables
const apiKey = process.env.MESSAGEBIRD_API_KEY;
const originator = process.env.MESSAGEBIRD_ORIGINATOR;
if (!apiKey) {
console.error('ERROR: MESSAGEBIRD_API_KEY environment variable not set.');
process.exit(1); // Exit if key is missing
}
if (!originator) {
console.warn('WARN: MESSAGEBIRD_ORIGINATOR environment variable not set. Using default or MessageBird assigned number might occur.');
// Depending on MessageBird's behavior, this might still work if you have a default number,
// but explicitly setting it is highly recommended.
}
let messagebird;
try {
// Initialize using the API key directly
messagebird = messagebirdClient(apiKey);
console.log('MessageBird SDK initialized successfully.');
} catch (error) {
console.error('ERROR: Failed to initialize MessageBird SDK:', error);
process.exit(1);
}
/**
* Sends a single SMS message using the MessageBird API.
* Note: The new SDK might return Promises directly, simplifying this.
* Check SDK documentation for the exact signature of messages.create.
* Assuming a callback pattern for consistency with the original article for now.
*
* @param {string} recipient - The recipient's phone number (E.164 format, e.g., +12025550147).
* @param {string} body - The text message content.
* @returns {Promise<object>} - A promise that resolves with the MessageBird API response.
* @throws {Error} - Throws an error if the API call fails.
*/
const sendSms = async (recipient, body) => {
if (!messagebird) {
throw new Error('MessageBird SDK is not initialized.');
}
if (!recipient || !body) {
throw new Error('Recipient and body are required for sending SMS.');
}
const params = {
originator: originator, // Use the configured originator
recipients: [recipient], // API expects an array
body: body,
// You can add more parameters here if needed, e.g., scheduledAt, reportUrl
// See: https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms
};
console.log(`Attempting to send SMS to ${recipient} via MessageBird...`);
// Using Promise wrapper for the callback pattern shown in original article.
// If the new SDK returns a promise directly, this can be simplified:
// try {
// const response = await messagebird.messages.create(params);
// console.log(`SUCCESS: MessageBird API response for ${recipient}:`, JSON.stringify(response, null, 2));
// return response;
// } catch (err) {
// console.error(`ERROR sending SMS to ${recipient}:`, JSON.stringify(err, null, 2));
// throw {
// message: `Failed to send SMS via MessageBird to ${recipient}`,
// details: err.errors || [err]
// };
// }
return new Promise((resolve, reject) => {
messagebird.messages.create(params, (err, response) => {
if (err) {
// Log detailed error information from MessageBird
console.error(`ERROR sending SMS to ${recipient}:`, JSON.stringify(err, null, 2));
// Reject the promise with a structured error
reject({
message: `Failed to send SMS via MessageBird to ${recipient}`,
details: err.errors || [err] // Normalize error structure
});
} else {
console.log(`SUCCESS: MessageBird API response for ${recipient}:`, JSON.stringify(response, null, 2));
// Potentially check response status here if needed
resolve(response); // Resolve with the successful response
}
});
});
};
module.exports = {
sendSms,
messagebird // Export client if needed elsewhere (e.g., for balance checks)
};
- Initialization: Loads the API key and originator from environment variables and initializes the MessageBird client using the
@messagebird/api
package style. Includes error handling for missing keys or initialization failures. sendSms
function:- Takes
recipient
andbody
as arguments. - Constructs the
params
object required bymessagebird.messages.create
. - Uses a
Promise
to handle the asynchronous nature (assuming the SDK might still use callbacks, or providing compatibility with the original code). Includes comments on how to simplify if the new SDK returns promises directly. - Logs attempts, successes, and failures with detailed error information.
- Rejects the promise with a structured error object for better upstream handling.
- Takes
2.2 Configure Redis and BullMQ
Set up the connection to Redis and define the BullMQ queue.
src/config/redis.js
:
const Redis = require('ioredis');
const dotenv = require('dotenv');
dotenv.config();
const redisConfig = {
host: process.env.REDIS_HOST || '127.0.0.1',
port: process.env.REDIS_PORT || 6379,
// password: process.env.REDIS_PASSWORD, // Uncomment if needed
maxRetriesPerRequest: null, // Recommended by BullMQ docs
};
// Support for Redis URL connection string
const redisUrl = process.env.REDIS_URL;
let connection;
if (redisUrl) {
connection = new Redis(redisUrl, { maxRetriesPerRequest: null });
console.log(`Connecting to Redis using URL: ${redisUrl.split('@')[1] || redisUrl}`); // Avoid logging password
} else {
connection = new Redis(redisConfig);
console.log(`Connecting to Redis at ${redisConfig.host}:${redisConfig.port}`);
}
connection.on('connect', () => {
console.log('Successfully connected to Redis.');
});
connection.on('error', (err) => {
console.error('Redis connection error:', err);
// Consider exiting the process if Redis connection is critical and fails initially
// process.exit(1);
});
module.exports = connection;
- Loads Redis connection details from
.env
. - Provides flexibility for connecting via host/port or a
REDIS_URL
. - Includes basic connection logging and error handling.
src/jobs/smsQueue.js
:
const { Queue } = require('bullmq');
const redisConnection = require('../config/redis');
const dotenv = require('dotenv');
dotenv.config();
const QUEUE_NAME = process.env.QUEUE_NAME || 'sms_sending_queue';
// Create a new Queue instance
// It's recommended to reuse the connection object
const smsQueue = new Queue(QUEUE_NAME, {
connection: redisConnection,
defaultJobOptions: {
attempts: 3, // Number of times to retry a failed job
backoff: {
type: 'exponential', // Exponential backoff strategy
delay: 1000, // Initial delay in ms (1 second)
},
removeOnComplete: true, // Automatically remove job after completion
removeOnFail: false, // Keep failed jobs for inspection (set to true or a number of jobs to keep if preferred)
},
});
console.log(`BullMQ queue ""${QUEUE_NAME}"" initialized.`);
// Optional: Listen for queue events globally if needed (e.g., for monitoring)
smsQueue.on('error', (error) => {
console.error(`Queue ""${QUEUE_NAME}"" error:`, error);
});
smsQueue.on('waiting', (jobId) => {
// console.log(`Job ${jobId} is waiting in queue ""${QUEUE_NAME}"".`);
});
smsQueue.on('active', (job) => {
// console.log(`Job ${job.id} has started processing in queue ""${QUEUE_NAME}"".`);
});
smsQueue.on('completed', (job, result) => {
// console.log(`Job ${job.id} completed successfully in queue ""${QUEUE_NAME}"". Result:`, result);
});
smsQueue.on('failed', (job, err) => {
console.error(`Job ${job.id} failed in queue ""${QUEUE_NAME}"". Attempt ${job.attemptsMade}/${job.opts.attempts}. Error:`, err);
});
/**
* Adds a job to send a single SMS message to the queue.
* @param {string} recipient - The recipient's phone number.
* @param {string} body - The message content.
* @returns {Promise<import('bullmq').Job>} - A promise that resolves with the added job.
*/
const addSmsJob = async (recipient, body) => {
if (!recipient || !body) {
throw new Error('Recipient and body are required to add SMS job.');
}
console.log(`Adding SMS job to queue for recipient: ${recipient}`);
// Job data should be serializable (plain objects, strings, numbers)
const jobData = { recipient, body };
// Job name is optional but useful for categorizing/identifying jobs
const jobName = 'send-single-sms';
try {
const job = await smsQueue.add(jobName, jobData);
console.log(`Job ${job.id} added to queue ""${QUEUE_NAME}"" for recipient ${recipient}.`);
return job;
} catch (error) {
console.error(`Error adding job to queue ""${QUEUE_NAME}"":`, error);
throw error; // Re-throw to be handled by the caller (e.g., API route)
}
};
module.exports = {
smsQueue,
addSmsJob,
QUEUE_NAME
};
- Initialization: Creates a
Queue
instance, connecting it to Redis via the shared connection. defaultJobOptions
: Configures crucial settings:attempts
,backoff
,removeOnComplete
/removeOnFail
.- Queue Events: Includes listeners for various queue events for logging and monitoring.
addSmsJob
function: Adds a new job to the queue with recipient and body data.
2.3 Create the Background Worker
This process listens to the queue and executes the jobs.
src/worker.js
:
const { Worker } = require('bullmq');
const redisConnection = require('./config/redis');
const { QUEUE_NAME } = require('./jobs/smsQueue');
const { sendSms } = require('./services/messagebirdService');
// Optional: Prisma client for database updates
// const { PrismaClient } = require('@prisma/client');
// const prisma = new PrismaClient();
console.log(`Starting worker for queue ""${QUEUE_NAME}""...`);
// Define the processing function for jobs
const processSmsJob = async (job) => {
const { recipient, body } = job.data; // Extract data added by addSmsJob
console.log(`Processing job ${job.id} (Attempt ${job.attemptsMade + 1}/${job.opts.attempts}): Send SMS to ${recipient}`);
try {
// Optional: Update database status to 'processing' before sending
// await prisma.messageLog.update({ where: { jobId: job.id }, data: { status: 'processing' } });
const result = await sendSms(recipient, body);
// Optional: Update database status to 'sent' on success
// await prisma.messageLog.update({ where: { jobId: job.id }, data: { status: 'sent', messageBirdId: result.id /* Store MB ID if available */ } });
console.log(`Job ${job.id} completed successfully for ${recipient}. MessageBird Response ID: ${result?.id || 'N/A'}`);
return { messageBirdId: result?.id, status: 'sent' }; // Return value stored if job completes
} catch (error) {
console.error(`Job ${job.id} failed for recipient ${recipient}:`, error.message || error);
// Optional: Update database status to 'failed'
// await prisma.messageLog.update({
// where: { jobId: job.id },
// data: { status: 'failed', failureReason: JSON.stringify(error.details || error.message) }
// });
// IMPORTANT: Throw the error again to signal BullMQ that the job failed
// This allows BullMQ to handle retries based on the queue's configuration.
throw error;
}
};
// Create a new Worker instance
const worker = new Worker(QUEUE_NAME, processSmsJob, {
connection: redisConnection,
concurrency: 5, // Process up to 5 jobs concurrently (adjust based on resources/API limits)
limiter: { // Optional: Rate limiting to avoid hitting MessageBird limits
max: 10, // Max 10 jobs
duration: 1000 // per 1000 ms (1 second) -> Adjust based on MessageBird's rate limits for your account/channel
}
});
// --- Worker Event Listeners (Similar to Queue, but specific to this worker instance) ---
worker.on('completed', (job, returnValue) => {
// console.log(`Worker completed job ${job.id} for queue ""${QUEUE_NAME}"". Return value:`, returnValue);
});
worker.on('failed', (job, err) => {
console.error(`Worker failed job ${job.id} for queue ""${QUEUE_NAME}"" after ${job.attemptsMade} attempts. Error:`, err.message || err);
});
worker.on('error', (err) => {
console.error(`Worker error for queue ""${QUEUE_NAME}"":`, err);
});
worker.on('active', (job) => {
// console.log(`Worker started processing job ${job.id} for queue ""${QUEUE_NAME}"".`);
});
worker.on('progress', (job, progress) => {
// If your job reports progress
// console.log(`Worker progress for job ${job.id}:`, progress);
});
console.log(`Worker listening for jobs on queue ""${QUEUE_NAME}"" with concurrency ${worker.opts.concurrency}.`);
// Graceful shutdown
const gracefulShutdown = async (signal) => {
console.log(`\nReceived ${signal}. Closing worker and Redis connection...`);
await worker.close();
await redisConnection.quit();
// Optional: Close database connection
// await prisma.$disconnect();
console.log('Worker and connections closed gracefully.');
process.exit(0);
};
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Catches Ctrl+C
- Initialization: Creates a
Worker
instance listening to the queue. processSmsJob
Function: The core job execution logic, callingsendSms
. Handles errors and re-throws them for BullMQ retries. Includes optional Prisma integration points.- Concurrency & Rate Limiting: Configures how many jobs run in parallel and applies rate limiting for MessageBird API calls.
- Event Listeners: Logs worker events.
- Graceful Shutdown: Ensures clean shutdown on termination signals.
3. Building the Fastify API layer
Let's create the Fastify server and the API endpoint to receive bulk SMS requests.
src/api.js
:
const Fastify = require('fastify');
const dotenv = require('dotenv');
const { addSmsJob, QUEUE_NAME } = require('./jobs/smsQueue');
// Optional: Prisma client for database interaction
// const { PrismaClient } = require('@prisma/client');
// const prisma = new PrismaClient();
dotenv.config();
const server = Fastify({
logger: true // Enable built-in Pino logger (good for production)
});
// --- Request Validation Schema ---
const bulkSmsSchema = {
body: {
type: 'object',
required: ['recipients', 'body'],
properties: {
recipients: {
type: 'array',
minItems: 1,
items: {
type: 'string',
// Basic E.164 format regex (ensures +country code and digits, matches entire string)
pattern: '^\\+[1-9]\\d{1,14}$'
},
description: 'An array of recipient phone numbers in E.164 format (e.g., +12025550147).'
},
body: {
type: 'string',
minLength: 1,
maxLength: 1600, // Allow longer messages (MessageBird handles splitting)
description: 'The text content of the SMS message.'
},
// Optional: Add a correlation ID for tracking batches
correlationId: {
type: 'string',
description: 'Optional identifier to correlate this batch request.'
}
},
additionalProperties: false
},
response: { // Define expected success response structure
202: { // Use 202 Accepted as processing is asynchronous
type: 'object',
properties: {
message: { type: 'string' },
jobCount: { type: 'integer' },
correlationId: { type: 'string', nullable: true }, // Reflect back correlationId if provided
queueName: { type: 'string' }
}
}
}
};
// --- API Route ---
server.post('/send-bulk', { schema: bulkSmsSchema }, async (request, reply) => {
const { recipients, body, correlationId } = request.body;
const addedJobs = [];
const failedAdds = [];
request.log.info(`Received bulk SMS request. CorrelationID: ${correlationId || 'N/A'}, Recipients: ${recipients.length}, Body: "${body.substring(0, 50)}..."`);
try {
// (Optional) Record the initial batch request in the database if needed
// Add a job to the queue for each recipient
for (const recipient of recipients) {
try {
// Optional: Create initial DB record before adding job
// const dbRecord = await prisma.messageLog.create({
// data: {
// recipient: recipient,
// body: body,
// status: 'pending', // Initial status
// correlationId: correlationId,
// // jobId will be added after job creation if needed
// }
// });
const job = await addSmsJob(recipient, body);
addedJobs.push({ recipient: recipient, jobId: job.id });
// Optional: Update DB record with the jobId
// await prisma.messageLog.update({
// where: { id: dbRecord.id },
// data: { jobId: job.id }
// });
} catch (queueError) {
request.log.error(`Failed to add job for recipient ${recipient}:`, queueError);
failedAdds.push({ recipient: recipient, error: queueError.message });
// Decide if you want to stop the whole batch or continue
// For robustness, we continue adding other jobs here.
}
}
if (failedAdds.length > 0) {
// Partial success - some jobs added, some failed to queue
// Log this clearly, maybe return a specific status or detailed error
request.log.warn(`Partial success: ${addedJobs.length} jobs added, ${failedAdds.length} failed to queue.`);
// Decide on the response - here we still acknowledge the queued ones
}
// Use 202 Accepted: Request received, processing will happen asynchronously.
reply.code(202).send({
message: `Accepted ${addedJobs.length} SMS messages for background processing.`,
jobCount: addedJobs.length,
correlationId: correlationId || null, // Echo back ID or null
queueName: QUEUE_NAME
});
} catch (error) {
request.log.error('Error processing bulk SMS request:', error);
// General server error if something unexpected happens before/during looping
reply.code(500).send({ error: 'Internal Server Error', message: 'Failed to process the bulk SMS request.' });
}
});
// --- Health Check Route ---
server.get('/health', async (request, reply) => {
// Basic health check - can be expanded (see Section 10.1)
return { status: 'ok', timestamp: new Date().toISOString() };
});
// --- Start Server ---
const start = async () => {
try {
const port = process.env.API_PORT || 3000;
const host = process.env.API_HOST || '0.0.0.0';
await server.listen({ port: port, host: host });
server.log.info(`API server listening on ${server.server.address().address}:${server.server.address().port}`);
} catch (err) {
server.log.error(err);
// Optional: Disconnect Prisma on error
// await prisma.$disconnect();
process.exit(1);
}
};
start();
- Fastify Instance: Creates server with logging.
- Validation Schema (
bulkSmsSchema
): Defines request body structure, including updated E.164 regex (^\\+[1-9]\\d{1,14}$
). Defines202 Accepted
response schema. /send-bulk
Route: Validates input, iterates recipients, callsaddSmsJob
for each, handles queuing errors, returns202 Accepted
. Includes optional Prisma integration points./health
Route: Basic health check endpoint.- Server Start: Standard Fastify server startup logic.
4. Integrating with MessageBird (Covered in Section 2.1)
The core integration with MessageBird happens within the src/services/messagebirdService.js
file, which was created and updated in Section 2.1.
Key Integration Points Recap:
- Initialization:
messagebirdClient(process.env.MESSAGEBIRD_API_KEY)
securely initializes the SDK using the API key from environment variables and the@messagebird/api
package. - Sending:
messagebird.messages.create(params, callback)
(or its promise-based equivalent in the newer SDK) is the method used to send individual SMS messages. ThesendSms
function wraps this logic. - Configuration:
MESSAGEBIRD_ORIGINATOR
environment variable determines the sender ID. - Credentials: API keys are obtained from the MessageBird Dashboard -> Developers -> API access. Ensure you use your LIVE key for actual sending. Store it securely in the
.env
file.
Obtaining API Credentials (Dashboard Navigation):
- Log in to your MessageBird Dashboard.
- Navigate to Developers in the left-hand sidebar.
- Click on API access.
- You will see sections for Live API Key and Test API Key.
- Click Show key next to the Live API Key.
- Copy this key and paste it as the value for
MESSAGEBIRD_API_KEY
in your.env
file. - Important: Keep this key confidential. Do not commit it to version control.
Environment Variable Summary:
MESSAGEBIRD_API_KEY
:- Purpose: Authenticates your application with the MessageBird API.
- Format: A string of alphanumeric characters (e.g.,
live_xxxxxxxxxxxxxxxxxxxxxxx
). - How to Obtain: MessageBird Dashboard -> Developers -> API access -> Live API Key.
MESSAGEBIRD_ORIGINATOR
:- Purpose: Sets the sender ID displayed on the recipient's device.
- Format: Either an international phone number in E.164 format (e.g.,
+12025550147
) or an alphanumeric string (max 11 chars, e.g.,MyCompany
). - How to Obtain/Configure: Use a number purchased/verified in MessageBird or register an alphanumeric sender ID (subject to country restrictions). Set this value in your
.env
file.