Frequently Asked Questions
Create a NestJS service that integrates with the MessageBird Verify API using their Node.js SDK. This service should handle sending the OTP to the user's phone number via SMS after validating the number format. The service will interact with MessageBird's API to generate and send the OTP, returning a verification ID to your application.
MessageBird's Verify API is a service for sending and verifying one-time passwords (OTPs) via SMS, commonly used for two-factor authentication (2FA). The API handles generating the OTP, delivering it to the user's phone number, and verifying user-submitted tokens, enhancing application security by adding an extra layer of verification beyond traditional passwords.
NestJS provides a structured and scalable framework for building server-side applications in Node.js, making it ideal for implementing OTP verification. It offers features like dependency injection, configuration management, and a modular architecture, making development more efficient and easier to maintain. Its support for TypeScript further improves code quality.
You can install the MessageBird Node.js SDK using either npm or yarn with the command `npm install messagebird` or `yarn add messagebird`. This SDK simplifies interaction with the MessageBird API, allowing your NestJS application to easily send SMS messages for OTP verification.
Store your MessageBird API key (test key for development, live key for production) in a `.env` file in your project root. Use the `@nestjs/config` module to load these environment variables securely into your NestJS application. Never commit `.env` to version control.
Class-validator, along with class-transformer, facilitates request data validation in NestJS. By defining Data Transfer Objects (DTOs) and decorating properties with validation rules, you ensure data integrity and prevent invalid data from reaching your service logic. This adds a layer of security and reliability.
Create a controller in your OTP module, define a `POST /otp/verify` endpoint, and inject the `OtpService`. Use the `@Body()` decorator and a DTO (VerifyOtpDto) to validate incoming data containing the verification ID and token. The controller calls the service's `verifyOtp` method, then returns the status ('verified' or an error) back to the client.
In your OTP controller, create a `POST /otp/send` endpoint and inject the `OtpService`. Use the `@Body()` decorator with a DTO (SendOtpDto) to validate the incoming phone number. The controller should then call the service's `sendOtp` method, returning the verification ID to the client.
The verification ID, returned by MessageBird's `verify.create` call, is a unique identifier associated with a specific OTP request. It links the OTP sent to the user with the subsequent verification process. This ID is then used along with the user-submitted OTP to complete the verification.
Implement comprehensive error handling in your `OtpService` by mapping MessageBird API error codes to appropriate HTTP exceptions. For example, an invalid token should return a 400 Bad Request, and an unexpected API failure should return a 500 Internal Server Error. Log errors with details to facilitate debugging.
Use `class-validator`'s `@IsPhoneNumber()` decorator within the `SendOtpDto` to validate the phone number format before it reaches your service logic. Handle any remaining format issues or MessageBird-specific number errors (like code 21) in the `OtpService` by throwing a `BadRequestException` with a clear message.
Only use your live MessageBird API key in your production environment. During development and testing, use the test key. This prevents accidental charges and allows you to use MessageBird's test phone numbers. Remember to switch to the live key for real OTP delivery when deploying to production.
Implement phone number normalization in your `OtpService` to handle variations in user input (spaces, dashes, parentheses). A basic `replace` function can handle simple cases, or consider using a dedicated phone number formatting library for more robust normalization across international formats.
This guide provides a step-by-step walkthrough for integrating MessageBird's Verify API into a NestJS application to implement robust SMS-based Two-Factor Authentication (2FA) or phone number verification using One-Time Passwords (OTPs).
We'll build a secure and scalable backend API that handles sending OTPs via SMS and verifying user-submitted tokens. This enhances application security by adding an extra layer of verification beyond traditional passwords, confirming user possession of a specific phone number.
Project Overview and Goals
What We'll Build:
Problem Solved:
This implementation addresses the need for verifying user phone numbers and adding a 2FA layer to applications. It helps prevent fraudulent account creation, secures user accounts against unauthorized access, and verifies key transactions by confirming control over a registered phone number.
Technologies Used:
System Architecture:
Prerequisites:
npm install -g @nestjs/cli
).Final Outcome:
By the end of this guide, you will have a functional NestJS API capable of:
1. Setting up the Project
Let's initialize a new NestJS project and install the necessary dependencies.
Create NestJS Project: Open your terminal and run the NestJS CLI command to create a new project. Let's call it
nestjs-messagebird-otp
.Choose your preferred package manager (npm or yarn) when prompted.
Navigate to Project Directory:
Install Dependencies: We need several packages:
@nestjs/config
: For managing environment variables.messagebird
: The official MessageBird Node.js SDK.class-validator
,class-transformer
: For request data validation using DTOs.(Optional) Install Prisma Dependencies: If you plan to integrate with a database (e.g., to link verified numbers to users), install Prisma. This section is optional for the core OTP functionality.
Project Structure: NestJS provides a standard structure. Key directories we'll work with:
src/
: Contains application source code (main.ts
,app.module.ts
, etc.).src/otp/
: We'll create this module to encapsulate OTP logic..env
: We'll create this file for environment variables.(Optional) prisma/
: Contains Prisma schema and migrations if used.2. Environment Configuration
Securely managing API keys is crucial. We'll use the
@nestjs/config
module and a.env
file.Create
.env
file: Create a file named.env
in the project's root directory.Add MessageBird API Key: Obtain your API keys from the MessageBird Dashboard:
.env
file:Replace
YOUR_TEST_API_KEY_HERE
with your actual test key initially. Remember to switch to the live key in your production environment.Purpose: This variable holds the secret key required to authenticate requests to the MessageBird API. Using test keys avoids costs and real messages during development.
(Optional) Add Database URL: If using Prisma (optional feature), add your database connection string:
Adjust the URL according to your database credentials and provider.
Purpose: This variable tells Prisma how to connect to your database.
Load
ConfigModule
: Import and configureConfigModule
in your main application module (src/app.module.ts
) to make environment variables available throughout the app viaConfigService
. Make it global and load.env
variables.Why
isGlobal: true
? This makes theConfigService
available in any module without needing to importConfigModule
repeatedly.3. Implementing Core Functionality (OTP Service)
We'll create a dedicated module and service to handle the logic of interacting with the MessageBird API.
Generate OTP Module and Service: Use the NestJS CLI to generate the
otp
module and service.This creates
src/otp/otp.module.ts
andsrc/otp/otp.service.ts
(and spec file). NestJS automatically updatessrc/app.module.ts
to importOtpModule
.Implement
OtpService
: Opensrc/otp/otp.service.ts
. We will injectConfigService
to access the API key and instantiate the MessageBird client.new Promise
? The MessageBird SDK uses callbacks. We wrap the callback logic in a Promise to work seamlessly with NestJS's async/await syntax.BadRequestException
is thrown for known user-input issues or specific MessageBird error codes (like invalid number code 21, invalid token code 10).InternalServerErrorException
is used for unexpected API failures or missing IDs. Logging the full error (JSON.stringify(err)
) and specific codes helps diagnose issues.originator
should ideally be a purchased virtual number from MessageBird or a short alphanumeric code (check country restrictions).template
includes the mandatory%token
placeholder.phoneNumber.replace(/[\s\-()]/g, '')
to strip common characters before sending to MessageBird.Register
ConfigService
inOtpModule
: SinceConfigModule
is global andConfigService
is injected intoOtpService
(which is part ofOtpModule
), no explicit import ofConfigModule
or registration ofConfigService
is needed withinOtpModule
itself.4. Building the API Layer (OTP Controller)
Now, let's create the controller with endpoints that clients can call.
Generate OTP Controller:
This creates
src/otp/otp.controller.ts
(and spec file) and adds it toOtpModule
.Define Data Transfer Objects (DTOs): Create DTOs to define the expected request body structure and apply validation rules using
class-validator
.Create
src/otp/dto/send-otp.dto.ts
:Create
src/otp/dto/verify-otp.dto.ts
:Implement
OtpController
: Opensrc/otp/otp.controller.ts
. InjectOtpService
and define the endpoints.@Controller('otp')
: Sets the base route for all methods in this controller to/otp
.@Post('send')
,@Post('verify')
: Define POST endpoints at/otp/send
and/otp/verify
.@Body()
: Injects the request body.ValidationPipe
: (Applied globally below) Automatically validates the incoming request body against the DTO.@HttpCode(HttpStatus.OK)
: Ensures a200 OK
status is returned on success.Enable
ValidationPipe
Globally (Recommended): Enable the validation pipe globally insrc/main.ts
for cleaner controllers.With the global pipe enabled, the
@UsePipes(...)
decorators can be removed from the controller methods (as shown in the controller example above).Testing Endpoints with
curl
:Start the application:
npm run start:dev
Send OTP: Replace
+12345678900
with a real phone number (use a test number if using your test API key).Expected Response (Success):
Expected Response (Validation Error - e.g., invalid number format):
Verify OTP: Replace
some-verification-id-from-messagebird
with the ID you received, and123456
with the code sent to your phone (or the test code provided by MessageBird if using test keys).Expected Response (Success):
Expected Response (Incorrect Token):
5. Integrating with Third-Party Services (MessageBird)
We've covered the core integration points:
.env
(use Test key for dev, Live key for prod) and loaded via@nestjs/config
. Ensure the.env
file is never committed to version control (add it to.gitignore
).messagebird
SDK is initialized in theOtpService
constructor using the API key fromConfigService
. The type assertionas unknown as MessageBird.MessageBird
is included due to potential SDK typing nuances.verify.create
andverify.verify
methods are used withinOtpService
to interact with MessageBird.OtpService
around the MessageBird calls, especially for transient network errors (distinguish from user errors like invalid tokens).type: 'tts'
(Text-to-Speech). You could modifysendOtp
to offer this if SMS fails or as a preference.6. Implementing Error Handling and Logging
NestJS provides robust mechanisms for handling errors and logging.
Error Handling Strategy:
ValidationPipe
, returning400 Bad Request
.OtpService
throws specificHttpException
subclasses (BadRequestException
,InternalServerErrorException
) based on MessageBird API responses or internal issues. Specific MessageBird error codes (e.g., 10, 21) are mapped toBadRequestException
.500 Internal Server Error
.Logging:
Logger
(@nestjs/common
) inOtpService
,OtpController
, andmain.ts
.OtpService
logs specific MessageBird errors.pino
,nestjs-pino
) for easier parsing by log aggregation tools, and include correlation IDs for request tracing.Retry Mechanisms (Conceptual Example): Adding retries to
OtpService
calls can improve resilience against transient network issues.Caveat: The
isTransientError
function is critical and provided as a non-working placeholder. You must implement logic to correctly identify which errors (based on codes, status, or types) should trigger a retry. Retrying on user errors (like invalid number) is incorrect. The usage example withinsendOtp
is commented out to avoid making the code block non-functional due to the placeholderisTransientError
and context assumptions.7. (Optional) Creating a Database Schema and Data Layer (Prisma Example)
This section is optional and only relevant if you need to store user data or link verified numbers.
Initialize Prisma: If not done in Section 1, install Prisma dependencies and initialize:
Ensure your
.env
has the correctDATABASE_URL
.