Send SMS with MessageBird in NestJS: Complete Node.js Integration Guide
Learn how to integrate MessageBird SMS API with NestJS. Step-by-step tutorial covering MessageBird Node.js SDK setup, ValidationPipe, E.164 format, error handling, and production deployment.
This guide provides a step-by-step walkthrough for building a production-ready NestJS application capable of sending SMS messages using the MessageBird API and its Node.js SDK. We will cover project setup, core implementation, configuration, error handling, security, testing, and deployment considerations.
By the end of this guide, you will have a robust API endpoint that accepts a recipient phone number and a message body, validates the input, and uses MessageBird to reliably deliver the SMS. This solves the common need for programmatic SMS notifications, alerts, or basic communication features within a modern web application.
Time to Complete: 30-45 minutes
Skill Level Required: Intermediate (familiarity with TypeScript, Node.js, REST APIs, and basic NestJS concepts)
Project Overview and Goals
Goal: Create a simple NestJS API endpoint (POST /sms/send) to send outbound SMS messages via MessageBird.
Problem Solved: Enables applications to programmatically send SMS messages for various purposes like notifications, verification codes (though MessageBird's Verify API is often better suited for OTP), or simple alerts.
Common Use Cases:
Transactional notifications: Order confirmations, shipping updates, appointment reminders
Marketing: Promotional offers, event announcements (ensure compliance with local regulations)
Alerts: System monitoring alerts, security notifications
Cost Implications: MessageBird operates on a prepaid model with per-message pricing that varies by destination country. Typical costs range from $0.02-$0.10 per SMS depending on the country. Free trial accounts receive 10 test SMS credits that can only send to your verified phone number (source).
Technologies:
Node.js: The underlying runtime environment. Requires Node.js >= 0.10 or io.js (MessageBird SDK requirement).
NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Chosen for its robust structure, dependency injection, modularity, and built-in support for features like validation and configuration management.
MessageBird Node.js SDK: Simplifies interaction with the MessageBird REST API. Official SDK (BSD 2-Clause License) maintained at github.com/messagebird/messagebird-nodejs. Latest stable version available via npm: npm install messagebird.
dotenv / @nestjs/config: For managing environment variables securely.
class-validator / class-transformer: For robust request payload validation. Note: class-validator v0.14.0+ changed the default for forbidUnknownValues from false to true, which may affect existing implementations.
libphonenumber-js (Optional): For stricter phone number validation.
Architecture:
Text
Client (e.g., Postman, Frontend App)
|
| HTTP POST Request (/sms/send)
V
NestJS Application
| - Controller (Handles request, validation using DTO)
| - Service (Contains business logic, interacts with MessageBird SDK)
| - Module (Organizes components)
| - ConfigModule (Loads environment variables)
V
MessageBird SDK
|
| API Call (messages.create)
V
MessageBird API
|
| SMS Delivery Network
V
Recipient's Phone
Prerequisites:
Node.js (LTS version 14.x or higher recommended) and npm/yarn installed.
A MessageBird account (Free trial available at messagebird.com).
A text editor or IDE (e.g., VS Code).
Basic understanding of TypeScript, Node.js, and REST APIs.
A purchased virtual number or registered Alphanumeric Sender ID in your MessageBird account to use as the originator. Important: Some countries (e.g., United States) prohibit alphanumeric sender IDs and require a numeric phone number. See MessageBird's sender ID documentation for country-specific restrictions.
1. Setting up the Project
Let's initialize our NestJS project and install the necessary dependencies.
Install NestJS CLI: If you don't have it, install the NestJS command-line interface globally.
Bash
npm install -g @nestjs/cli
Create New NestJS Project: Generate a new project using the CLI.
Bash
nest new nestjs-messagebird-sms
Choose your preferred package manager (npm or yarn) when prompted.
Navigate to Project Directory:
Bash
cd nestjs-messagebird-sms
Install Dependencies: We need the MessageBird SDK, and packages for configuration management and validation.
Bash
# Using npm
npm install messagebird @nestjs/config dotenv class-validator class-transformer
# Using yarn
yarn add messagebird @nestjs/config dotenv class-validator class-transformer
messagebird: The official Node.js SDK for the MessageBird API.
@nestjs/config: Handles environment variables gracefully within NestJS.
dotenv: Loads environment variables from a .env file into process.env. Used by @nestjs/config.
class-validator & class-transformer: Enable easy implementation of validation rules using decorators on DTOs (Data Transfer Objects).
Package Version Compatibility:
MessageBird SDK: Latest version (3.x or higher recommended). Check npm registry for current version.
@nestjs/config: Compatible with your NestJS version (typically @nestjs/config@^2.0.0 or @nestjs/config@^3.0.0).
class-validator: ^0.14.0 or higher.
class-transformer: ^0.5.0 or higher.
Common Installation Troubleshooting:
Node version mismatch: If you encounter errors during installation, verify your Node.js version with node -v. MessageBird SDK requires Node.js >= 0.10, but NestJS requires Node.js >= 14.x. Upgrade Node.js if needed using nvm or download from nodejs.org.
Permission errors on global install: Use sudo npm install -g @nestjs/cli on macOS/Linux or run your terminal as administrator on Windows. Alternatively, configure npm to use a user-level directory for global packages.
Package conflicts: Clear npm cache with npm cache clean --force and delete node_modules and package-lock.json, then reinstall.
Project Structure: The Nest CLI sets up a standard structure (src/ contains app.module.ts, app.controller.ts, app.service.ts, main.ts). We will create dedicated modules, services, and controllers for SMS functionality.
2. Configuring Environment Variables
Sensitive information like API keys should never be hardcoded. We'll use environment variables.
Get MessageBird API Key:
Log in to your MessageBird Dashboard.
Navigate to the Developers section in the left-hand menu.
Click on API access.
You can use your live API key or switch to test mode to get a test key. For initial development, the test key is sufficient and won't incur charges or send actual SMS. Copy the appropriate key.
Important: Keep your live API key secure and confidential.
API Key Types:
Test keys (prefix: test_): Simulate SMS sending without actual delivery or charges. Messages appear as "sent" but no real SMS is delivered.
Live keys (prefix: live_): Send actual messages, consume account balance, and deliver to real phone numbers. Requires a topped-up prepaid balance.
API Key Permissions: MessageBird API keys have full account access. For enhanced security in production, consider using MessageBird's Conversations API which supports channel-specific tokens with limited scopes.
Get MessageBird Originator:
This is the sender ID displayed on the recipient's phone. It can be:
A virtual mobile number purchased from MessageBird (e.g., +12025550134). Ensure it's SMS-enabled.
An Alphanumeric Sender ID (e.g., MyCompany) registered and approved in your MessageBird account. Note: Alphanumeric sender IDs are subject to country-specific regulations and restrictions. For example, the United States requires numeric phone numbers and does not allow alphanumeric originators. Pre-registration may be required in certain countries.
Find or purchase/register this in the "Numbers" section of the MessageBird Dashboard.
Alphanumeric Sender ID Registration: Registration typically takes 1-3 business days depending on the country. Some countries (e.g., India, Saudi Arabia) require additional documentation. Check MessageBird's country restrictions for specific requirements.
Create .env File: In the root directory of your project, create a file named .env.
Bash
touch .env
Add Variables to .env: Add your MessageBird API key and originator to the file. Replace placeholders with your actual values.
.env
# .env
# MessageBird Configuration
MESSAGEBIRD_API_KEY=YOUR_API_KEY # Use 'test_...' or 'live_...' key
MESSAGEBIRD_ORIGINATOR=YOUR_SENDER_ID # e.g., +12025550134 or MyCompany
Ignore .env File: Add .env to your .gitignore file to prevent accidentally committing secrets.
.gitignore
# .gitignore
# ... other entries
.env
Configure NestJS ConfigModule: Import and configure ConfigModule in your main application module (src/app.module.ts) to make environment variables accessible throughout the application via dependency injection.
isGlobal: true makes the ConfigService available in any module without needing to import ConfigModule everywhere.
envFilePath: '.env' tells the module where to find the environment file.
3. Implementing Core SMS Functionality (Service)
We'll create a dedicated module and service to handle SMS logic.
Generate SMS Module and Service: Use the Nest CLI.
Bash
nest generate module sms
nest generate service sms
This creates src/sms/sms.module.ts and src/sms/sms.service.ts (along with a test file).
Implement SmsService: Open src/sms/sms.service.ts and add the logic to interact with the MessageBird SDK.
TypeScript
// src/sms/sms.service.tsimport { Injectable, Logger } from'@nestjs/common';
import { ConfigService } from'@nestjs/config';
import * asMessageBirdfrom'messagebird'; // Import MessageBird SDK types if available, or use as namespace@Injectable()
exportclassSmsService {
privatereadonly logger = newLogger(SmsService.name);
privatemessagebird: MessageBird.MessageBird; // Declare messagebird client instance using SDK typesprivateoriginator: string;
constructor(privateconfigService: ConfigService) {
// Retrieve API key and originator from environment variablesconst apiKey = this.configService.get<string>('MESSAGEBIRD_API_KEY');
this.originator = this.configService.get<string>('MESSAGEBIRD_ORIGINATOR');
if (!apiKey || !this.originator) {
this.logger.error('MessageBird API Key or Originator not configured in .env file');
thrownewError('MessageBird environment variables missing.');
}
// Initialize the MessageBird client// The require('messagebird')(key) pattern is common for initializing CJS modules like this SDK.// Using 'any' here if specific types for the initializer function aren't readily available or inferred.try {
constmbInitializer: any = require('messagebird');
this.messagebird = mbInitializer(apiKey);
this.logger.log('MessageBird SDK initialized successfully.');
} catch (error) {
this.logger.error('Failed to initialize MessageBird SDK', error);
throw error; // Re-throw to prevent service usage if initialization fails
}
}
/**
* Sends an SMS message using the MessageBird API.
* @paramrecipient - The recipient's phone number in E.164 format (e.g., 14155552671).
* Note: MessageBird API expects numbers WITHOUT the '+' prefix.
* The SDK typically handles this, but ensure numbers are in international format.
* @parambody - The text content of the SMS message.
* @returns Promise resolving with the MessageBird API response or rejecting with an error.
*/asyncsendSms(recipient: string, body: string): Promise<any> { // Consider defining a stricter return type based on expected MessageBird responseconstparams: MessageBird.MessageParameters = {
originator: this.originator,
recipients: [recipient], // Expects an array of recipientsbody: body,
};
this.logger.log(`Attempting to send SMS to ${recipient} from ${this.originator}`);
// The MessageBird SDK's messages.create uses a callback.// We wrap it in a Promise for modern async/await usage.returnnewPromise((resolve, reject) => {
this.messagebird.messages.create(params, (err, response) => {
if (err) {
this.logger.error(`Failed to send SMS to ${recipient}`, err);
// Provide more context if available in the error objectconst errorDetails = err.errors ? JSON.stringify(err.errors) : err.message;
reject(newError(`MessageBird API Error: ${errorDetails}`));
} else {
this.logger.log(`SMS sent successfully to ${recipient}. Message ID: ${response.id}`);
// The response contains details about the sent message(s)resolve(response);
}
});
});
}
}
Dependencies: We inject ConfigService to access environment variables. Logger provides built-in NestJS logging.
Initialization: The constructor retrieves the API key and originator, then initializes the messagebird client. Robust error handling is added for missing variables or initialization failure. The comment clarifies the use of require and any for CJS interop.
sendSms Method:
Takes recipient and body as arguments.
Constructs the params object required by messagebird.messages.create, using the configured originator.
Wraps the callback-based SDK call in a Promise for compatibility with NestJS's async/await patterns.
Logs success or failure using the Logger.
Resolves with the MessageBird response on success, rejects with a formatted error on failure.
MessageBird Response Structure: The response object from messages.create includes:
id: Unique message identifier (string)
href: URL to retrieve message details
direction: "mt" (mobile terminated - sent to mobile)
originator: The sender ID used
body: Message content
recipients: Object with delivery status details
totalCount: Total recipient count
totalSentCount: Count of messages sent
totalDeliveredCount: Count of delivered messages
items: Array of per-recipient status objects including recipient, status, statusReason, messageLength, messagePartCount, price
Update SmsModule: Ensure SmsService is listed as a provider in src/sms/sms.module.ts. Nest CLI usually does this automatically.
TypeScript
// src/sms/sms.module.tsimport { Module } from'@nestjs/common';
import { SmsService } from'./sms.service';
// If we add a controller later, import it hereimport { SmsController } from'./sms.controller'; // Import controller@Module({
controllers: [SmsController], // Add controller when createdproviders: [SmsService],
exports: [SmsService], // Export service if needed by other modules
})
exportclassSmsModule {}
4. Building the API Layer (Controller and DTO)
Now, let's create the API endpoint to trigger the SmsService.
Generate SMS Controller:
Bash
nest generate controller sms
This creates src/sms/sms.controller.ts.
Create Request DTO: Create a Data Transfer Object (DTO) to define the expected shape and validation rules for the request body. Create a file src/sms/dto/send-sms.dto.ts.
TypeScript
// src/sms/dto/send-sms.dto.tsimport { IsNotEmpty, IsString, MinLength, IsPhoneNumber } from'class-validator';
exportclassSendSmsDto {
@IsNotEmpty({ message: 'Recipient phone number is required.' })
// Use IsPhoneNumber for stricter E.164 validation.// Requires installing libphonenumber-js: run 'npm install libphonenumber-js' or 'yarn add libphonenumber-js'// @IsPhoneNumber(null, { message: 'Recipient must be a valid phone number (E.164 format recommended).' })@IsString() // Use IsString for a basic check if libphonenumber-js is not installedreadonlyrecipient: string;
@IsNotEmpty({ message: 'Message body cannot be empty.' })
@IsString()
@MinLength(1, { message: 'Message body must contain at least 1 character.' })
readonlybody: string;
}
We use decorators from class-validator (@IsNotEmpty, @IsString, @MinLength, optionally @IsPhoneNumber) to define validation rules.
Clear instructions are added for installing libphonenumber-js if @IsPhoneNumber is used.
SMS Character Limits and Encoding:
GSM-7 encoding (standard): Supports 160 characters per SMS segment. Used for basic Latin characters, numbers, and common symbols defined in GSM 03.38 character set.
Unicode (UCS-2/UTF-16) encoding: Supports 70 characters per SMS segment. Automatically used when message contains characters outside GSM-7 (e.g., emojis, non-Latin scripts, certain special characters).
Multi-part messages: Messages exceeding the character limit are split into segments. GSM-7 segments are 153 characters each (7 characters reserved for concatenation headers), Unicode segments are 67 characters each. Each segment is billed separately.
MessageBird's datacoding parameter can be set to plain (GSM-7), unicode, or auto (default - automatically detects based on content). See MessageBird SMS API - datacoding.
Implement SmsController: Open src/sms/sms.controller.ts and define the endpoint.
TypeScript
// src/sms/sms.controller.tsimport { Controller, Post, Body, UsePipes, ValidationPipe, Logger, HttpException, HttpStatus } from'@nestjs/common';
import { SmsService } from'./sms.service';
import { SendSmsDto } from'./dto/send-sms.dto';
@Controller('sms') // Route prefix for this controllerexportclassSmsController {
privatereadonly logger = newLogger(SmsController.name);
constructor(privatereadonlysmsService: SmsService) {}
@Post('send') // Route: POST /sms/send@UsePipes(newValidationPipe({ transform: true, whitelist: true })) // Enable validation using the DTOasyncsendSms(@Body() sendSmsDto: SendSmsDto) {
this.logger.log(`Received request to send SMS: ${JSON.stringify(sendSmsDto)}`);
try {
const result = awaitthis.smsService.sendSms(sendSmsDto.recipient, sendSmsDto.body);
// Successfully sent (or at least accepted by MessageBird)return {
message: 'SMS submitted successfully.',
data: {
messageId: result.id, // Or relevant data from the MessageBird responserecipients: result.recipients.totalSentCount,
},
};
} catch (error) {
this.logger.error(`Error sending SMS: ${error.message}`, error.stack);
// Throw standard HTTP exceptionsthrownewHttpException(
`Failed to send SMS: ${error.message}`,
HttpStatus.INTERNAL_SERVER_ERROR, // Or BAD_GATEWAY if it's clearly an external API issue
);
}
}
}
@Controller('sms'): Sets the base path for all routes in this controller to /sms.
@Post('send'): Defines a handler for POST requests to /sms/send.
@Body() sendSmsDto: SendSmsDto: Injects the validated request body into the sendSmsDto parameter.
@UsePipes(new ValidationPipe(...)): Automatically validates the incoming body against the SendSmsDto.
transform: true: Automatically transforms the incoming plain object to an instance of SendSmsDto.
whitelist: true: Strips any properties from the request body that are not defined in the DTO.
Error Handling: The try...catch block handles potential errors from the SmsService and throws an HttpException with an appropriate status code and message.
Update SmsModule: Add the SmsController to the controllers array in src/sms/sms.module.ts.
Enable Validation Globally (Optional but Recommended): Instead of applying @UsePipes to every controller method, you can enable the ValidationPipe globally in src/main.ts.
TypeScript
// src/main.tsimport { NestFactory } from'@nestjs/core';
import { AppModule } from'./app.module';
import { Logger, ValidationPipe } from'@nestjs/common'; // Import ValidationPipeasyncfunctionbootstrap() {
const app = awaitNestFactory.create(AppModule);
const logger = newLogger('Bootstrap'); // Create a logger instance// Enable global validation pipe
app.useGlobalPipes(newValidationPipe({
transform: true, // Automatically transform payloads to DTO instanceswhitelist: true, // Strip properties not defined in DTOforbidNonWhitelisted: true, // Throw error if non-whitelisted properties are presenttransformOptions: {
enableImplicitConversion: true, // Allow basic type conversions
},
}));
// Configure global prefix (optional)// app.setGlobalPrefix('api/v1');const port = process.env.PORT || 3000;
await app.listen(port);
logger.log(`Application listening on port ${port}`);
// Removed log related to /api documentation as Swagger setup is not included.
}
bootstrap();
If you enable it globally, you can remove the @UsePipes(...) decorator from the SmsController.
5. Testing the API Endpoint
Let's verify our implementation.
Start the Application:
Bash
npm run start:dev
This starts the NestJS application in watch mode. Look for the log message indicating the port it's running on (usually 3000).
Send a Test Request (Using curl): Open a new terminal window and use curl (or a tool like Postman or Insomnia) to send a POST request. Replace YOUR_RECIPIENT_NUMBER with a valid phone number (use your own for testing with a live key, or any validly formatted number for a test key).
Bash
curl -X POST http://localhost:3000/sms/send \
-H ""Content-Type: application/json"" \
-d '{
""recipient"": ""YOUR_RECIPIENT_NUMBER"",
""body"": ""Hello from my NestJS MessageBird App!""
}'
Check Response:
Success: You should receive a JSON response similar to:
Check your application console logs for success messages. If using a live key, check the recipient phone for the SMS. Check the MessageBird Dashboard logs (Logs -> Messages) to see the status.
Validation Error: If you send invalid data (e.g., missing body), you'll get a 400 Bad Request response detailing the errors:
JSON
{""statusCode"":400,""message"":[""Message body cannot be empty."",""Message body must contain at least 1 character.""],""error"":""Bad Request""}
API Error: If MessageBird rejects the request (e.g., invalid API key, insufficient balance, invalid originator), you'll get a 500 Internal Server Error (or the status code you configured in the controller's HttpException):
JSON
{""statusCode"":500,""message"":""Failed to send SMS: MessageBird API Error: Authentication failed""}
Check your application console logs for detailed error messages from the SmsService.
6. Error Handling and Logging
We've already incorporated basic logging and error handling:
NestJS Logger: Used in the service and controller to log informational messages and errors. Consider configuring log levels (e.g., only log errors in production) via environment variables or configuration files. You can also integrate more advanced logging libraries like Winston or Pino with NestJS.
try...catch + HttpException: Catches errors from the SmsService (including MessageBird API errors) and translates them into appropriate HTTP responses (e.g., 500 Internal Server Error, 502 Bad Gateway).
MessageBird Error Structure: When the SDK rejects the promise, the err object often contains an errors array with more specific details from the MessageBird API (code, description, parameter). Logging JSON.stringify(err.errors) can be very helpful for debugging.
Retry Mechanisms (Advanced): For critical SMS, you might implement retries with exponential backoff, especially for transient network errors or temporary MessageBird API issues (e.g., rate limiting). Libraries like async-retry or NestJS scheduler features (@nestjs/schedule) could be used. Example retry logic for 429 errors:
Never commit API keys to version control (.env in .gitignore).
Use environment variables managed securely in your deployment environment (e.g., secrets management tools, platform environment variables).
Consider rotating API keys periodically (recommended: every 90 days for live keys).
API Key Rotation Best Practices:
Generate new API key in MessageBird Dashboard
Update environment variables in staging environment
Test thoroughly in staging
Update production environment with zero downtime using rolling deployments
Monitor for errors for 24-48 hours
Revoke old API key in MessageBird Dashboard
Input Validation: Already implemented using class-validator DTOs. This prevents malformed requests and basic injection attempts in the body parameters. Always validate and sanitize any user-provided input.
Rate Limiting: Protect your API endpoint from abuse. Use @nestjs/throttler to limit the number of requests per IP address or user within a specific timeframe.
MessageBird Rate Limits: MessageBird API enforces the following rate limits (source):
GET requests: 50 requests/second
POST requests: 500 requests/second
Exceeding these limits returns HTTP 429 (Too Many Requests). The request is not processed and should be retried with exponential backoff.
Handling 429 Errors: Implement retry logic with exponential backoff (see Section 6). Monitor Retry-After header if provided in 429 responses.
Authentication/Authorization: This guide assumes the API endpoint might be internal or protected by other means (e.g., API Gateway, firewall). For public-facing APIs, implement proper authentication (e.g., JWT, API Keys specific to your API consumers) to ensure only authorized clients can trigger SMS sending.
Originator Restrictions: Be aware of country-specific regulations regarding Alphanumeric Sender IDs and ensure your chosen originator is compliant and approved.
GDPR and Data Retention: If operating in the EU or handling EU citizen data, ensure compliance with GDPR:
Store phone numbers securely with encryption at rest
Implement data retention policies (e.g., delete message logs after 90 days)
Provide mechanisms for users to request data deletion
Obtain explicit consent before sending marketing SMS
Include opt-out instructions in all messages
Maintain audit logs of consent and opt-out requests
8. Unit Testing the Service
NestJS encourages testing. Let's write a basic unit test for SmsService.
Open Test File: Navigate to src/sms/sms.service.spec.ts. The CLI generates a basic shell.
Write Unit Tests: Mock dependencies (ConfigService, messagebird SDK) and test the sendSms method logic.
TypeScript
// src/sms/sms.service.spec.tsimport { Test, TestingModule } from'@nestjs/testing';
import { SmsService } from'./sms.service';
import { ConfigService } from'@nestjs/config';
// Logger is not explicitly needed for these tests, so it's removed from providers// Mock the messagebird libraryconst mockMessageBirdCreate = jest.fn();
const mockMessageBirdClient = {
messages: {
create: mockMessageBirdCreate,
},
};
// Mock the require('messagebird') call
jest.mock('messagebird', () => {
// This mocks the function returned by require('messagebird')return jest.fn().mockImplementation(() => mockMessageBirdClient);
}, { virtual: true }); // Use virtual mock for requiredescribe('SmsService', () => {
letservice: SmsService;
letconfigService: ConfigService;
// Mock ConfigService valuesconst mockConfigService = {
get: jest.fn((key: string) => {
if (key === 'MESSAGEBIRD_API_KEY') return'test_api_key';
if (key === 'MESSAGEBIRD_ORIGINATOR') return'+1234567890';
returnnull;
}),
};
beforeEach(async () => {
// Reset mocks before each test
mockMessageBirdCreate.mockClear();
// Also clear mocks on the module factory itself if require was mocked
(require('messagebird') as jest.Mock).mockClear();
constmodule: TestingModule = awaitTest.createTestingModule({
providers: [
SmsService,
{
provide: ConfigService,
useValue: mockConfigService, // Use the mock implementation
},
// Logger removed from providers as it's not directly injected/tested here
],
}).compile();
service = module.get<SmsService>(SmsService);
configService = module.get<ConfigService>(ConfigService); // Get instance if needed for assertions
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('sendSms', () => {
const recipient = '+9876543210';
const body = 'Test message';
const originator = '+1234567890'; // Expected from mockConfigServiceit('should call messagebird.messages.create with correct parameters and resolve on success', async () => {
const mockResponse = { id: 'mock-message-id', recipients: { totalSentCount: 1 } };
// Configure the mock implementation for the SDK call for this test
mockMessageBirdCreate.mockImplementation((params, callback) => {
callback(null, mockResponse); // Simulate success
});
awaitexpect(service.sendSms(recipient, body)).resolves.toEqual(mockResponse);
expect(mockMessageBirdCreate).toHaveBeenCalledTimes(1);
expect(mockMessageBirdCreate).toHaveBeenCalledWith(
{
originator: originator,
recipients: [recipient],
body: body,
},
expect.any(Function), // The callback function
);
});
it('should reject with an error if messagebird.messages.create fails', async () => {
const mockError = {
message: "API Error",
errors: [{ code: 2, description: 'Request parameter validation failed', parameter: 'recipients' }]
};
// Configure the mock implementation for the SDK call for this test
mockMessageBirdCreate.mockImplementation((params, callback) => {
callback(mockError, null); // Simulate error
});
awaitexpect(service.sendSms(recipient, body)).rejects.toThrow(
`MessageBird API Error: ${JSON.stringify(mockError.errors)}`// Check for the formatted error
);
expect(mockMessageBirdCreate).toHaveBeenCalledTimes(1);
expect(mockMessageBirdCreate).toHaveBeenCalledWith(
{
originator: originator,
recipients: [recipient],
body: body,
},
expect.any(Function),
);
});
it('should throw an error during initialization if API key is missing', async () => {
// Override config mock for this specific scenarioconst missingKeyConfig = {
get: jest.fn((key: string) => {
if (key === 'MESSAGEBIRD_ORIGINATOR') return'+1234567890';
returnnull; // Simulate missing API key
})
};
// We need to test the module compilation which triggers the constructorawaitexpect(Test.createTestingModule({
providers: [
SmsService,
{ provide: ConfigService, useValue: missingKeyConfig },
// Logger not needed here either
],
}).compile()).rejects.toThrow('MessageBird environment variables missing.');
});
});
});
Mocking: We use jest.fn() and jest.mock to create mock implementations of ConfigService and the messagebird SDK.
Test Cases: We test the successful path, the error path, and the constructor initialization error handling.
Logger:Logger has been removed from the test module providers for cleanliness, as it wasn't actively used in the tests.
Run Tests:
Bash
npm run test
Or for a specific file:
Bash
npm run test -- src/sms/sms.service.spec.ts
9. Troubleshooting and Caveats
Common Error Messages and Solutions:
Error Message
Likely Cause
Solution
Authentication failed
Invalid API key
Verify MESSAGEBIRD_API_KEY in .env, check for typos/spaces, ensure correct key type (test/live)
message could not be sent because the originator is invalid
Unregistered/invalid originator
Verify MESSAGEBIRD_ORIGINATOR matches a purchased VMN or approved alphanumeric sender ID. Check country restrictions.
Request parameter validation failed (recipients)
Invalid phone number format
Ensure E.164 format (country code + number, no +). Example: 14155552671 for US number.
No (suitable) routes found
Insufficient balance or unsupported destination
Check account balance for live keys. Verify destination country is supported. Check originator type compatibility.
Error: Authentication failed: Double-check your MESSAGEBIRD_API_KEY in .env. Ensure you're using the correct key (live vs. test) and that it hasn't been revoked. Verify there are no typos or extra spaces.
Error: message could not be sent because the originator is invalid: Ensure MESSAGEBIRD_ORIGINATOR in .env matches a purchased number or a registered and approved Alphanumeric Sender ID in your MessageBird account. Check country restrictions for Alphanumeric Sender IDs.
Error: Request parameter validation failed (Parameter: recipients): Ensure the recipient number is in a valid format. While MessageBird is somewhat flexible, E.164 format (country code and number without '+', e.g., 14155552671) is strongly recommended. The SDK expects recipients as an array. The MessageBird API expects numbers in international format without the '+' symbol, though the SDK may handle normalization.
Error: No (suitable) routes found: You might have insufficient balance (for live keys) or the recipient country/network might not be supported by your MessageBird account setup or the originator type. Check your balance and MessageBird's coverage documentation.
SMS Not Received (Live Key):
Verify recipient number is correct and has signal.
Check MessageBird Dashboard Logs for detailed delivery status (e.g., "delivered", "failed", "expired").
Ensure the number isn't on a Do-Not-Disturb list or blocked by the carrier.
Check for country-specific regulations or carrier filtering.
Callback vs. Promise: Remember the base MessageBird Node SDK uses callbacks. Our Promise wrapper is crucial for clean async/await usage in NestJS. Errors in the promise handling logic can mask SDK errors.
Rate Limits: MessageBird imposes API rate limits (GET: 50 req/s, POST: 500 req/s). If sending high volumes, implement rate limiting and retry logic with exponential backoff. When you receive a 429 response, the request has not been processed and can be safely retried (source).
SDK Version: Ensure you are using a reasonably recent version of the messagebird package, as APIs and methods can change. Check package.json and compare with npm registry.
10. Deployment and CI/CD
Build: Create a production build:
Bash
npm run build
This compiles TypeScript to JavaScript in the dist folder.
Run Production: Start the application using Node.js directly on the compiled output:
Bash
node dist/main
Environment Variables: Ensure your production environment (e.g., Docker container, PaaS like Heroku/Vercel/AWS Elastic Beanstalk, VM) has the MESSAGEBIRD_API_KEY and MESSAGEBIRD_ORIGINATOR environment variables set securely. Do not include the .env file in your production artifact; use the platform's mechanism for environment variables.
Docker Containerization Example:
Dockerfile
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/main"]
CI/CD Pipeline Example (GitHub Actions):
YAML
# .github/workflows/deploy.ymlname:BuildandDeployon:push:branches: [main]
jobs:build:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v3-uses:actions/setup-node@v3with:node-version:'18'cache:'npm'-run:npmci-run:npmrunlint-run:npmruntest-run:npmrunbuild-name:Deploytoproductionenv:MESSAGEBIRD_API_KEY:${{secrets.MESSAGEBIRD_API_KEY}}MESSAGEBIRD_ORIGINATOR:${{secrets.MESSAGEBIRD_ORIGINATOR}}run:|
# Add your deployment commands here
# Example: scp, rsync, or platform-specific CLI
Process Management: Use a process manager like pm2 or rely on your platform's built-in service management (e.g., systemd, Docker orchestration) to keep the Node.js application running reliably, handle restarts, and manage logs.
Health Check Endpoint: Add a health check endpoint for monitoring:
How do I send SMS messages with MessageBird in NestJS?
To send SMS with MessageBird in NestJS, install the MessageBird Node.js SDK (npm install messagebird), create a service that wraps the SDK's messages.create method with promises, and use NestJS dependency injection to access it from your controller. Configure your API key via @nestjs/config and validate requests using DTOs with class-validator.
What phone number format does MessageBird require?
MessageBird requires phone numbers in E.164 international format without the '+' prefix (e.g., 14155552671 for a US number). The MessageBird Node.js SDK typically handles format normalization, but providing properly formatted numbers ensures reliable delivery and reduces API errors.
Can I use alphanumeric sender IDs with MessageBird in all countries?
No, alphanumeric sender IDs are not supported in all countries. The United States, for example, prohibits alphanumeric sender IDs and requires a registered numeric phone number. Check MessageBird's sender ID documentation for country-specific restrictions before configuring your originator.
How do I handle MessageBird API errors in NestJS?
Wrap MessageBird SDK callbacks in promises and use try-catch blocks in your service methods. Map MessageBird API errors to appropriate HTTP status codes using NestJS HttpException classes. Common errors include authentication failures (401), parameter validation errors (400), and insufficient balance or routing issues.
What's the difference between MessageBird test and live API keys?
Test API keys allow you to simulate SMS sending without actual delivery or charges. Messages appear as "sent" in your code, but no real SMS is delivered. Live API keys send actual messages, consume your account balance, and deliver to real phone numbers. Always use test keys during development.
How do I validate phone numbers in NestJS before sending SMS?
Use class-validator decorators in your DTO, such as @IsString() and @Matches() with an E.164 regex pattern. For more robust validation, integrate libphonenumber-js to verify phone number validity, format, and country code before passing to the MessageBird API.
What dependencies do I need for MessageBird NestJS integration?
You need @nestjs/core, @nestjs/common, messagebird (the official Node.js SDK), @nestjs/config for environment variables, class-validator and class-transformer for request validation, and optionally libphonenumber-js for advanced phone number validation.
How do I deploy a MessageBird NestJS application to production?
Build your NestJS application with npm run build, set MESSAGEBIRD_API_KEY and MESSAGEBIRD_ORIGINATOR as environment variables in your hosting platform (not in a committed .env file), and use a process manager like pm2 or platform-native service management. Implement proper error logging, rate limiting, and monitoring for production reliability.
Related Resources
For more comprehensive guides on SMS integration and phone number formatting:
E.164 Phone Number Format: Learn about the international standard for phone number formatting required by most SMS APIs
NestJS Configuration Management: Deep dive into secure environment variable handling and ConfigModule best practices
SMS Delivery Best Practices: Understand delivery rates, carrier filtering, and compliance requirements
MessageBird API Reference: Official documentation for advanced features like scheduled sending, delivery reports, and webhook integration
MessageBird Country Restrictions: Check regulations and requirements for specific countries before sending SMS
Frequently asked questions
Set up a new NestJS project, install the MessageBird Node.js SDK, configure environment variables for your API key and originator, create an SMS service and controller, define a DTO for validation, and implement the API endpoint logic to send SMS messages via the MessageBird API. Don't forget to properly handle errors and secure your API keys, and consider using a service like pm2 upon deployment.
It simplifies interaction with the MessageBird REST API, making it easier to send SMS messages and access other MessageBird services within your Node.js or NestJS applications. It handles API requests and responses, reducing the amount of manual coding required.
Dependency injection in NestJS makes managing dependencies, like the MessageBird service and configuration variables, cleaner and more efficient. It promotes modularity and testability by allowing you to easily swap out real implementations with mocks during testing.
The MessageBird Verify API is recommended over basic SMS sending for OTP-based authentication and verification, as it's specifically designed for that purpose. For basic alerts, notifications, or two-factor authentication, SMS is suitable.
Yes, you can register an alphanumeric sender ID (e.g., 'MyCompany') with MessageBird and use it as the originator for SMS messages. Keep in mind that country-specific regulations apply and must be adhered to.
Create a .env file in your project's root directory. Add your MessageBird API key (live or test) and originator (phone number or alphanumeric ID) to this file as environment variables (MESSAGEBIRD_API_KEY and MESSAGEBIRD_ORIGINATOR). Import and configure ConfigModule in app.module.ts to access them in your application via ConfigService.
The recommended structure includes an SMS module that houses an SMS service and controller. A DTO (Data Transfer Object) is defined to validate incoming requests to the controller's API endpoint (POST /sms/send). These components interact to handle SMS sending logic, validation, and API requests.
These libraries simplify validation and transformation in NestJS. class-validator provides decorators for defining validation rules on DTOs. class-transformer transforms plain JavaScript objects into class instances, applying the validation rules seamlessly.
Use npm run start:dev to start the server and send test requests using a tool like curl or Postman to the /sms/send endpoint. You can also write unit tests for your SMS service using the NestJS testing framework, mocking the MessageBird SDK and ConfigService to isolate your tests.
Common errors include "Authentication failed" (incorrect API key), "message could not be sent because the originator is invalid" (incorrect sender ID), "Request parameter validation failed" (invalid recipient format), and "No (suitable) routes found" (insufficient balance or coverage issues). Thorough logging and error handling help address them.
Use try...catch blocks to handle errors from the MessageBird SDK. Log errors using Logger to track failures and issues. For API errors, throw HttpException to return meaningful HTTP responses to clients (e.g., 500 Internal Server Error or 400 Bad Request), including specific details from the error response if possible.
Never hardcode API keys in your code. Store them in a .env file (and add it to .gitignore), and use environment variables or secure configuration services, especially in production deployments. Do not commit API keys to version control. Rotating keys regularly enhances security.
Implement rate limiting using @nestjs/throttler to prevent abuse. Use input validation with class-validator to prevent injection attacks. For public-facing APIs, add authentication mechanisms like JWT to protect the endpoint.
The ValidationPipe in NestJS automatically validates incoming requests against the defined DTOs, ensuring data integrity and security. It returns 400 Bad Request errors for invalid input and transforms plain objects into DTO instances, applying the validation rules.
Build your application using npm run build, creating a dist folder with optimized code. Deploy this folder, along with necessary dependencies and configuration files, to your hosting environment. Manage the process in production using tools like pm2 or platform-specific mechanisms. Ensure environment variables are set securely in the production environment, and do not include the .env file in the deployment.
This guide provides a step-by-step walkthrough for building a production-ready NestJS application capable of sending SMS messages using the MessageBird API and its Node.js SDK. We will cover project setup, core implementation, configuration, error handling, security, testing, and deployment considerations.
By the end of this guide, you will have a robust API endpoint that accepts a recipient phone number and a message body, validates the input, and uses MessageBird to reliably deliver the SMS. This solves the common need for programmatic SMS notifications, alerts, or basic communication features within a modern web application.
Time to Complete: 30-45 minutes Skill Level Required: Intermediate (familiarity with TypeScript, Node.js, REST APIs, and basic NestJS concepts)
Project Overview and Goals
Goal: Create a simple NestJS API endpoint (
POST /sms/send) to send outbound SMS messages via MessageBird.Problem Solved: Enables applications to programmatically send SMS messages for various purposes like notifications, verification codes (though MessageBird's Verify API is often better suited for OTP), or simple alerts.
Common Use Cases:
Cost Implications: MessageBird operates on a prepaid model with per-message pricing that varies by destination country. Typical costs range from $0.02-$0.10 per SMS depending on the country. Free trial accounts receive 10 test SMS credits that can only send to your verified phone number (source).
Technologies:
npm install messagebird.forbidUnknownValuesfromfalsetotrue, which may affect existing implementations.Architecture:
Prerequisites:
originator. Important: Some countries (e.g., United States) prohibit alphanumeric sender IDs and require a numeric phone number. See MessageBird's sender ID documentation for country-specific restrictions.1. Setting up the Project
Let's initialize our NestJS project and install the necessary dependencies.
Install NestJS CLI: If you don't have it, install the NestJS command-line interface globally.
Create New NestJS Project: Generate a new project using the CLI.
Choose your preferred package manager (npm or yarn) when prompted.
Navigate to Project Directory:
cd nestjs-messagebird-smsInstall Dependencies: We need the MessageBird SDK, and packages for configuration management and validation.
# Using npm npm install messagebird @nestjs/config dotenv class-validator class-transformer # Using yarn yarn add messagebird @nestjs/config dotenv class-validator class-transformermessagebird: The official Node.js SDK for the MessageBird API.@nestjs/config: Handles environment variables gracefully within NestJS.dotenv: Loads environment variables from a.envfile intoprocess.env. Used by@nestjs/config.class-validator&class-transformer: Enable easy implementation of validation rules using decorators on DTOs (Data Transfer Objects).Package Version Compatibility:
Common Installation Troubleshooting:
node -v. MessageBird SDK requires Node.js >= 0.10, but NestJS requires Node.js >= 14.x. Upgrade Node.js if needed using nvm or download from nodejs.org.sudo npm install -g @nestjs/clion macOS/Linux or run your terminal as administrator on Windows. Alternatively, configure npm to use a user-level directory for global packages.npm cache clean --forceand deletenode_modulesandpackage-lock.json, then reinstall.Project Structure: The Nest CLI sets up a standard structure (
src/containsapp.module.ts,app.controller.ts,app.service.ts,main.ts). We will create dedicated modules, services, and controllers for SMS functionality.2. Configuring Environment Variables
Sensitive information like API keys should never be hardcoded. We'll use environment variables.
Get MessageBird API Key:
Developerssection in the left-hand menu.API access.test_): Simulate SMS sending without actual delivery or charges. Messages appear as "sent" but no real SMS is delivered.live_): Send actual messages, consume account balance, and deliver to real phone numbers. Requires a topped-up prepaid balance.Get MessageBird Originator:
+12025550134). Ensure it's SMS-enabled.MyCompany) registered and approved in your MessageBird account. Note: Alphanumeric sender IDs are subject to country-specific regulations and restrictions. For example, the United States requires numeric phone numbers and does not allow alphanumeric originators. Pre-registration may be required in certain countries.Create
.envFile: In the root directory of your project, create a file named.env.touch .envAdd Variables to
.env: Add your MessageBird API key and originator to the file. Replace placeholders with your actual values.Ignore
.envFile: Add.envto your.gitignorefile to prevent accidentally committing secrets.Configure NestJS ConfigModule: Import and configure
ConfigModulein your main application module (src/app.module.ts) to make environment variables accessible throughout the application via dependency injection.// 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 { SmsModule } from './sms/sms.module'; // We will create this next @Module({ imports: [ ConfigModule.forRoot({ // Configure ConfigModule isGlobal: true, // Make ConfigService available globally envFilePath: '.env', // Specify the env file path }), SmsModule, // Import our SMS module ], controllers: [AppController], providers: [AppService], }) export class AppModule {}isGlobal: truemakes theConfigServiceavailable in any module without needing to importConfigModuleeverywhere.envFilePath: '.env'tells the module where to find the environment file.3. Implementing Core SMS Functionality (Service)
We'll create a dedicated module and service to handle SMS logic.
Generate SMS Module and Service: Use the Nest CLI.
This creates
src/sms/sms.module.tsandsrc/sms/sms.service.ts(along with a test file).Implement SmsService: Open
src/sms/sms.service.tsand add the logic to interact with the MessageBird SDK.// src/sms/sms.service.ts import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as MessageBird from 'messagebird'; // Import MessageBird SDK types if available, or use as namespace @Injectable() export class SmsService { private readonly logger = new Logger(SmsService.name); private messagebird: MessageBird.MessageBird; // Declare messagebird client instance using SDK types private originator: string; constructor(private configService: ConfigService) { // Retrieve API key and originator from environment variables const apiKey = this.configService.get<string>('MESSAGEBIRD_API_KEY'); this.originator = this.configService.get<string>('MESSAGEBIRD_ORIGINATOR'); if (!apiKey || !this.originator) { this.logger.error('MessageBird API Key or Originator not configured in .env file'); throw new Error('MessageBird environment variables missing.'); } // Initialize the MessageBird client // The require('messagebird')(key) pattern is common for initializing CJS modules like this SDK. // Using 'any' here if specific types for the initializer function aren't readily available or inferred. try { const mbInitializer: any = require('messagebird'); this.messagebird = mbInitializer(apiKey); this.logger.log('MessageBird SDK initialized successfully.'); } catch (error) { this.logger.error('Failed to initialize MessageBird SDK', error); throw error; // Re-throw to prevent service usage if initialization fails } } /** * Sends an SMS message using the MessageBird API. * @param recipient - The recipient's phone number in E.164 format (e.g., 14155552671). * Note: MessageBird API expects numbers WITHOUT the '+' prefix. * The SDK typically handles this, but ensure numbers are in international format. * @param body - The text content of the SMS message. * @returns Promise resolving with the MessageBird API response or rejecting with an error. */ async sendSms(recipient: string, body: string): Promise<any> { // Consider defining a stricter return type based on expected MessageBird response const params: MessageBird.MessageParameters = { originator: this.originator, recipients: [recipient], // Expects an array of recipients body: body, }; this.logger.log(`Attempting to send SMS to ${recipient} from ${this.originator}`); // The MessageBird SDK's messages.create uses a callback. // We wrap it in a Promise for modern async/await usage. return new Promise((resolve, reject) => { this.messagebird.messages.create(params, (err, response) => { if (err) { this.logger.error(`Failed to send SMS to ${recipient}`, err); // Provide more context if available in the error object const errorDetails = err.errors ? JSON.stringify(err.errors) : err.message; reject(new Error(`MessageBird API Error: ${errorDetails}`)); } else { this.logger.log(`SMS sent successfully to ${recipient}. Message ID: ${response.id}`); // The response contains details about the sent message(s) resolve(response); } }); }); } }ConfigServiceto access environment variables.Loggerprovides built-in NestJS logging.constructorretrieves the API key and originator, then initializes themessagebirdclient. Robust error handling is added for missing variables or initialization failure. The comment clarifies the use ofrequireandanyfor CJS interop.sendSmsMethod:recipientandbodyas arguments.paramsobject required bymessagebird.messages.create, using the configuredoriginator.Promisefor compatibility with NestJS's async/await patterns.Logger.messages.createincludes:id: Unique message identifier (string)href: URL to retrieve message detailsdirection: "mt" (mobile terminated - sent to mobile)originator: The sender ID usedbody: Message contentrecipients: Object with delivery status detailstotalCount: Total recipient counttotalSentCount: Count of messages senttotalDeliveredCount: Count of delivered messagesitems: Array of per-recipient status objects includingrecipient,status,statusReason,messageLength,messagePartCount,priceUpdate SmsModule: Ensure
SmsServiceis listed as a provider insrc/sms/sms.module.ts. Nest CLI usually does this automatically.// src/sms/sms.module.ts import { Module } from '@nestjs/common'; import { SmsService } from './sms.service'; // If we add a controller later, import it here import { SmsController } from './sms.controller'; // Import controller @Module({ controllers: [SmsController], // Add controller when created providers: [SmsService], exports: [SmsService], // Export service if needed by other modules }) export class SmsModule {}4. Building the API Layer (Controller and DTO)
Now, let's create the API endpoint to trigger the
SmsService.Generate SMS Controller:
This creates
src/sms/sms.controller.ts.Create Request DTO: Create a Data Transfer Object (DTO) to define the expected shape and validation rules for the request body. Create a file
src/sms/dto/send-sms.dto.ts.// src/sms/dto/send-sms.dto.ts import { IsNotEmpty, IsString, MinLength, IsPhoneNumber } from 'class-validator'; export class SendSmsDto { @IsNotEmpty({ message: 'Recipient phone number is required.' }) // Use IsPhoneNumber for stricter E.164 validation. // Requires installing libphonenumber-js: run 'npm install libphonenumber-js' or 'yarn add libphonenumber-js' // @IsPhoneNumber(null, { message: 'Recipient must be a valid phone number (E.164 format recommended).' }) @IsString() // Use IsString for a basic check if libphonenumber-js is not installed readonly recipient: string; @IsNotEmpty({ message: 'Message body cannot be empty.' }) @IsString() @MinLength(1, { message: 'Message body must contain at least 1 character.' }) readonly body: string; }class-validator(@IsNotEmpty,@IsString,@MinLength, optionally@IsPhoneNumber) to define validation rules.libphonenumber-jsif@IsPhoneNumberis used.datacodingparameter can be set toplain(GSM-7),unicode, orauto(default - automatically detects based on content). See MessageBird SMS API - datacoding.Implement SmsController: Open
src/sms/sms.controller.tsand define the endpoint.// src/sms/sms.controller.ts import { Controller, Post, Body, UsePipes, ValidationPipe, Logger, HttpException, HttpStatus } from '@nestjs/common'; import { SmsService } from './sms.service'; import { SendSmsDto } from './dto/send-sms.dto'; @Controller('sms') // Route prefix for this controller export class SmsController { private readonly logger = new Logger(SmsController.name); constructor(private readonly smsService: SmsService) {} @Post('send') // Route: POST /sms/send @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) // Enable validation using the DTO async sendSms(@Body() sendSmsDto: SendSmsDto) { this.logger.log(`Received request to send SMS: ${JSON.stringify(sendSmsDto)}`); try { const result = await this.smsService.sendSms(sendSmsDto.recipient, sendSmsDto.body); // Successfully sent (or at least accepted by MessageBird) return { message: 'SMS submitted successfully.', data: { messageId: result.id, // Or relevant data from the MessageBird response recipients: result.recipients.totalSentCount, }, }; } catch (error) { this.logger.error(`Error sending SMS: ${error.message}`, error.stack); // Throw standard HTTP exceptions throw new HttpException( `Failed to send SMS: ${error.message}`, HttpStatus.INTERNAL_SERVER_ERROR, // Or BAD_GATEWAY if it's clearly an external API issue ); } } }@Controller('sms'): Sets the base path for all routes in this controller to/sms.@Post('send'): Defines a handler forPOSTrequests to/sms/send.@Body() sendSmsDto: SendSmsDto: Injects the validated request body into thesendSmsDtoparameter.@UsePipes(new ValidationPipe(...)): Automatically validates the incoming body against theSendSmsDto.transform: true: Automatically transforms the incoming plain object to an instance ofSendSmsDto.whitelist: true: Strips any properties from the request body that are not defined in the DTO.try...catchblock handles potential errors from theSmsServiceand throws anHttpExceptionwith an appropriate status code and message.Update SmsModule: Add the
SmsControllerto thecontrollersarray insrc/sms/sms.module.ts.// src/sms/sms.module.ts import { Module } from '@nestjs/common'; import { SmsService } from './sms.service'; import { SmsController } from './sms.controller'; // Import the controller @Module({ controllers: [SmsController], // Add the controller providers: [SmsService], exports: [SmsService], }) export class SmsModule {}Enable Validation Globally (Optional but Recommended): Instead of applying
@UsePipesto every controller method, you can enable theValidationPipeglobally insrc/main.ts.// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { Logger, ValidationPipe } from '@nestjs/common'; // Import ValidationPipe async function bootstrap() { const app = await NestFactory.create(AppModule); const logger = new Logger('Bootstrap'); // Create a logger instance // Enable global validation pipe app.useGlobalPipes(new ValidationPipe({ transform: true, // Automatically transform payloads to DTO instances whitelist: true, // Strip properties not defined in DTO forbidNonWhitelisted: true, // Throw error if non-whitelisted properties are present transformOptions: { enableImplicitConversion: true, // Allow basic type conversions }, })); // Configure global prefix (optional) // app.setGlobalPrefix('api/v1'); const port = process.env.PORT || 3000; await app.listen(port); logger.log(`Application listening on port ${port}`); // Removed log related to /api documentation as Swagger setup is not included. } bootstrap();If you enable it globally, you can remove the
@UsePipes(...)decorator from theSmsController.5. Testing the API Endpoint
Let's verify our implementation.
Start the Application:
This starts the NestJS application in watch mode. Look for the log message indicating the port it's running on (usually 3000).
Send a Test Request (Using
curl): Open a new terminal window and usecurl(or a tool like Postman or Insomnia) to send a POST request. ReplaceYOUR_RECIPIENT_NUMBERwith a valid phone number (use your own for testing with a live key, or any validly formatted number for a test key).curl -X POST http://localhost:3000/sms/send \ -H ""Content-Type: application/json"" \ -d '{ ""recipient"": ""YOUR_RECIPIENT_NUMBER"", ""body"": ""Hello from my NestJS MessageBird App!"" }'Check Response:
Success: You should receive a JSON response similar to:
{ ""message"": ""SMS submitted successfully."", ""data"": { ""messageId"": ""some-messagebird-message-id"", ""recipients"": 1 } }Check your application console logs for success messages. If using a live key, check the recipient phone for the SMS. Check the MessageBird Dashboard logs (Logs -> Messages) to see the status.
Validation Error: If you send invalid data (e.g., missing
body), you'll get a 400 Bad Request response detailing the errors:{ ""statusCode"": 400, ""message"": [ ""Message body cannot be empty."", ""Message body must contain at least 1 character."" ], ""error"": ""Bad Request"" }API Error: If MessageBird rejects the request (e.g., invalid API key, insufficient balance, invalid originator), you'll get a 500 Internal Server Error (or the status code you configured in the controller's
HttpException):{ ""statusCode"": 500, ""message"": ""Failed to send SMS: MessageBird API Error: Authentication failed"" }Check your application console logs for detailed error messages from the
SmsService.6. Error Handling and Logging
We've already incorporated basic logging and error handling:
SmsService(including MessageBird API errors) and translates them into appropriate HTTP responses (e.g., 500 Internal Server Error, 502 Bad Gateway).errobject often contains anerrorsarray with more specific details from the MessageBird API (code, description, parameter). LoggingJSON.stringify(err.errors)can be very helpful for debugging.Common MessageBird Error Codes:
.envFull error code reference: MessageBird SMS Errors
Retry Mechanisms (Advanced): For critical SMS, you might implement retries with exponential backoff, especially for transient network errors or temporary MessageBird API issues (e.g., rate limiting). Libraries like
async-retryor NestJS scheduler features (@nestjs/schedule) could be used. Example retry logic for 429 errors:async sendSmsWithRetry(recipient: string, body: string, maxRetries = 3): Promise<any> { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await this.sendSms(recipient, body); } catch (error) { if (error.message.includes('429') && attempt < maxRetries - 1) { const delay = Math.pow(2, attempt) * 1000; // Exponential backoff this.logger.warn(`Rate limit hit, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, delay)); } else { throw error; // Re-throw if not retryable or max retries reached } } } }7. Security Considerations
API Key Security:
.envin.gitignore).Input Validation: Already implemented using
class-validatorDTOs. This prevents malformed requests and basic injection attempts in the body parameters. Always validate and sanitize any user-provided input.Rate Limiting: Protect your API endpoint from abuse. Use
@nestjs/throttlerto limit the number of requests per IP address or user within a specific timeframe.Configure it in
app.module.ts:// src/app.module.ts import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler'; import { APP_GUARD } from '@nestjs/core'; // Import APP_GUARD import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { SmsModule } from './sms/sms.module'; @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }), ThrottlerModule.forRoot([{ // Configure rate limiting ttl: 60000, // Time window in milliseconds (e.g., 60 seconds) limit: 10, // Max requests per window per IP }]), SmsModule, ], controllers: [AppController], providers: [ AppService, { // Apply ThrottlerGuard globally provide: APP_GUARD, useClass: ThrottlerGuard, }, ], }) export class AppModule {}MessageBird Rate Limits: MessageBird API enforces the following rate limits (source):
Retry-Afterheader if provided in 429 responses.Authentication/Authorization: This guide assumes the API endpoint might be internal or protected by other means (e.g., API Gateway, firewall). For public-facing APIs, implement proper authentication (e.g., JWT, API Keys specific to your API consumers) to ensure only authorized clients can trigger SMS sending.
Originator Restrictions: Be aware of country-specific regulations regarding Alphanumeric Sender IDs and ensure your chosen originator is compliant and approved.
GDPR and Data Retention: If operating in the EU or handling EU citizen data, ensure compliance with GDPR:
8. Unit Testing the Service
NestJS encourages testing. Let's write a basic unit test for
SmsService.Open Test File: Navigate to
src/sms/sms.service.spec.ts. The CLI generates a basic shell.Write Unit Tests: Mock dependencies (
ConfigService,messagebirdSDK) and test thesendSmsmethod logic.// src/sms/sms.service.spec.ts import { Test, TestingModule } from '@nestjs/testing'; import { SmsService } from './sms.service'; import { ConfigService } from '@nestjs/config'; // Logger is not explicitly needed for these tests, so it's removed from providers // Mock the messagebird library const mockMessageBirdCreate = jest.fn(); const mockMessageBirdClient = { messages: { create: mockMessageBirdCreate, }, }; // Mock the require('messagebird') call jest.mock('messagebird', () => { // This mocks the function returned by require('messagebird') return jest.fn().mockImplementation(() => mockMessageBirdClient); }, { virtual: true }); // Use virtual mock for require describe('SmsService', () => { let service: SmsService; let configService: ConfigService; // Mock ConfigService values const mockConfigService = { get: jest.fn((key: string) => { if (key === 'MESSAGEBIRD_API_KEY') return 'test_api_key'; if (key === 'MESSAGEBIRD_ORIGINATOR') return '+1234567890'; return null; }), }; beforeEach(async () => { // Reset mocks before each test mockMessageBirdCreate.mockClear(); // Also clear mocks on the module factory itself if require was mocked (require('messagebird') as jest.Mock).mockClear(); const module: TestingModule = await Test.createTestingModule({ providers: [ SmsService, { provide: ConfigService, useValue: mockConfigService, // Use the mock implementation }, // Logger removed from providers as it's not directly injected/tested here ], }).compile(); service = module.get<SmsService>(SmsService); configService = module.get<ConfigService>(ConfigService); // Get instance if needed for assertions }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('sendSms', () => { const recipient = '+9876543210'; const body = 'Test message'; const originator = '+1234567890'; // Expected from mockConfigService it('should call messagebird.messages.create with correct parameters and resolve on success', async () => { const mockResponse = { id: 'mock-message-id', recipients: { totalSentCount: 1 } }; // Configure the mock implementation for the SDK call for this test mockMessageBirdCreate.mockImplementation((params, callback) => { callback(null, mockResponse); // Simulate success }); await expect(service.sendSms(recipient, body)).resolves.toEqual(mockResponse); expect(mockMessageBirdCreate).toHaveBeenCalledTimes(1); expect(mockMessageBirdCreate).toHaveBeenCalledWith( { originator: originator, recipients: [recipient], body: body, }, expect.any(Function), // The callback function ); }); it('should reject with an error if messagebird.messages.create fails', async () => { const mockError = { message: "API Error", errors: [{ code: 2, description: 'Request parameter validation failed', parameter: 'recipients' }] }; // Configure the mock implementation for the SDK call for this test mockMessageBirdCreate.mockImplementation((params, callback) => { callback(mockError, null); // Simulate error }); await expect(service.sendSms(recipient, body)).rejects.toThrow( `MessageBird API Error: ${JSON.stringify(mockError.errors)}` // Check for the formatted error ); expect(mockMessageBirdCreate).toHaveBeenCalledTimes(1); expect(mockMessageBirdCreate).toHaveBeenCalledWith( { originator: originator, recipients: [recipient], body: body, }, expect.any(Function), ); }); it('should throw an error during initialization if API key is missing', async () => { // Override config mock for this specific scenario const missingKeyConfig = { get: jest.fn((key: string) => { if (key === 'MESSAGEBIRD_ORIGINATOR') return '+1234567890'; return null; // Simulate missing API key }) }; // We need to test the module compilation which triggers the constructor await expect(Test.createTestingModule({ providers: [ SmsService, { provide: ConfigService, useValue: missingKeyConfig }, // Logger not needed here either ], }).compile()).rejects.toThrow('MessageBird environment variables missing.'); }); }); });jest.fn()andjest.mockto create mock implementations ofConfigServiceand themessagebirdSDK.Loggerhas been removed from the test moduleprovidersfor cleanliness, as it wasn't actively used in the tests.Run Tests:
npm run testOr for a specific file:
npm run test -- src/sms/sms.service.spec.ts9. Troubleshooting and Caveats
Common Error Messages and Solutions:
Authentication failedMESSAGEBIRD_API_KEYin.env, check for typos/spaces, ensure correct key type (test/live)message could not be sent because the originator is invalidMESSAGEBIRD_ORIGINATORmatches a purchased VMN or approved alphanumeric sender ID. Check country restrictions.Request parameter validation failed(recipients)+). Example:14155552671for US number.No (suitable) routes found429 Too Many RequestsAuthentication failed: Double-check yourMESSAGEBIRD_API_KEYin.env. Ensure you're using the correct key (live vs. test) and that it hasn't been revoked. Verify there are no typos or extra spaces.message could not be sent because the originator is invalid: EnsureMESSAGEBIRD_ORIGINATORin.envmatches a purchased number or a registered and approved Alphanumeric Sender ID in your MessageBird account. Check country restrictions for Alphanumeric Sender IDs.Request parameter validation failed(Parameter:recipients): Ensure the recipient number is in a valid format. While MessageBird is somewhat flexible, E.164 format (country code and number without '+', e.g.,14155552671) is strongly recommended. The SDK expectsrecipientsas an array. The MessageBird API expects numbers in international format without the '+' symbol, though the SDK may handle normalization.No (suitable) routes found: You might have insufficient balance (for live keys) or the recipient country/network might not be supported by your MessageBird account setup or the originator type. Check your balance and MessageBird's coverage documentation.Promisewrapper is crucial for cleanasync/awaitusage in NestJS. Errors in the promise handling logic can mask SDK errors.messagebirdpackage, as APIs and methods can change. Checkpackage.jsonand compare with npm registry.10. Deployment and CI/CD
Build: Create a production build:
This compiles TypeScript to JavaScript in the
distfolder.Run Production: Start the application using Node.js directly on the compiled output:
Environment Variables: Ensure your production environment (e.g., Docker container, PaaS like Heroku/Vercel/AWS Elastic Beanstalk, VM) has the
MESSAGEBIRD_API_KEYandMESSAGEBIRD_ORIGINATORenvironment variables set securely. Do not include the.envfile in your production artifact; use the platform's mechanism for environment variables.Docker Containerization Example:
CI/CD Pipeline Example (GitHub Actions):
# .github/workflows/deploy.yml name: Build and Deploy on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' cache: 'npm' - run: npm ci - run: npm run lint - run: npm run test - run: npm run build - name: Deploy to production env: MESSAGEBIRD_API_KEY: ${{ secrets.MESSAGEBIRD_API_KEY }} MESSAGEBIRD_ORIGINATOR: ${{ secrets.MESSAGEBIRD_ORIGINATOR }} run: | # Add your deployment commands here # Example: scp, rsync, or platform-specific CLIProcess Management: Use a process manager like
pm2or rely on your platform's built-in service management (e.g., systemd, Docker orchestration) to keep the Node.js application running reliably, handle restarts, and manage logs.Health Check Endpoint: Add a health check endpoint for monitoring:
// src/app.controller.ts @Get('health') healthCheck() { return { status: 'ok', timestamp: new Date().toISOString() }; }Frequently Asked Questions
How do I send SMS messages with MessageBird in NestJS?
To send SMS with MessageBird in NestJS, install the MessageBird Node.js SDK (
npm install messagebird), create a service that wraps the SDK'smessages.createmethod with promises, and use NestJS dependency injection to access it from your controller. Configure your API key via@nestjs/configand validate requests using DTOs with class-validator.What phone number format does MessageBird require?
MessageBird requires phone numbers in E.164 international format without the '+' prefix (e.g.,
14155552671for a US number). The MessageBird Node.js SDK typically handles format normalization, but providing properly formatted numbers ensures reliable delivery and reduces API errors.Can I use alphanumeric sender IDs with MessageBird in all countries?
No, alphanumeric sender IDs are not supported in all countries. The United States, for example, prohibits alphanumeric sender IDs and requires a registered numeric phone number. Check MessageBird's sender ID documentation for country-specific restrictions before configuring your originator.
How do I handle MessageBird API errors in NestJS?
Wrap MessageBird SDK callbacks in promises and use try-catch blocks in your service methods. Map MessageBird API errors to appropriate HTTP status codes using NestJS
HttpExceptionclasses. Common errors include authentication failures (401), parameter validation errors (400), and insufficient balance or routing issues.What's the difference between MessageBird test and live API keys?
Test API keys allow you to simulate SMS sending without actual delivery or charges. Messages appear as "sent" in your code, but no real SMS is delivered. Live API keys send actual messages, consume your account balance, and deliver to real phone numbers. Always use test keys during development.
How do I validate phone numbers in NestJS before sending SMS?
Use class-validator decorators in your DTO, such as
@IsString()and@Matches()with an E.164 regex pattern. For more robust validation, integrate libphonenumber-js to verify phone number validity, format, and country code before passing to the MessageBird API.What dependencies do I need for MessageBird NestJS integration?
You need
@nestjs/core,@nestjs/common,messagebird(the official Node.js SDK),@nestjs/configfor environment variables,class-validatorandclass-transformerfor request validation, and optionallylibphonenumber-jsfor advanced phone number validation.How do I deploy a MessageBird NestJS application to production?
Build your NestJS application with
npm run build, setMESSAGEBIRD_API_KEYandMESSAGEBIRD_ORIGINATORas environment variables in your hosting platform (not in a committed.envfile), and use a process manager like pm2 or platform-native service management. Implement proper error logging, rate limiting, and monitoring for production reliability.Related Resources
For more comprehensive guides on SMS integration and phone number formatting: