Frequently Asked Questions
Use a robust queuing system like BullMQ with Redis, along with a framework like Fastify and the Sinch SMS API. This architecture handles individual messages reliably in the background, preventing server overload and ensuring delivery even with temporary failures. The provided example uses a Fastify API endpoint to accept bulk requests and queue individual SMS sending tasks, processed by a dedicated Node.js worker.
Fastify is a high-performance Node.js web framework. It serves as the API layer, receiving bulk SMS requests, validating input, and adding individual message sending jobs to the BullMQ queue. Fastify's speed and efficiency are beneficial for handling a high volume of requests.
A message queue like BullMQ with Redis decouples the API request from the actual SMS sending. This allows the API to respond quickly without waiting for each message to be sent, improving performance and reliability. It also provides retry mechanisms and handles failures gracefully.
PostgreSQL, combined with the Prisma ORM, provides persistent storage for tracking the status of each individual message, the overall batch status, and any errors encountered during the sending process. Prisma simplifies database interaction and ensures type safety.
The system leverages BullMQ's built-in retry mechanism, allowing it to automatically retry failed messages multiple times with exponential backoff. Error logs are also stored to assist with identifying and resolving persistent issues. The system uses a database to track each message's status, supporting better error analysis and recovery.
Redis acts as the backend for the BullMQ job queue, storing the individual SMS sending tasks until the worker process can pick them up and send the messages via the Sinch API. Its in-memory nature ensures fast queue operations.
Yes, the provided system includes a dedicated `/status/:batchId` API endpoint. Clients can use this endpoint to retrieve information about a specific batch of SMS messages, including the overall status and the status of each individual message within the batch.
Use `npm install fastify axios bullmq ioredis dotenv @prisma/client pino-pretty @fastify/rate-limit @fastify/helmet` for production and `npm install --save-dev prisma nodemon` for development dependencies. This installs the web framework, API request library, queueing system, Redis client, and other essentials. You'll also need Docker for local Redis and PostgreSQL setup or access to standalone instances.
You'll need a Sinch account with a Service Plan ID, an API Token, and a configured Sinch virtual number. This information can be obtained from the Sinch Customer Dashboard under SMS -> APIs. The Sinch number should be in E.164 format (e.g. +12xxxxxxxxxx).
Consider using this system for applications requiring large-scale messaging, such as marketing campaigns, important notifications, or alerts. The queueing system and retry logic ensure reliable delivery, essential when reaching a wide audience. Don't use this approach for sending a small number of messages, as a simple API call would be more efficient in that case.
Use the provided `docker-compose.yml` to start PostgreSQL and Redis containers locally. This simplifies the setup process and ensures consistency across development environments. Ensure your `.env` file's connection URLs match the Docker configuration.
The worker process is dedicated to consuming jobs from the Redis queue. It fetches individual SMS sending tasks from the queue, sends the messages via the Sinch API, and updates the database with the status of each message. This asynchronous operation keeps the main API responsive and allows for high throughput.
The system uses the `@fastify/rate-limit` plugin. By default, it limits to 100 requests per minute per IP address using an in-memory store. For scaled environments, a Redis backend is highly recommended for distributed rate limiting. You can configure `REDIS_URL` in the `.env` file.
Prisma is a modern database toolkit that simplifies database operations and provides type safety. It serves as the ORM (Object-Relational Mapper) for interacting with PostgreSQL, managing database migrations, and generating a type-safe client for accessing data.
Sending bulk SMS messages – whether for marketing campaigns, notifications, or alerts – requires more than just a simple loop calling an API. A production-ready system needs to handle potential failures, manage rate limits, provide status tracking, and scale reliably.
This guide details how to build such a system using Node.js with the high-performance Fastify framework, leveraging the Sinch SMS API for message delivery and Redis with BullMQ for robust background job queuing.
What we will build:
Core Technologies:
System Architecture:
Prerequisites:
curl
or a tool like Postman for testing the API.Final Outcome:
By the end of this guide, you will have a scalable and reliable Node.js application capable of accepting requests to send thousands of SMS messages, processing them reliably in the background, handling errors gracefully, and allowing clients to check the status of their bulk sends.
1. Setting up the project
Let's start by initializing our Node.js project and installing the necessary dependencies.
1. Initialize the project:
Open your terminal, create a project directory, and navigate into it.
This creates a basic
package.json
file.2. Install dependencies:
We need Fastify for the web server, Axios for HTTP requests, BullMQ for the queue, ioredis as the Redis client for BullMQ, dotenv for environment variables, Prisma for database interaction, and pino-pretty for human-readable logs during development.
fastify
: The web framework.axios
: To make requests to the Sinch API.bullmq
: The job queue library.ioredis
: Redis client required by BullMQ and potentially rate limiting.dotenv
: To load environment variables from a.env
file.@prisma/client
: Prisma's database client.pino-pretty
: Makes Fastify's logs readable during development.@fastify/rate-limit
: Plugin for API rate limiting.@fastify/helmet
: Plugin for setting security headers.prisma
(dev): The Prisma CLI for migrations and generation.nodemon
(dev): Automatically restarts the server during development when files change.3. Configure Development Scripts:
Open your
package.json
and modify thescripts
section:start
: Runs the main API server for production.dev
: Runs the API server in development mode usingnodemon
andpino-pretty
.worker
: Runs the background job worker for production.dev:worker
: Runs the worker in development mode usingnodemon
.prisma:migrate
: Applies database schema changes.prisma:generate
: Generates the Prisma Client based on your schema.4. Set up Project Structure:
Create the following directory structure within your project root:
prisma/
: Will contain your database schema and migrations.src/
: Contains all the application source code.src/config/
: Configuration files (e.g., queue setup).src/lib/
: Shared libraries/utilities (e.g., Prisma client, Sinch client).src/routes/
: Fastify route handlers.src/workers/
: Background worker logic.src/server.js
: Entry point for the Fastify API server.src/worker.js
: Entry point for the BullMQ worker process..env
: Stores environment variables (API keys, database URLs, etc.)..gitignore
: Specifies files/directories to ignore in Git.docker-compose.yml
: Defines local development services (Postgres, Redis).5. Create
.gitignore
:Create a
.gitignore
file in the project root to avoid committing sensitive information and unnecessary files:6. Set up Environment Variables (
.env
):Create a
.env
file in the project root. This file will hold your secrets and configuration. Never commit this file to version control.How to obtain Sinch Credentials:
+12xxxxxxxxxx
).SINCH_API_BASE_URL
reflects this.7. Set up Local Database and Redis (Using Docker):
For local development, Docker Compose is an excellent way to manage dependencies like Redis and PostgreSQL. Create a
docker-compose.yml
file in the project root:Run
docker-compose up -d
in your terminal to start the database and Redis containers in the background. Ensure your.env
file'sREDIS_URL
andDATABASE_URL
match the credentials and ports defined here (and change the default password!).2. Implementing core functionality (Queuing)
Directly sending SMS messages within the API request handler is inefficient and unreliable for bulk operations. A loop sending messages one by one would block the server, timeout easily, and offer no mechanism for retries or status tracking if the server crashes.
The solution is a background job queue. The API endpoint will quickly validate the request and add individual SMS sending tasks (jobs) to a Redis-backed queue (BullMQ). A separate worker process will then consume these jobs independently.
1. Configure BullMQ Queue:
Create a file to manage the queue instance.
dotenv
.REDIS_URL
using the built-inURL
constructor, which is more robust than basic string splitting and handles complex URLs (with auth, different ports, etc.).Queue
instance, naming it based on the environment variable.defaultJobOptions
:attempts: 3
: If a job fails (e.g., Sinch API is down), BullMQ will retry it up to 3 times.backoff
: Specifies how long to wait between retries.exponential
increases the delay after each failure (1s, 2s, 4s).removeOnComplete
: Cleans up successful jobs from Redis to save space.removeOnFail
: Keeps a history of the last 500 failed jobs for debugging.3. Building the API layer (Fastify)
Now, let's create the Fastify server and the API endpoints.
1. Initialize Prisma:
Run the Prisma init command. This creates the
prisma
directory and a basicschema.prisma
file.Make sure the
url
inprisma/schema.prisma
points to yourDATABASE_URL
environment variable:2. Set up Prisma Client:
Create a singleton instance of the Prisma client to be reused across the application.
3. Create the Sinch Client:
Create a module to encapsulate interactions with the Sinch API.
axios.create
to configure a base URL and default headers (including the crucialAuthorization
bearer token). A timeout is added.sendSms
function takes the recipient, sender number, and message body.SINCH_SERVICE_PLAN_ID
.POST
request with the payload required by the Sinch/batches
endpoint. Note that even for a single message, theto
field expects an array.4. Define API Routes:
Create the route handler for submitting bulk SMS jobs.
smsQueue
andprisma
client.sendBulkSchema
andgetStatusSchema
use Fastify's built-in JSON Schema validation. The regex pattern insendBulkSchema
is corrected to include an end anchor ($
) and remove the erroneous trailing comma./send-bulk
endpoint:batchId
.SmsBatch
record in the database first.recipients
array.jobData
object containing all necessary info (to
,from
,body
,batchId
,jobId
).smsQueue
usingsmsQueue.add()
. We give the job a name ('send-single-sms'
) and pass thejobData
. We also use our generatedjobId
as the BullMQ job ID.Promise.all
waits for all jobs to be added to the queue.202 Accepted
, indicating the task is queued, along with thebatchId
./status/:batchId
endpoint:batchId
from the URL parameters.SmsBatch
and its associatedSmsMessageJob
records.batchId
is not found (404).5. Create the Fastify Server:
Set up the main server entry point.