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:
- A NestJS application with dedicated endpoints for initiating OTP requests and verifying submitted tokens.
- Integration with the MessageBird Verify API via their official Node.js SDK.
- Secure handling of API keys using NestJS configuration modules.
- Input validation for phone numbers and tokens.
- Robust error handling for common scenarios.
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:
- Node.js: The underlying JavaScript runtime environment.
- NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Chosen for its modular architecture, dependency injection, and built-in support for features like configuration management and validation.
- MessageBird: The third-party service provider for sending SMS OTPs via their Verify API. Chosen for its straightforward API and specific OTP verification features.
- MessageBird Node.js SDK: Simplifies interaction with the MessageBird API.
- TypeScript: Provides static typing for enhanced code quality and maintainability within NestJS.
- (Optional) Prisma: A modern ORM for database interaction (example included for context, but not strictly required for basic OTP flow).
System Architecture:
graph LR
A[User Client (Web/Mobile)] -- 1. Request OTP (POST /otp/send) --> B(NestJS API);
B -- 2. Call OtpService.sendOtp --> C(OtpService);
C -- 3. Call MessageBird SDK (verify.create) --> D(MessageBird Verify API);
D -- 4. Send SMS OTP --> E[User's Phone];
D -- 5. Return Verification ID --> C;
C -- 6. Return Verification ID --> B;
B -- 7. Send Verification ID back --> A;
A -- 8. Submit Token & ID (POST /otp/verify) --> B;
B -- 9. Call OtpService.verifyOtp --> C;
C -- 10. Call MessageBird SDK (verify.verify) --> D;
D -- 11. Return Verification Status --> C;
C -- 12. Return Status --> B;
B -- 13. Send Verification Result back --> A;
style B fill:#f9f,stroke:#333,stroke-width:2px
style C fill:#ccf,stroke:#333,stroke-width:2px
style D fill:#ff9,stroke:#333,stroke-width:2px
Prerequisites:
- Node.js (LTS version recommended, e.g., v18 or v20) and npm/yarn installed.
- A MessageBird account with API credentials (both test and live keys are recommended).
- Basic understanding of TypeScript, NestJS concepts (modules, controllers, services), and REST APIs.
- NestJS CLI installed (
npm install -g @nestjs/cli
). - (Optional) Docker and Docker Compose for database setup if using Prisma.
- (Optional) A PostgreSQL database instance (local or cloud-based) if implementing the database layer.
Final Outcome:
By the end of this guide, you will have a functional NestJS API capable of:
- Accepting a phone number via a POST request.
- Using MessageBird to send an SMS OTP to that number.
- Returning a unique verification ID to the client.
- Accepting the verification ID and the user-submitted OTP via another POST request.
- Using MessageBird to verify the token against the ID.
- Returning a success or failure status to the client.
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
.nest new nestjs-messagebird-otp
Choose your preferred package manager (npm or yarn) when prompted.
-
Navigate to Project Directory:
cd nestjs-messagebird-otp
-
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.
# Using npm npm install @nestjs/config messagebird class-validator class-transformer # Using yarn yarn add @nestjs/config messagebird class-validator class-transformer
-
(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.
# Using npm npm install prisma @prisma/client --save-dev # Using yarn yarn add prisma @prisma/client --dev
-
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:
- Log in to your MessageBird account.
- Navigate to the Developers section in the left sidebar.
- Go to the API access (REST) tab.
- You will find both Live and Test API keys.
- Test Key: Use this key during development and testing. It allows you to simulate API calls without sending actual SMS messages or incurring costs. You can usually test against specific MessageBird test numbers.
- Live Key: Use this key for your production application to send real SMS messages to users.
- Add the key you intend to use (start with the test key) to your
.env
file:
# .env # Use your TEST key for development, switch to LIVE key for production MESSAGEBIRD_API_KEY=YOUR_TEST_API_KEY_HERE
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:
# .env (Optional - only if using Prisma) # Example for PostgreSQL: postgresql://USER:PASSWORD@HOST:PORT/DATABASE DATABASE_URL=""postgresql://postgres:password@localhost:5432/otp_app?schema=public""
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.// 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 { OtpModule } from './otp/otp.module'; // We'll create this soon // import { PrismaModule } from './prisma.module'; // Import if using Prisma @Module({ imports: [ ConfigModule.forRoot({ // Load .env variables globally isGlobal: true, }), OtpModule, // Import our feature module // PrismaModule, // Add PrismaModule if using database (ensure it's defined) ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
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.nest generate module otp nest generate service otp
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.// src/otp/otp.service.ts import { Injectable, InternalServerErrorException, BadRequestException, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as MessageBird from 'messagebird'; // Import MessageBird SDK @Injectable() export class OtpService { private readonly logger = new Logger(OtpService.name); private messagebird: MessageBird.MessageBird; constructor(private configService: ConfigService) { const apiKey = this.configService.get<string>('MESSAGEBIRD_API_KEY'); if (!apiKey) { throw new Error('MessageBird API Key not found in environment variables.'); } // Initialize MessageBird client // Type assertion might be needed due to MessageBird SDK typings; this could change with SDK updates. this.messagebird = MessageBird(apiKey) as unknown as MessageBird.MessageBird; } /** * Sends an OTP code to the specified phone number via MessageBird Verify API. * @param phoneNumber The recipient's phone number in E.164 format (e.g., +14155552671) * @returns The verification ID from MessageBird. * @throws InternalServerErrorException if the API call fails unexpectedly. * @throws BadRequestException if MessageBird returns a specific user-related error (e.g., invalid number format - code 21). */ async sendOtp(phoneNumber: string): Promise<string> { // Basic normalization example: remove spaces, dashes, parentheses // Consider a more robust library for complex normalization if needed. const normalizedPhoneNumber = phoneNumber.replace(/[\s\-()]/g, ''); this.logger.log(`Sending OTP to normalized number: ${normalizedPhoneNumber}`); const params: MessageBird.VerifyCreateParams = { originator: 'VerifyApp', // Or your App Name (max 11 chars alphanumeric) or verified phone number template: 'Your verification code is %token.', type: 'sms', // tokenLength: 6, // Default is 6 // timeout: 30, // Default is 30 seconds }; return new Promise((resolve, reject) => { this.messagebird.verify.create(normalizedPhoneNumber, params, (err, response) => { if (err) { this.logger.error('MessageBird verify.create error:', JSON.stringify(err)); // Log the full error structure // Check for specific, actionable MessageBird error codes if (err.errors && err.errors.length > 0) { const mbError = err.errors[0]; this.logger.error(`MessageBird Error Code: ${mbError.code}, Description: ${mbError.description}`); // Example: Code 21 indicates an invalid phone number if (mbError.code === 21) { return reject(new BadRequestException(`Invalid phone number: ${mbError.description}`)); } // Add checks for other relevant codes if needed (e.g., originator issues) // Fallback for other specific MessageBird errors return reject(new BadRequestException(`Failed to send OTP: ${mbError.description}`)); } // Generic internal error if no specific code matched or error structure is unexpected return reject(new InternalServerErrorException('Failed to initiate OTP verification due to an unexpected error.')); } this.logger.log(`OTP request successful for ${normalizedPhoneNumber}. ID: ${response?.id}`); // Ensure response.id is treated as a string if (response && typeof response.id === 'string') { resolve(response.id); } else { this.logger.error('MessageBird verify.create response missing or invalid ID:', response); reject(new InternalServerErrorException('Failed to get a valid verification ID from MessageBird.')); } }); }); } /** * Verifies the OTP token submitted by the user against the verification ID. * @param id The verification ID received from sendOtp. * @param token The 6-digit token entered by the user. * @returns An object indicating successful verification. * @throws BadRequestException if the token is invalid or expired (e.g., MessageBird error code 10). * @throws InternalServerErrorException on other API errors. */ async verifyOtp(id: string, token: string): Promise<{ status: string }> { this.logger.log(`Verifying OTP for ID: ${id}`); return new Promise((resolve, reject) => { this.messagebird.verify.verify(id, token, (err, response) => { if (err) { this.logger.error(`MessageBird verify.verify error for ID ${id}:`, JSON.stringify(err)); // Handle specific errors, e.g., invalid token (code 10) if (err.errors && err.errors.length > 0) { const mbError = err.errors[0]; this.logger.error(`MessageBird Verify Error Code: ${mbError.code}, Description: ${mbError.description}`); if (mbError.code === 10) { // Code 10 typically means invalid token return reject(new BadRequestException('Invalid or expired OTP token.')); } // Fallback for other specific MessageBird errors during verify return reject(new BadRequestException(`Failed to verify OTP: ${mbError.description}`)); } // Generic internal error return reject(new InternalServerErrorException('Failed to verify OTP due to an unexpected error.')); } // Check the status if available if (response && response.status === 'verified') { this.logger.log(`OTP verification successful for ID: ${id}`); resolve({ status: 'verified' }); } else { // Log the actual status received if not 'verified' (e.g., 'expired', 'failed') this.logger.warn(`OTP verification for ID ${id} returned non-verified status: ${response?.status ?? 'unknown'}`); // Treat any non-verified status as a failure from the client's perspective reject(new BadRequestException('Invalid or expired OTP token.')); } }); }); } }
- Why
new Promise
? The MessageBird SDK uses callbacks. We wrap the callback logic in a Promise to work seamlessly with NestJS's async/await syntax. - Error Handling: We catch errors from the SDK. Specific
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. - Configuration:
originator
should ideally be a purchased virtual number from MessageBird or a short alphanumeric code (check country restrictions).template
includes the mandatory%token
placeholder. - Normalization: Added a basic example
phoneNumber.replace(/[\s\-()]/g, '')
to strip common characters before sending to MessageBird.
- Why
-
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.// src/otp/otp.module.ts import { Module } from '@nestjs/common'; import { OtpService } from './otp.service'; import { OtpController } from './otp.controller'; // We'll create this next @Module({ controllers: [OtpController], // Add controller here providers: [OtpService], exports: [OtpService], // Export if other modules need it }) export class OtpModule {}
4. Building the API Layer (OTP Controller)
Now, let's create the controller with endpoints that clients can call.
-
Generate OTP Controller:
nest generate controller otp
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
:// src/otp/dto/send-otp.dto.ts import { IsNotEmpty, IsPhoneNumber, IsString } from 'class-validator'; export class SendOtpDto { @IsNotEmpty() @IsString() @IsPhoneNumber(null) // Use null for generic E.164 format validation // Input might still contain formatting chars; normalization happens in the service. phoneNumber: string; }
-
Create
src/otp/dto/verify-otp.dto.ts
:// src/otp/dto/verify-otp.dto.ts import { IsNotEmpty, IsString, Length } from 'class-validator'; export class VerifyOtpDto { @IsNotEmpty() @IsString() // Add validation if MessageBird always returns IDs of a specific format/length id: string; @IsNotEmpty() @IsString() @Length(6, 6) // Assuming default 6-digit token token: string; }
-
-
Implement
OtpController
: Opensrc/otp/otp.controller.ts
. InjectOtpService
and define the endpoints.// src/otp/otp.controller.ts import { Controller, Post, Body, HttpCode, HttpStatus, ValidationPipe, UsePipes, Logger } from '@nestjs/common'; import { OtpService } from './otp.service'; import { SendOtpDto } from './dto/send-otp.dto'; import { VerifyOtpDto } from './dto/verify-otp.dto'; @Controller('otp') // Route prefix: /otp export class OtpController { private readonly logger = new Logger(OtpController.name); constructor(private readonly otpService: OtpService) {} @Post('send') // Endpoint: POST /otp/send @HttpCode(HttpStatus.OK) // Return 200 OK on success // If ValidationPipe is global, @UsePipes is not needed here // @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) async sendOtp(@Body() sendOtpDto: SendOtpDto): Promise<{ id: string }> { this.logger.log(`Received request to send OTP to ${sendOtpDto.phoneNumber}`); const verificationId = await this.otpService.sendOtp(sendOtpDto.phoneNumber); return { id: verificationId }; } @Post('verify') // Endpoint: POST /otp/verify @HttpCode(HttpStatus.OK) // Return 200 OK on successful verification // If ValidationPipe is global, @UsePipes is not needed here // @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) async verifyOtp(@Body() verifyOtpDto: VerifyOtpDto): Promise<{ status: string }> { this.logger.log(`Received request to verify OTP for ID: ${verifyOtpDto.id}`); return this.otpService.verifyOtp(verifyOtpDto.id, verifyOtpDto.token); } }
@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.// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe, Logger } from '@nestjs/common'; // Import Logger & ValidationPipe async function bootstrap() { const app = await NestFactory.create(AppModule); const logger = new Logger('Bootstrap'); // Enable global validation pipe app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip properties not in DTO transform: true, // Transform payload to DTO instances forbidNonWhitelisted: true, // Throw error if non-whitelisted properties are sent transformOptions: { enableImplicitConversion: true, // Allow auto-conversion of types }, })); // Enable CORS - By default allows all origins. // **IMPORTANT**: Restrict origins in production for security! app.enableCors(); // Example for production: // app.enableCors({ origin: 'https://your-frontend-app.com' }); logger.log('CORS enabled. Remember to restrict origins in production.'); const port = process.env.PORT || 3000; await app.listen(port); logger.log(`Application listening on port ${port}`); } bootstrap();
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).curl -X POST http://localhost:3000/otp/send \ -H ""Content-Type: application/json"" \ -d '{ ""phoneNumber"": ""+12345678900"" }'
Expected Response (Success):
{ ""id"": ""some-verification-id-from-messagebird"" }
Expected Response (Validation Error - e.g., invalid number format):
{ ""statusCode"": 400, ""message"": [ ""phoneNumber must be a valid phone number"" ], ""error"": ""Bad Request"" }
-
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).curl -X POST http://localhost:3000/otp/verify \ -H ""Content-Type: application/json"" \ -d '{ ""id"": ""some-verification-id-from-messagebird"", ""token"": ""123456"" }'
Expected Response (Success):
{ ""status"": ""verified"" }
Expected Response (Incorrect Token):
{ ""statusCode"": 400, ""message"": ""Invalid or expired OTP token."", ""error"": ""Bad Request"" }
-
5. Integrating with Third-Party Services (MessageBird)
We've covered the core integration points:
- Configuration: API key stored securely in
.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
). - SDK Initialization: The
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. - API Calls:
verify.create
andverify.verify
methods are used withinOtpService
to interact with MessageBird. - Dashboard Navigation for API Key: As mentioned in Section 2: MessageBird Dashboard -> Developers -> API access (REST) -> Copy/Create Test and Live API Keys.
- Fallback Mechanisms: The current implementation relies directly on MessageBird. For production robustness:
- Retries: Implement retry logic (see Section 6) within
OtpService
around the MessageBird calls, especially for transient network errors (distinguish from user errors like invalid tokens). - Alternative Providers: Consider integrating a secondary SMS provider as a fallback if MessageBird experiences prolonged outages (adds complexity).
- Voice Fallback: MessageBird Verify supports
type: 'tts'
(Text-to-Speech). You could modifysendOtp
to offer this if SMS fails or as a preference.
- Retries: Implement retry logic (see Section 6) within
6. Implementing Error Handling and Logging
NestJS provides robust mechanisms for handling errors and logging.
-
Error Handling Strategy:
- Validation Errors: Handled automatically by the global
ValidationPipe
, returning400 Bad Request
. - Service-Level Errors:
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
. - Unhandled Errors: NestJS catches unhandled exceptions and returns
500 Internal Server Error
.
- Validation Errors: Handled automatically by the global
-
Logging:
- We've used the built-in
Logger
(@nestjs/common
) inOtpService
,OtpController
, andmain.ts
. - NestJS logs basic request/response info and exceptions.
OtpService
logs specific MessageBird errors. - Enhancements: Consider structured logging (
pino
,nestjs-pino
) for easier parsing by log aggregation tools, and include correlation IDs for request tracing.
- We've used the built-in
-
Retry Mechanisms (Conceptual Example): Adding retries to
OtpService
calls can improve resilience against transient network issues.// Simplified conceptual retry logic for OtpService methods // Consider using a library like 'nestjs-retry' or 'async-retry' for production // Define logger at a scope accessible by isTransientError if needed, or pass it const logger = new Logger('RetryLogic'); async function callWithRetry<T>(action: () => Promise<T>, maxAttempts: number, logger: Logger): Promise<T> { let attempts = 0; while (attempts < maxAttempts) { attempts++; try { return await action(); // Attempt the action } catch (error) { logger.error(`Attempt ${attempts} failed: ${error.message}`_ error.stack); // Check if the error is potentially transient AND if more attempts remain if (isTransientError(error) && attempts < maxAttempts) { const delay = 1000 * Math.pow(2_ attempts - 1); // Exponential backoff logger.warn(`Retrying after ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); continue; // Go to the next iteration } else { // If not transient or max attempts reached, re-throw the error throw error; } } } // This line should theoretically not be reached if maxAttempts > 0 throw new InternalServerErrorException('Action failed after maximum retry attempts.'); } // **Placeholder Function - Requires Real Implementation** // This function MUST be implemented to identify errors that justify a retry // (e.g., network timeouts, specific 5xx errors from MessageBird if identifiable). // Do NOT retry on errors like 'invalid phone number' or 'invalid token'. function isTransientError(error: any): boolean { logger.warn('`isTransientError` check is currently a placeholder and always returns false. Implement real transient error detection logic.'); // Example checks (modify based on actual error structure and codes): // if (error?.status === 503) return true; // Service Unavailable // if (error?.code === 'ETIMEDOUT' || error?.code === 'ENETUNREACH') return true; // Network issues // if (error?.errors?.[0]?.code === SOME_MESSAGEBIRD_TRANSIENT_CODE) return true; return false; // Default: Do not retry } // Usage within OtpService (Example for sendOtp): // Assume OtpService class context for 'this.logger', 'this.messagebird' etc. /* async sendOtp(phoneNumber: string): Promise<string> { const normalizedPhoneNumber = phoneNumber.replace(/[\s\-()]/g, ''); this.logger.log(`Sending OTP to normalized number: ${normalizedPhoneNumber}`); const action = () => new Promise<string>((resolve, reject) => { const params: MessageBird.VerifyCreateParams = { originator: 'VerifyApp', template: 'Your verification code is %token.', type: 'sms', }; this.messagebird.verify.create(normalizedPhoneNumber, params, (err, response) => { if (err) return reject(err); // Reject promise on error to be caught by retry logic if (response && typeof response.id === 'string') { resolve(response.id); } else { reject(new InternalServerErrorException('Failed to get a valid verification ID from MessageBird.')); } }); }); try { // Wrap the promise-based SDK call within the retry function // Important: The retry function handles the actual SDK error mapping (BadRequest vs InternalServer) return await callWithRetry(action, 3, this.logger); } catch (error) { // Map final error after retries (if any occurred) this.logger.error('Final error after retries (or on first attempt if non-transient):', JSON.stringify(error)); if (error instanceof HttpException) { throw error; // Re-throw exceptions already mapped (like BadRequest) } // Map specific MessageBird errors after retries failed if (error.errors && error.errors.length > 0) { const mbError = error.errors[0]; if (mbError.code === 21) { throw new BadRequestException(`Invalid phone number: ${mbError.description}`); } throw new BadRequestException(`Failed to send OTP: ${mbError.description}`); } // Fallback generic error throw new InternalServerErrorException('Failed to initiate OTP verification after multiple attempts.'); } } // Apply similar retry logic structure to verifyOtp if needed. */
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:
# Install if needed: npm install prisma @prisma/client --save-dev npx prisma init --datasource-provider postgresql
Ensure your
.env
has the correctDATABASE_URL
.