Frequently Asked Questions
Build a scalable bulk SMS application using NestJS, Vonage Messages API, and BullMQ. This setup allows you to create a NestJS API endpoint to handle requests, queue messages with BullMQ and Redis, and process them in the background with a worker that interacts with the Vonage API.
BullMQ, a Redis-based message queue, is crucial for handling background job processing, rate limiting, and retries in bulk SMS applications. It decouples the API request from the actual sending, enabling throttling and reliable message delivery even with temporary failures.
A message queue like BullMQ helps manage provider rate limits, ensuring deliverability and graceful failure handling. Without a queue, simply looping through recipients can lead to errors, blocked messages, and an unreliable system. Queues enable asynchronous processing and retries.
Use the Vonage Messages API when you need to send messages across various channels, including SMS, to a large audience. The provided example uses the official `@vonage/server-sdk` to interact with the API, sending text messages within the configured rate limits.
Yes, the example demonstrates basic status tracking using Prisma and PostgreSQL. Individual messages are initially marked as PENDING, then PROCESSING, and finally SENT or FAILED. Vonage can optionally send Delivery Receipts (DLRs) via webhooks to update the message status further.
The example leverages BullMQ's rate limiting feature. The worker processes a configurable number of jobs within a specific timeframe (e.g., one per second), ensuring compliance with Vonage's limits for different number types like long codes.
Prisma simplifies database access, migrations, and type safety with PostgreSQL. It's a modern database toolkit for TypeScript and Node.js that makes it easier to interact with your database and manage its schema.
The example provides a Dockerfile and docker-compose.yml to containerize the NestJS application, PostgreSQL database, and Redis instance. This ensures a consistent development and deployment environment and simplifies setup.
Prerequisites include Node.js (LTS recommended), npm or yarn, Docker and Docker Compose, a Vonage API account, a Vonage phone number capable of sending SMS, and basic understanding of NestJS, TypeScript, APIs, and databases. You'll also need `curl` or Postman for testing.
The application demonstrates error handling and automatic retries. BullMQ automatically requeues failed jobs after a backoff period. If the final attempt fails, the message status is updated to FAILED, and error details are logged.
The API receives recipient lists and message content via POST requests to /broadcasts. It then creates database records and adds individual jobs to the BullMQ queue for each recipient, allowing for asynchronous processing.
Redis serves as the backend for BullMQ, storing the queued SMS sending jobs. It's an in-memory data structure store, providing speed and efficiency for the message queue operations.
The Vonage private key is used to authenticate the application with the Vonage Messages API. The file should be kept secure, mounted into the container read-only, and never committed to version control.
Vonage credentials like API key, secret, application ID, sender ID, and private key path are stored in a .env file. This file should never be committed to version control to protect sensitive information.
Sending SMS messages to a large audience requires careful planning to handle provider rate limits, ensure deliverability, and manage failures gracefully. Simply looping through recipients and calling an API will quickly lead to errors, blocked messages, and an unreliable system.
This guide demonstrates how to build a robust and scalable bulk SMS broadcasting application using NestJS, the Vonage Messages API, and BullMQ for background job processing and rate limiting. By leveraging a message queue, we decouple the initial API request from the actual sending process, allowing us to throttle message dispatch according to Vonage's limitations and retry failed attempts automatically.
Project Goals:
Technologies Used:
@vonage/server-sdk
.System Architecture:
The system follows this general flow:
/broadcasts
) with a list of recipients and the message text.Prerequisites:
curl
or a tool like Postman for testing the API.Final Outcome:
You will have a containerized NestJS application with an API endpoint to initiate bulk SMS broadcasts. Messages will be reliably sent in the background, respecting Vonage rate limits, with status tracking and automatic retries.
1. Setting Up the Project
Let's initialize the NestJS project, set up the directory structure, install dependencies, and configure Docker.
1.1 Initialize NestJS Project
Open your terminal and run the NestJS CLI command:
1.2 Project Structure
We'll organize our code into modules for better separation of concerns:
1.3 Install Dependencies
Install the necessary packages for BullMQ, Vonage, Prisma, configuration, and validation:
1.4 Configure Environment Variables
Create a
.env
file in the project root. This file will store sensitive credentials and configuration. Never commit this file to version control.private.key
file are generated later when creating a Vonage Application specifically for the Messages API. The Sender ID is the Vonage virtual number you'll send messages from.docker-compose.yml
.1.5 Configure Docker
Create a
Dockerfile
in the project root:Create a
docker-compose.yml
file:.env
file to configure containers.private.key
file into the application container (make sure this file exists before runningdocker-compose up
).depends_on
conditions.Create a
.dockerignore
file to optimize build context:1.6 Initialize Prisma
Run the Prisma init command:
This creates a
prisma
directory with aschema.prisma
file and updates.env
with a placeholderDATABASE_URL
. Replace the placeholderDATABASE_URL
in.env
with the one we defined earlier, matching the Docker Compose setup (ensure it's quoted if it contains special characters).Modify
prisma/schema.prisma
later (Section 6) when defining the data model.1.7 Configure NestJS Modules
We need to import and configure the modules we'll be using.
Update
src/app.module.ts
:This root module now:
ConfigModule
..env
.ThrottlerModule
) using Redis for distributed storage.2. Implementing Core Functionality (Queue & Worker)
Now, let's set up BullMQ to handle the SMS jobs and create the worker process that consumes these jobs.
2.1 Configure Queue Module
Create
src/queue/queue.module.ts
:BullModule.registerQueueAsync
: Registers a specific queue namedbroadcast-sms
.defaultJobOptions
: Sets default behavior for jobs added to this queue (attempts, backoff strategy).limiter
: Crucially, this configures BullMQ's built-in rate limiter. It ensures theBroadcastProcessor
will only processBULLMQ_RATE_LIMIT_MAX
jobs withinBULLMQ_RATE_LIMIT_DURATION
milliseconds, effectively throttling calls to the Vonage API.VonageModule
,PrismaModule
, andConfigModule
to make their services injectable into theBroadcastProcessor
.2.2 Create Vonage Service
This service encapsulates interaction with the Vonage SDK.
Create
src/vonage/vonage.service.ts
:Create
src/vonage/vonage.module.ts
:.env
. Includes path resolution and existence check for the private key.sendSms
method that wraps the SDK'smessages.send
call.message_uuid
on success and potential error details on failure.2.3 Create the Queue Processor
This class defines how jobs from the
broadcast-sms
queue are handled.Create
src/queue/broadcast.processor.ts
:@Processor('broadcast-sms')
: Decorator linking this class to the specified queue.process(job: Job)
: The core method called by BullMQ for each job. It receives thejob
object containing our data (recipient
,messageText
,messageId
).VonageService
,PrismaService
, andConfigService
.PENDING
->PROCESSING
->SENT
/FAILED
). Increments attempt count.VonageService
and general processing errors.defaultJobOptions
. Updates DB status accordingly:FAILED
on final failure, keepsPROCESSING
during intermediate retry attempts, storing the latest error.@OnWorkerEvent
) for detailed logging.3. Building the API Layer
This layer exposes an endpoint to receive broadcast requests and add jobs to the queue.
3.1 Create DTO (Data Transfer Object)
DTOs define the expected request body structure and enable automatic validation using
class-validator
.Create
src/broadcast/dto/create-broadcast.dto.ts
:class-validator
to enforce rules (must be an array, not empty, must contain valid phone numbers, message must be a non-empty string).@ApiProperty
is commented out but can be used if Swagger is integrated.3.2 Create Broadcast Service
This service handles the business logic: creating the broadcast record and adding individual message jobs to the queue.
Create
src/broadcast/broadcast.service.ts
:@InjectQueue('broadcast-sms')
: Injects the BullMQ queue instance.PrismaService
.createBroadcast
:Broadcast
record in the database.Message
record (statusPENDING
) in the database for tracking.broadcastQueue
with themessageId
(linking back to the DB record), recipient, and message text.Broadcast
object.getBroadcastStatus
,getMessageStatus
) for querying the state of broadcasts and individual messages from the database.