Building Production-Ready Two-Way SMS with NestJS and MessageBird
This guide provides a step-by-step walkthrough for building a robust two-way SMS messaging system using the NestJS framework and the MessageBird API. We'll create an application capable of receiving incoming SMS messages via MessageBird webhooks and sending replies back to the original sender.
This implementation solves the common need for applications to engage in real-time, bidirectional SMS conversations with users, essential for features like customer support chat, notifications requiring user responses, or interactive SMS campaigns.
Key Technologies:
- NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Its modular architecture and built-in features (like validation pipes and configuration management) accelerate development.
- MessageBird: A communication platform providing APIs for SMS, voice, and chat. We'll use its SMS API and virtual numbers.
- MessageBird Node.js SDK: Simplifies interaction with the MessageBird API (we'll use v10+).
- Tunneling Service (ngrok/localtunnel): Required during development to expose your local NestJS application to the public internet so MessageBird webhooks can reach it.
- (Optional) Database: For persisting message history (e.g., PostgreSQL with TypeORM).
System Architecture:
graph LR
User -- SMS --> MessageBird_VN[MessageBird Virtual Number]
MessageBird_VN -- Configured Flow --> MB_Flow[MessageBird Flow Builder]
MB_Flow -- POST Webhook --> Tunnel[Tunnel (ngrok/localtunnel)]
Tunnel -- Forwards Request --> NestJS_App[NestJS Application (Webhook Endpoint)]
NestJS_App -- Processes & Stores (Optional) --> DB[(Database)]
NestJS_App -- Uses SDK --> MB_API[MessageBird API]
MB_API -- Sends SMS --> User
Prerequisites:
- Node.js (LTS version recommended) and npm/yarn.
- A MessageBird account.
- A purchased MessageBird virtual mobile number (VMN) with SMS capabilities.
- A tunneling tool like
ngrok
orlocaltunnel
installed globally. - Basic understanding of TypeScript and NestJS concepts.
- Access to a terminal or command prompt.
By the end of this guide, you will have a functional NestJS application that:
- Listens for incoming SMS messages on a specific endpoint.
- Validates the incoming webhook payload.
- Logs received messages.
- Sends an automated reply back to the sender using the MessageBird API.
- Includes basic error handling, configuration management, and security considerations.
1. Setting up the Project
Let's initialize our NestJS project and install the necessary dependencies.
-
Install NestJS CLI: If you haven't already, install the NestJS CLI globally.
npm install -g @nestjs/cli # or yarn global add @nestjs/cli
-
Create NestJS Project: Generate a new NestJS project.
nest new messagebird-nestjs-webhook cd messagebird-nestjs-webhook
-
Install Dependencies: We need the MessageBird SDK, NestJS config module, and
dotenv
for environment variable management.npm install messagebird @nestjs/config dotenv class-validator class-transformer # or yarn add messagebird @nestjs/config dotenv class-validator class-transformer
messagebird
: The official Node.js SDK (v10+).@nestjs/config
: For managing environment variables.dotenv
: To load environment variables from a.env
file during development.class-validator
&class-transformer
: For validating incoming webhook data using DTOs.
-
Environment Configuration (
.env
): Create a.env
file in the project root. This file will store sensitive credentials and configuration, and should not be committed to version control (ensure.env
is listed in your.gitignore
file).- Obtain MessageBird API Key:
- Log in to your MessageBird Dashboard.
- Navigate to Developers > API access (REST).
- If you don't have one, click Add access key. Ensure it's a Live key (not Test).
- Copy the generated access key.
- Obtain MessageBird Originator (Virtual Number):
- Navigate to Numbers in your Dashboard.
- Copy the virtual mobile number you purchased (including the
+
and country code, e.g.,+12015550123
).
Add these values to your
.env
file:# .env MESSAGEBIRD_API_KEY=YOUR_LIVE_API_KEY_HERE MESSAGEBIRD_ORIGINATOR=YOUR_VIRTUAL_NUMBER_HERE
- Obtain MessageBird API Key:
-
Configure NestJS ConfigModule: Import and configure the
ConfigModule
in your main application module (src/app.module.ts
) to load environment variables from the.env
file.// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; // Import ConfigModule import { AppController } from './app.controller'; import { AppService } from './app.service'; import { WebhooksModule } from './webhooks/webhooks.module'; // We'll create this next // Import HealthModule if implementing health checks (Section 10) // import { HealthModule } from './health/health.module'; @Module({ imports: [ ConfigModule.forRoot({ // Configure the module isGlobal: true, // Make config available globally envFilePath: '.env', // Specify the env file path }), WebhooksModule, // Import our feature module // HealthModule, // Include HealthModule if using Terminus ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
-
Enable Validation Pipe Globally: To automatically validate incoming request bodies against our DTOs, enable the
ValidationPipe
globally insrc/main.ts
.// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; // Import ValidationPipe import { ConfigService } from '@nestjs/config'; // Import ConfigService async function bootstrap() { const app = await NestFactory.create(AppModule); // Enable global validation pipe app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip properties not defined in DTO. This enhances security // by preventing potential prototype pollution and ensures only // explicitly expected data enters your service layer. transform: true, // Automatically transform payloads to DTO instances })); // Optional: Get port from environment variables or default const configService = app.get(ConfigService); const port = configService.get<number>('PORT', 3000); // Default to 3000 if PORT not set await app.listen(port); console.log(`Application is running on: ${await app.getUrl()}`); } bootstrap();
Now the basic project structure and configuration are in place.
2. Implementing Core Functionality
We'll create a dedicated module, controller, service, and DTO to handle incoming MessageBird webhooks.
-
Generate Module, Controller, and Service: Use the NestJS CLI to scaffold these components within a
webhooks
feature directory.nest g module webhooks nest g controller webhooks/messagebird --flat --no-spec nest g service webhooks/messagebird --flat --no-spec
--flat
: Prevents creating an extra subdirectory for the controller/service files.--no-spec
: Skips generating test files for now (you should add them later).
Ensure the
WebhooksModule
is imported intoAppModule
(as shown in the previous step). -
Create Incoming Message DTO: Define a Data Transfer Object (DTO) to represent the expected payload from the MessageBird SMS webhook and add validation rules using
class-validator
.// src/webhooks/dto/incoming-message.dto.ts import { IsNotEmpty, IsString, IsPhoneNumber } from 'class-validator'; export class IncomingMessageDto { @IsNotEmpty() @IsString() @IsPhoneNumber(null) // Use null for international format validation originator: string; // Sender's phone number (e.g., +14155552671) @IsNotEmpty() @IsString() payload: string; // The content of the SMS message // Note: MessageBird often sends more fields in the webhook payload // (e.g., 'recipient', 'id', 'receivedDatetime'). However, we only define // the fields essential for our core logic here. The `whitelist: true` // option in the ValidationPipe ensures any extra fields are automatically // stripped and ignored, enhancing security and simplifying our handler. // If you need other fields, add them to this DTO with appropriate validation. }
-
Implement the MessageBird Service: This service will contain the logic to initialize the MessageBird SDK (v10+) and send reply messages using
async/await
.// src/webhooks/messagebird.service.ts import { Injectable, Logger, InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { MessageBird, MessageParameters } from 'messagebird'; // Correct named import for v10+ import { IncomingMessageDto } from './dto/incoming-message.dto'; @Injectable() export class MessagebirdService { private readonly logger = new Logger(MessagebirdService.name); private messagebird: MessageBird; // SDK instance type private originator: string; // Our sending number constructor(private configService: ConfigService) { const apiKey = this.configService.get<string>('MESSAGEBIRD_API_KEY'); this.originator = this.configService.get<string>('MESSAGEBIRD_ORIGINATOR'); if (!apiKey || !this.originator) { throw new Error('MessageBird API Key or Originator not configured in environment variables.'); } // Initialize the SDK using 'new' for v10+ this.messagebird = new MessageBird(apiKey); } /** * Handles the validated incoming message payload. * For this example, it simply logs and sends a reply. * @param incomingMessage - Validated incoming message DTO. */ async handleIncomingMessage(incomingMessage: IncomingMessageDto): Promise<void> { this.logger.log(`Received message from ${incomingMessage.originator}: ""${incomingMessage.payload}""`); // Simple reply logic const replyText = `Thanks for your message: ""${incomingMessage.payload}"". We'll be in touch!`; try { // Await the async sendMessage method await this.sendMessage(incomingMessage.originator, replyText); } catch (error) { // Error handling is done within sendMessage, just log context here this.logger.error(`Failed to send reply to ${incomingMessage.originator}`, error instanceof Error ? error.stack : String(error)); // Depending on requirements, you might re-throw or handle differently // Note: The controller will catch this and return 500 if not handled here. } } /** * Sends an SMS message using the MessageBird SDK (v10+ with Promises). * @param recipient - The destination phone number (international format). * @param body - The text content of the message. */ async sendMessage(recipient: string, body: string): Promise<void> { // Use MessageParameters type from the SDK const params: MessageParameters = { originator: this.originator, recipients: [recipient], body: body, }; this.logger.log(`Attempting to send SMS to ${recipient} from ${this.originator}`); try { // Use async/await with the SDK's promise-based method const response = await this.messagebird.messages.create(params); this.logger.log(`MessageBird SMS sent successfully to ${recipient}. ID: ${response?.id}`); // Log response details if needed (be mindful of sensitive data) // this.logger.debug('MessageBird Response:', response); } catch (error: any) { // Catch as 'any' or 'unknown' for better type safety on error properties this.logger.error(`Error sending MessageBird SMS to ${recipient}:`, error); // Provide a more specific internal error to the caller // Check if error has structure expected from messagebird SDK (optional) const errorMessage = error?.errors?.[0]?.description || error?.message || 'Unknown MessageBird API error'; throw new InternalServerErrorException(`Failed to send SMS via MessageBird: ${errorMessage}`); } } }
-
Implement the MessageBird Controller: This controller defines the webhook endpoint. It uses the DTO for validation and delegates processing to the service.
// src/webhooks/messagebird.controller.ts import { Controller, Post, Body, HttpCode, HttpStatus, Logger } from '@nestjs/common'; import { MessagebirdService } from './messagebird.service'; import { IncomingMessageDto } from './dto/incoming-message.dto'; @Controller('webhooks/messagebird') // Base path for MessageBird webhooks export class MessagebirdController { private readonly logger = new Logger(MessagebirdController.name); constructor(private readonly messagebirdService: MessagebirdService) {} @Post('sms') // Endpoint: POST /webhooks/messagebird/sms @HttpCode(HttpStatus.OK) // Respond with 200 OK on success async handleIncomingSms(@Body() incomingMessageDto: IncomingMessageDto): Promise<string> { // DTO validation is handled automatically by ValidationPipe (configured in main.ts) this.logger.log(`Webhook received: ${JSON.stringify(incomingMessageDto)}`); // Delegate processing to the service. // While this example uses 'await' for simplicity, directly awaiting potentially // long-running operations (like external API calls) in a webhook handler // can make it slow to respond. This increases the risk of timeouts and retries // from MessageBird. For production robustness, especially with more complex logic // or less reliable external services, it's highly recommended to offload the actual // processing (like calling messagebirdService.handleIncomingMessage) to a background // job queue (see Sections 5 & 9). This allows the handler to return 'OK' much faster. try { await this.messagebirdService.handleIncomingMessage(incomingMessageDto); } catch (error: any) { // Log the error from the service, but still return OK to MessageBird // unless it's critical that MessageBird knows about the failure (rare for SMS replies). this.logger.error(`Error handling incoming SMS after receiving: ${error.message}`, error.stack); // Optionally rethrow if MessageBird should retry (e.g., return 5xx status implicitly) // throw error; } // MessageBird primarily expects a 200 OK status to acknowledge receipt. // The response body is usually ignored for SMS webhooks. return 'OK'; } }
With these components, the core logic for receiving and replying to SMS messages is implemented using the modern MessageBird SDK.
3. Building a Complete API Layer
In this specific scenario, the primary "API" is the webhook endpoint (POST /webhooks/messagebird/sms
) that receives data from MessageBird. We aren't building a traditional REST API for external clients to call to initiate SMS actions (though you could certainly add endpoints like POST /messages
for that purpose if needed).
- Authentication/Authorization: The webhook endpoint itself doesn't typically require user authentication, as it's called by MessageBird's servers. Security relies on:
- Keeping the webhook URL non-obvious (avoiding generic paths like
/webhook
). - (Optional Advanced) Implementing a signature verification mechanism if MessageBird supported it for SMS webhooks (it doesn't directly, unlike some other services). Focus on rate limiting and firewall rules.
- Keeping the webhook URL non-obvious (avoiding generic paths like
- Request Validation: This is handled effectively by the
IncomingMessageDto
and the globalValidationPipe
withwhitelist: true
. Invalid requests (missing required fields, incorrect types) will automatically result in a400 Bad Request
response from NestJS before your controller code even runs. Fields not defined in the DTO are automatically stripped. - API Endpoint Documentation: The endpoint is:
- Method:
POST
- Path:
/webhooks/messagebird/sms
- Request Body (Content-Type: application/json or application/x-www-form-urlencoded):
Note: MessageBird often sends data as
{ "originator": "+14155552671", "payload": "Hello from user!", "recipient": "+12015550123", "id": "message_id_string", "receivedDatetime": "2025-04-20T10:00:00Z" // ... other potential fields sent by MessageBird }
application/x-www-form-urlencoded
. NestJS handles both JSON and form-urlencoded by default. Crucially, even though MessageBird might send fields likerecipient
,id
, etc., ourIncomingMessageDto
combined withwhitelist: true
ensures that onlyoriginator
andpayload
are accepted and passed to our service logic in this example. - Successful Response:
- Status Code:
200 OK
- Body:
"OK"
(The body content isn't critical for MessageBird).
- Status Code:
- Error Response (e.g., Validation Failed):
- Status Code:
400 Bad Request
- Body (Example):
{ "statusCode": 400, "message": [ "originator must be a phone number", "payload should not be empty" ], "error": "Bad Request" }
- Status Code:
- Method:
- Testing with cURL:
Assuming your app is running locally on port 3000:
# Valid request (only fields in DTO matter) curl -X POST http://localhost:3000/webhooks/messagebird/sms \ -H "Content-Type: application/json" \ -d '{ "originator": "+14155552671", "payload": "Test message from curl", "extra_field": "this will be ignored" }' # Invalid request (missing payload) curl -X POST http://localhost:3000/webhooks/messagebird/sms \ -H "Content-Type: application/json" \ -d '{ "originator": "+14155552671" }'
4. Integrating with MessageBird (Flow Builder Setup)
This is a critical step. We need to tell MessageBird where to send incoming SMS messages directed to your virtual number.
-
Start Tunneling: Before configuring MessageBird, you need a public URL that points to your local NestJS application.
- Start your NestJS app:
npm run start:dev
(oryarn start:dev
). It should be running (likely on port 3000). - Open a new terminal window.
- Start your tunneling tool (replace
3000
if your app uses a different port):- Using ngrok:
ngrok http 3000
- Using localtunnel:
lt --port 3000
- Using ngrok:
- The tool will output a public URL (e.g.,
https://random-subdomain.ngrok.io
orhttps://your-subdomain.localtunnel.me
). Copy this HTTPS URL. You'll need it in the next step. Keep this tunnel running while testing.
- Start your NestJS app:
-
Configure MessageBird Flow Builder:
- Log in to the MessageBird Dashboard.
- Navigate to Flow Builder.
- Click Create new flow > Create Custom Flow.
- Give your flow a descriptive name (e.g., ""NestJS SMS Webhook Handler"").
- Choose SMS as the trigger. Click Next.
- Click the SMS trigger step. In the right-hand panel, select the virtual number(s) you want to associate with this flow. Click Save.
- Click the
+
icon below the trigger step to add an action. - Select Call HTTP endpoint with SMS (or potentially ""Forward to URL"" depending on exact Flow Builder options available).
- Method: Select POST.
- URL: Paste your tunnel URL from step 1 and append the controller path:
[Your Tunnel HTTPS URL]/webhooks/messagebird/sms
.- Example:
https://random-subdomain.ngrok.io/webhooks/messagebird/sms
- Example:
- Leave other options as default unless you have specific needs (e.g., custom headers - usually not required here).
- Click Save.
- Click Publish in the top-right corner to activate the flow. Make sure it shows ""Published"".
-
Environment Variables Recap:
MESSAGEBIRD_API_KEY
: Your Live API key from the MessageBird Dashboard (Developers > API Access). Used by the SDK to authenticate requests to MessageBird (like sending replies).MESSAGEBIRD_ORIGINATOR
: Your purchased virtual mobile number (e.g.,+12015550123
) from the MessageBird Dashboard (Numbers). Used as theFrom
number when sending replies via the SDK.
Ensure these are correctly set in your
.env
file for local development and configured securely in your deployment environment.
5. Implementing Error Handling, Logging, and Retry Mechanisms
- Error Handling Strategy:
- Validation Errors: Handled automatically by
ValidationPipe
. NestJS returns a 400 response. - SDK Errors (Sending): The
messagebird.service.ts
uses atry...catch
block around themessagebird.messages.create
call within theasync sendMessage
method. It logs the specific error from the SDK and throws a NestJSInternalServerErrorException
(500). This prevents leaking detailed SDK errors to the caller (the controller) while signaling a failure. The controller can decide how to handle this (log and return 200, or let it bubble up to return 500). - Unexpected Errors: NestJS has built-in exception filters that catch unhandled errors and typically return a 500 response.
- Validation Errors: Handled automatically by
- Logging:
- We use NestJS's built-in
Logger
(@nestjs/common
). - Logs are generated for:
- Receiving a webhook request (
messagebird.controller.ts
). - Handling the message details (
messagebird.service.ts
). - Attempting to send a reply (
messagebird.service.ts
). - Successful reply transmission (
messagebird.service.ts
). - Errors during reply transmission (
messagebird.service.ts
).
- Receiving a webhook request (
- Production Logging: Consider using a more robust logging library (like
pino
withnestjs-pino
) to output structured JSON logs, which are easier to parse and analyze with log aggregation tools (e.g., Datadog, ELK stack). Configure log levels appropriately (e.g., INFO for standard operations, WARN for recoverable issues, ERROR for failures).
- We use NestJS's built-in
- Retry Mechanisms:
- Incoming Webhook: MessageBird may retry sending the webhook if your endpoint returns an error (5xx) or times out. Your endpoint should be idempotent if possible (see Section 8) to handle duplicate deliveries gracefully. The primary goal is to return
200 OK
quickly to acknowledge receipt. Offloading work to queues helps significantly here. - Outgoing Reply (SDK Call): The current implementation does not automatically retry sending the reply if the
messagebird.messages.create
call fails. For production robustness:- Simple Retry: Implement a basic retry loop within the
sendMessage
method (e.g., usingasync-retry
npm package) with exponential backoff for transient network issues or temporary MessageBird API problems. Be cautious not to block the webhook response for too long. - Queue-Based Approach (Recommended): For high reliability, decouple sending from the webhook handler. When a message needs to be sent:
- Add a job to a message queue (e.g., Redis with BullMQ, RabbitMQ).
- A separate worker process picks up jobs from the queue.
- The worker attempts to send the SMS via the MessageBird SDK.
- Configure the queue system for automatic retries with backoff on failure. This prevents webhook timeouts and handles temporary downstream issues robustly.
- Simple Retry: Implement a basic retry loop within the
- Incoming Webhook: MessageBird may retry sending the webhook if your endpoint returns an error (5xx) or times out. Your endpoint should be idempotent if possible (see Section 8) to handle duplicate deliveries gracefully. The primary goal is to return
6. Creating a Database Schema and Data Layer (Optional)
While not strictly required for a simple echo bot, persisting message history is crucial for real applications (support chats, order updates). Here's a conceptual outline using TypeORM and PostgreSQL (adapt for your chosen database).
-
Install Dependencies:
npm install @nestjs/typeorm typeorm pg # or yarn add @nestjs/typeorm typeorm pg
-
Configure TypeOrmModule: Set up the database connection in
app.module.ts
(or a dedicated database module), loading credentials fromConfigService
.// src/app.module.ts (Example addition) import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; // ... other imports @Module({ imports: [ ConfigModule.forRoot({ /* ... */ }), TypeOrmModule.forRootAsync({ // Async config for ConfigService imports: [ConfigModule], inject: [ConfigService], useFactory: (configService: ConfigService) => ({ type: 'postgres', host: configService.get<string>('DB_HOST', 'localhost'), port: configService.get<number>('DB_PORT', 5432), username: configService.get<string>('DB_USERNAME'), password: configService.get<string>('DB_PASSWORD'), database: configService.get<string>('DB_NAME'), entities: [__dirname + '/../**/*.entity{.ts,.js}'], // Auto-load entities synchronize: configService.get<string>('NODE_ENV') !== 'production', // ONLY true for dev // autoLoadEntities: true, // Alternative to 'entities' path }), }), WebhooksModule, // ... other modules like a MessageModule if you extract DB logic ], // ... controllers, providers }) export class AppModule {}
Remember to add
DB_HOST
,DB_PORT
,DB_USERNAME
,DB_PASSWORD
,DB_NAME
to your.env
. -
Define Message Entity: Create an entity representing a message record.
// src/messages/message.entity.ts (Example location) import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index } from 'typeorm'; export enum MessageDirection { INBOUND = 'in', OUTBOUND = 'out', } @Entity('messages') export class Message { @PrimaryGeneratedColumn('uuid') id: string; @Column({ type: 'enum', enum: MessageDirection, }) direction: MessageDirection; @Index() // Index for faster lookups @Column() contactNumber: string; // The external party's number (+1 format) @Column() virtualNumber: string; // Your MessageBird number involved (+1 format) @Column('text') body: string; @Column({ type: 'varchar', length: 64, nullable: true }) messagebirdId?: string; // Store the ID from MessageBird API if available @CreateDateColumn() timestamp: Date; // Add other fields as needed: status, conversationId, etc. }
-
Integrate with Service:
- Inject the
Repository<Message>
intoMessagebirdService
. - In
handleIncomingMessage
, create and save anINBOUND
message record before sending the reply. - In
sendMessage
, create and save anOUTBOUND
message record after successfully sending via the SDK (include themessagebirdId
from the response).
- Inject the
-
Migrations: For production, set
synchronize: false
and use TypeORM migrations to manage schema changes safely.- Add migration scripts to your
package.json
. - Generate migrations:
npm run typeorm -- migration:generate -n InitialSchema
- Run migrations:
npm run typeorm -- migration:run
- Add migration scripts to your
This provides a basic structure for message persistence, enabling conversation history and state management.
7. Adding Security Features
Securing your webhook endpoint and application is vital.
- Input Validation and Sanitization:
- Handled: Our use of
class-validator
within theIncomingMessageDto
and the globalValidationPipe
withwhitelist: true
already provides strong input validation. It ensures only expected fields with correct types are processed and strips unexpected data, mitigating risks like prototype pollution. - Sanitization: While validation checks format, explicit sanitization (e.g., preventing potential XSS if message content is ever displayed in HTML without proper encoding) should be done at the point of use (e.g., in a front-end), not typically within this backend service unless it directly renders HTML. Trust the raw SMS content initially but handle it safely downstream.
- Handled: Our use of
- Common Vulnerabilities:
- Webhook Abuse: An attacker could repeatedly call your webhook URL, causing unnecessary processing and cost (if replies are sent).
- Information Leakage: Ensure error messages (especially in production) don't leak sensitive information (stack traces, internal configurations). NestJS's default production error handling is generally safe, but be mindful of custom error logging.
- Rate Limiting:
Protect the webhook endpoint from brute-force/DoS attacks using
@nestjs/throttler
.- Install:
npm install @nestjs/throttler
oryarn add @nestjs/throttler
- Configure: Import and configure in
app.module.ts
:// src/app.module.ts import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; import { APP_GUARD } from '@nestjs/core'; // ... other imports @Module({ imports: [ // ... ConfigModule, TypeOrmModule etc. ThrottlerModule.forRoot([{ // Basic configuration ttl: 60000, // Time-to-live (milliseconds) - 60 seconds limit: 10, // Max requests per TTL from one IP }]), WebhooksModule, ], controllers: [AppController], providers: [ AppService, { // Apply ThrottlerGuard globally provide: APP_GUARD, useClass: ThrottlerGuard, }, ], }) export class AppModule {}
- This applies rate limiting based on IP address to all endpoints. You can customize limits per-route using the
@Throttle()
decorator if needed.
- Install:
- Secure API Key/Secrets Management:
- NEVER hardcode API keys or sensitive data in source code.
- Use
.env
for local development (and ensure it's in.gitignore
). - In production, use your hosting provider's secrets management service (e.g., AWS Secrets Manager, Google Secret Manager, Docker secrets, environment variables injected securely by the platform).
- Keep Dependencies Updated: Regularly update Node.js, NestJS, MessageBird SDK, and other dependencies to patch known security vulnerabilities (
npm outdated
,npm update
oryarn outdated
,yarn upgrade
). Use tools likenpm audit
oryarn audit
.
8. Handling Special Cases
Real-world SMS interaction involves nuances:
- Duplicate Messages (Idempotency): MessageBird might occasionally send the same webhook twice (e.g., if it didn't receive a timely
200 OK
initially).- Challenge: The standard MessageBird SMS webhook payload doesn't seem to include a reliable, unique ID for the incoming message event itself that's guaranteed across retries. Fields like
id
usually refer to the message stored on MessageBird's side. - Mitigation (if using Database): Before processing (e.g., saving to DB and sending reply), check if a message with a very similar timestamp (e.g., within the last few seconds) from the same
originator
and with the identicalpayload
already exists in your database. If so, log the duplicate and return200 OK
without reprocessing. This isn't foolproof but helps prevent obvious double replies. - Focus on Quick Response: The best defense is to ensure your webhook endpoint responds quickly with
200 OK
. Offloading processing to a background queue (as mentioned in Section 5) is the most robust way to achieve this, minimizing the chance MessageBird needs to retry.
- Challenge: The standard MessageBird SMS webhook payload doesn't seem to include a reliable, unique ID for the incoming message event itself that's guaranteed across retries. Fields like