Production-Ready NestJS & Infobip WhatsApp Integration Guide
This guide provides a comprehensive, step-by-step walkthrough for integrating Infobip's WhatsApp API into a NestJS application using Node.js. We'll cover everything from initial project setup and sending messages to handling incoming webhooks, error management, security, and deployment best practices. By the end, you'll have a robust foundation for production-level WhatsApp communication.
We assume minimal prior knowledge beyond basic NestJS and Node.js concepts, explaining each step and decision clearly. We prioritize practical, real-world implementation details to ensure your integration is reliable and scalable.
Project Overview and Goals
What We're Building:
We will build a NestJS application capable of:
- Sending WhatsApp Messages: Exposing an API endpoint to trigger sending text messages via the Infobip WhatsApp API.
- Receiving WhatsApp Messages: Setting up a webhook endpoint to receive incoming messages sent to our designated Infobip WhatsApp number.
- Basic Message Logging: Storing basic information about sent and received messages (optional, but good practice).
Problem Solved:
This integration enables applications to leverage WhatsApp for various communication needs, such as notifications, customer support interactions, OTP delivery, or marketing messages (respecting WhatsApp policies), automating processes previously handled manually or through less direct channels.
Technologies Used:
- Node.js: The JavaScript runtime environment.
- NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications using TypeScript. Its modular architecture and built-in features (dependency injection, validation pipes, configuration management) accelerate development.
- TypeScript: Superset of JavaScript adding static types, enhancing code quality and maintainability.
- Infobip: The Communication Platform as a Service (CPaaS) provider whose API and Node.js SDK we will use for WhatsApp communication.
@infobip-api/sdk
: The official Infobip Node.js SDK, simplifying interactions with the Infobip API.@nestjs/config
: For managing environment variables securely.class-validator
&class-transformer
: For robust request validation using Data Transfer Objects (DTOs).- (Optional) TypeORM / Prisma: For database interactions if message persistence is required.
- (Optional) Docker: For containerizing the application for deployment.
System Architecture:
graph LR
Client[API Client / Frontend] -- HTTPS Request --> NestJS_API[NestJS Application];
NestJS_API -- Send Message Request (SDK) --> Infobip[Infobip Platform];
Infobip -- Sends Message --> WhatsApp[WhatsApp User];
WhatsApp -- Sends Reply --> Infobip;
Infobip -- Incoming Message (Webhook POST) --> NestJS_API;
NestJS_API -- (Optional) Log Message --> DB[(Database)];
subgraph NestJS Application
direction LR
Controller[WhatsappController] --> Service[WhatsappService];
Service -- Uses --> InfobipSDK[@infobip-api/sdk];
Service -- Uses --> Config[ConfigService];
Service -- (Optional) Interacts --> Repo[MessageRepository];
end
subgraph Infobip
direction TB
APIs[Infobip APIs]
WebhookService[Webhook Service]
end
Prerequisites:
- Node.js: v16 or later installed.
- npm or yarn: Package manager.
- NestJS CLI: Installed globally (
npm install -g @nestjs/cli
). - Infobip Account: A registered Infobip account (a free trial is available).
- Access to your Infobip API Key and Base URL.
- A configured WhatsApp Sender number within your Infobip account.
- (Optional)
ngrok
or similar: For testing webhooks locally. - (Optional) Docker Desktop: For containerization.
- (Optional) Postman or
curl
: For testing API endpoints.
1. Setting up the Project
Let's initialize our NestJS project and install necessary dependencies.
-
Create a New NestJS Project: Open your terminal and run:
nest new infobip-whatsapp-app cd infobip-whatsapp-app
Choose your preferred package manager (npm or yarn) when prompted.
-
Install Dependencies: We need the Infobip SDK and NestJS configuration module.
# Using npm npm install @infobip-api/sdk @nestjs/config dotenv class-validator class-transformer # Using yarn yarn add @infobip-api/sdk @nestjs/config dotenv class-validator class-transformer
@infobip-api/sdk
: The core library for interacting with Infobip.@nestjs/config
: Manages environment variables.dotenv
: Loads environment variables from a.env
file for local development.class-validator
&class-transformer
: Enable validation using DTOs.
-
Configure Environment Variables:
- Create a
.env
file in the project root:touch .env
- Add your Infobip credentials and sender number to
.env
. Never commit this file to version control.#.env # Infobip Credentials INFOBIP_BASE_URL=your_infobip_base_url.api.infobip.com INFOBIP_API_KEY=YourInfobipApiKeyGoesHere INFOBIP_WHATSAPP_SENDER=YourInfobipWhatsAppSenderNumber # e.g., 447860099299 # Application Port (Optional) PORT=3000 # Webhook Secret (Add if implementing signature verification) # INFOBIP_WEBHOOK_SECRET=YourInfobipWebhookSecret
- How to find these values:
INFOBIP_BASE_URL
&INFOBIP_API_KEY
: Log in to your Infobip Portal. Navigate to the homepage or API Keys management section. Your unique Base URL and API Key will be displayed.INFOBIP_WHATSAPP_SENDER
: Navigate to Channels and Numbers -> WhatsApp -> Numbers. Copy the registered sender number you intend to use. Ensure it's approved and active.
- Create a
-
Load Environment Variables using
ConfigModule
: Import and configureConfigModule
in your main application module (src/app.module.ts
). Making it global simplifies access in other modules.// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { WhatsappModule } from './whatsapp/whatsapp.module'; // We will create this next @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Makes ConfigService available globally envFilePath: '.env', // Specifies the env file path }), WhatsappModule, // Import the feature module ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
- Why
ConfigModule
? It provides a structured and secure way to handle environment-specific configurations, preventing hardcoding sensitive data like API keys directly in the code.isGlobal: true
avoids needing to importConfigModule
into every feature module that requires access to environment variables.
- Why
-
Enable Validation Pipe Globally: Configure the
ValidationPipe
insrc/main.ts
to automatically validate incoming request payloads against DTOs.// src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; // Import ValidationPipe import { ConfigService } from '@nestjs/config'; // Import ConfigService async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); // Get ConfigService instance // Enable global validation pipe app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip properties that do not have any decorators forbidNonWhitelisted: true, // Throw error if non-whitelisted values are provided transform: true, // Automatically transform payloads to DTO instances })); const port = configService.get<number>('PORT') || 3000; // Get port from env await app.listen(port); console.log(`Application is running on: ${await app.getUrl()}`); } bootstrap();
-
Create the WhatsApp Feature Module: Generate a module, controller, and service for our WhatsApp functionality using the NestJS CLI.
nest generate module whatsapp nest generate controller whatsapp --no-spec # Optional: skip test file nest generate service whatsapp --no-spec # Optional: skip test file
This creates a
src/whatsapp
directory withwhatsapp.module.ts
,whatsapp.controller.ts
, andwhatsapp.service.ts
. The CLI automatically updatessrc/app.module.ts
to importWhatsappModule
.
2. Implementing Core Functionality (Sending Messages)
We'll implement the logic to send a WhatsApp message via the Infobip SDK within our WhatsappService
.
-
Initialize Infobip Client in the Service: Inject
ConfigService
intoWhatsappService
and initialize the Infobip client in the constructor.// src/whatsapp/whatsapp.service.ts import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Infobip, AuthType } from '@infobip-api/sdk'; // Import Infobip SDK elements @Injectable() export class WhatsappService { private readonly logger = new Logger(WhatsappService.name); private infobipClient: Infobip; private whatsappSender: string; constructor(private configService: ConfigService) { const apiKey = this.configService.get<string>('INFOBIP_API_KEY'); const baseUrl = this.configService.get<string>('INFOBIP_BASE_URL'); this.whatsappSender = this.configService.get<string>('INFOBIP_WHATSAPP_SENDER'); if (!apiKey || !baseUrl || !this.whatsappSender) { this.logger.error('Infobip API Key, Base URL, or Sender Number not configured in environment variables.'); throw new InternalServerErrorException('WhatsApp Service is not configured correctly.'); } this.infobipClient = new Infobip({ baseUrl: baseUrl, apiKey: apiKey, authType: AuthType.ApiKey, // Specify API Key authentication }); this.logger.log('Infobip client initialized successfully.'); } // Method to send a text message will go here }
- Why initialize here? The constructor is the ideal place to set up dependencies like the Infobip client. NestJS manages the service as a singleton by default, so the client is created only once. We also perform essential configuration checks.
-
Implement the
sendTextMessage
Method: Add a method to handle sending the message payload.// src/whatsapp/whatsapp.service.ts import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Infobip, AuthType } from '@infobip-api/sdk'; @Injectable() export class WhatsappService { private readonly logger = new Logger(WhatsappService.name); private infobipClient: Infobip; private whatsappSender: string; constructor(private configService: ConfigService) { // ... (constructor logic as above) ... } async sendTextMessage(to: string, text: string): Promise<any> { this.logger.log(`Attempting to send message to ${to}`); // Basic validation (more robust validation happens at controller level via DTOs) if (!to || !text) { throw new BadRequestException('Recipient number (to) and message text cannot be empty.'); } // Basic check for phone number format. Infobip usually expects international format without '+'. // WARNING: This regex is very basic. For production, use a robust library like 'libphonenumber-js' // or `class-validator`'s `@IsPhoneNumber(region)` in the DTO for proper validation. if (!/^\d{10,15}$/.test(to)) { this.logger.warn(`Potential invalid phone number format for recipient: ${to}. Expected format like 447123456789 (international without '+').`); // Depending on requirements, you might throw BadRequestException here: // throw new BadRequestException('Invalid recipient phone number format. Use international format without ""+"".'); } const payload = { type: 'text', // Specify message type as text from: this.whatsappSender, to: to, // Recipient number (validated format is crucial) content: { text: text, // The message content }, }; try { const response = await this.infobipClient.channels.whatsapp.send(payload); this.logger.log(`Message sent successfully to ${to}. Message ID: ${response.data.messages[0]?.messageId}`); // Consider logging the full response for detailed tracking if needed // this.logger.debug(`Infobip API Response: ${JSON.stringify(response.data)}`); // Optional: Persist message status to database here // await this.messageRepository.save({ ... }); return response.data; // Return the relevant part of the Infobip response } catch (error) { this.logger.error(`Failed to send message to ${to}: ${error.message}`, error.stack); // Enhance error handling based on Infobip error structure if available // Example: Check error.response?.data for specific API error codes/messages if (error.response?.data) { this.logger.error(`Infobip API Error Details: ${JSON.stringify(error.response.data)}`); } // Re-throw a more specific HTTP exception if possible, or a generic one throw new InternalServerErrorException(`Failed to send WhatsApp message: ${error.message}`); } } // Method to handle incoming messages will go here }
- Why this structure? We encapsulate the Infobip interaction within the service, keeping the controller thin. The
try...catch
block handles potential API errors gracefully, logs them, and throws appropriate NestJS HTTP exceptions. Basic input validation is added as a safety measure, though the primary validation occurs via DTOs in the controller. We return theresponse.data
which contains useful information likemessageId
.
- Why this structure? We encapsulate the Infobip interaction within the service, keeping the controller thin. The
3. Building the API Layer (Sending Messages)
Now, let's expose an endpoint in WhatsappController
to trigger the sendTextMessage
method.
-
Create a Data Transfer Object (DTO): Define a DTO to specify the expected shape and validation rules for the request body.
// src/whatsapp/dto/send-message.dto.ts import { IsNotEmpty, IsString, Length, IsPhoneNumber /* Recommended */ } from 'class-validator'; export class SendMessageDto { @IsNotEmpty() @IsString() // Basic length validation. For production, strongly consider using: // @IsPhoneNumber(null) // or specify region e.g. @IsPhoneNumber('GB') // Ensure the format matches Infobip expectations (usually international without '+', e.g., 447123456789) @Length(10, 15) // Adjust length constraints as needed based on expected format to: string; @IsNotEmpty() @IsString() @Length(1, 1600) // WhatsApp message length limit (check Infobip docs for specifics) text: string; }
- Why DTOs? They define a clear contract for your API and, combined with
ValidationPipe
, provide automatic request validation, improving security and data integrity. Using@IsPhoneNumber
is preferred for robust validation.
- Why DTOs? They define a clear contract for your API and, combined with
-
Implement the Controller Endpoint: Inject
WhatsappService
and create a POST endpoint.// src/whatsapp/whatsapp.controller.ts import { Controller, Post, Body, Logger, HttpCode, HttpStatus } from '@nestjs/common'; import { WhatsappService } from './whatsapp.service'; import { SendMessageDto } from './dto/send-message.dto'; // Import the DTO @Controller('whatsapp') // Route prefix for this controller export class WhatsappController { private readonly logger = new Logger(WhatsappController.name); constructor(private readonly whatsappService: WhatsappService) {} @Post('send') // Route: POST /whatsapp/send @HttpCode(HttpStatus.ACCEPTED) // Indicate request accepted, processing initiated async sendMessage(@Body() sendMessageDto: SendMessageDto) { this.logger.log(`Received request to send message: ${JSON.stringify(sendMessageDto)}`); try { // The 'to' number format should align with expectations set in the service/DTO comments const result = await this.whatsappService.sendTextMessage( sendMessageDto.to, sendMessageDto.text, ); // Return a minimal success response, including the message ID might be useful return { message: 'Message accepted for delivery.', infobipResponse: { bulkId: result.bulkId, messageId: result.messages[0]?.messageId, status: result.messages[0]?.status // Example status from response } }; } catch (error) { this.logger.error(`Error in sendMessage endpoint: ${error.message}`, error.stack); // Exceptions thrown from the service (like BadRequestException, InternalServerErrorException) // will be automatically handled by NestJS's exception filter. throw error; // Re-throw the error for the default exception handler } } // Webhook endpoint will go here }
- Why
@Body() sendMessageDto: SendMessageDto
? This tells NestJS to parse the request body and validate it against theSendMessageDto
using the globally configuredValidationPipe
. - Why
HttpStatus.ACCEPTED
? Sending a message is often asynchronous. Returning 202 Accepted signifies the request is valid and processing has started, but doesn't guarantee immediate delivery. - Why re-throw error? NestJS has built-in exception handling. By re-throwing errors caught from the service, we let NestJS map them to appropriate HTTP error responses (e.g., 400 for
BadRequestException
, 500 forInternalServerErrorException
).
- Why
-
Testing the Endpoint: Start your application:
npm run start:dev
Usecurl
or Postman to send a POST request:curl -X POST http://localhost:3000/whatsapp/send \ -H 'Content-Type: application/json' \ -d '{ ""to"": ""YOUR_REGISTERED_TEST_NUMBER"", ""text"": ""Hello from NestJS and Infobip!"" }'
Replace
YOUR_REGISTERED_TEST_NUMBER
with your registered test number (e.g.,447123456789
). You should receive a202 Accepted
response and the message on your WhatsApp (if using your registered trial number). Check the application logs and the Infobip portal for status updates.
4. Integrating with Third-Party Services (Infobip Webhook)
To receive messages sent to your Infobip WhatsApp number, you need to configure a webhook.
-
Configure Webhook in Infobip Portal:
- Log in to your Infobip Portal.
- Navigate to Channels and Numbers -> WhatsApp -> Numbers.
- Find your sender number and click Configure or similar option.
- Locate the Webhook URL or Incoming Messages URL section.
- Enter the publicly accessible URL of your NestJS application's webhook endpoint. For local development, use
ngrok
to expose your local port:- Run
ngrok http 3000
(if your app runs on port 3000). - Copy the
https://<your-ngrok-id>.ngrok.io
URL. - Your webhook URL will be
https://<your-ngrok-id>.ngrok.io/whatsapp/webhook
(we'll create/whatsapp/webhook
next).
- Run
- Important Security: Note down any secret or authentication token Infobip provides for webhook verification. This is crucial for ensuring requests genuinely come from Infobip (see Section 7). Add this secret to your
.env
file (e.g.,INFOBIP_WEBHOOK_SECRET
). - Save the configuration in the Infobip portal.
-
Create DTO for Incoming Webhook Payload: Define a DTO based on the expected structure of Infobip's incoming WhatsApp message webhook. Note: This structure might vary slightly; always refer to the official Infobip documentation for the most accurate payload format.
// src/whatsapp/dto/incoming-message.dto.ts import { Type } from 'class-transformer'; import { IsArray, IsNotEmpty, IsObject, IsOptional, IsString, ValidateNested, IsDateString } from 'class-validator'; // Define nested structures based on Infobip's documentation class MessageContent { @IsString() text: string; // Add other potential content types like 'imageUrl', 'documentUrl', etc. if needed } class MessagePrice { // Define price properties if needed (check Infobip docs) } class MessageStatus { // Define status properties if needed (check Infobip docs) } class MessageContext { // Define context properties if needed (e.g., for replies) @IsString() @IsOptional() from?: string; @IsString() @IsOptional() id?: string; } class MessageResult { @IsString() from: string; @IsString() to: string; @IsString() @IsOptional() integrationType?: string; @IsString() messageId: string; @IsObject() @ValidateNested() @Type(() => MessageContent) content: MessageContent; @IsString() // Consider using @IsDateString() if format is consistently ISO8601 receivedAt: string; @IsOptional() @IsObject() @ValidateNested() @Type(() => MessagePrice) price?: MessagePrice; @IsOptional() @IsObject() @ValidateNested() @Type(() => MessageStatus) status?: MessageStatus; @IsOptional() @IsObject() @ValidateNested() @Type(() => MessageContext) context?: MessageContext; // Add other potential properties like 'callbackData' } export class IncomingWhatsappDto { @IsArray() @ValidateNested({ each: true }) @Type(() => MessageResult) results: MessageResult[]; // These counts are often strings in webhook payloads, verify with Infobip docs @IsOptional() @IsString() messageCount?: string; @IsOptional() @IsString() pendingMessageCount?: string; }
- Why this DTO? It validates the structure of incoming data from Infobip, ensuring your handler receives correctly formatted information.
@ValidateNested
and@Type
are crucial for validating nested objects and arrays. Using@IsDateString()
forreceivedAt
is safer if you know the format is ISO8601.
- Why this DTO? It validates the structure of incoming data from Infobip, ensuring your handler receives correctly formatted information.
-
Implement the Webhook Handler in Controller: Add a new POST endpoint in
WhatsappController
to receive webhook events.// src/whatsapp/whatsapp.controller.ts import { Controller, Post, Body, Logger, HttpCode, HttpStatus, Req, ForbiddenException } from '@nestjs/common'; // Added Req, ForbiddenException import { WhatsappService } from './whatsapp.service'; import { SendMessageDto } from './dto/send-message.dto'; import { IncomingWhatsappDto } from './dto/incoming-message.dto'; // Import the DTO import { Request } from 'express'; // Import Request type if using Express @Controller('whatsapp') export class WhatsappController { private readonly logger = new Logger(WhatsappController.name); constructor(private readonly whatsappService: WhatsappService) {} // ... (sendMessage endpoint) ... @Post('webhook') // Route: POST /whatsapp/webhook @HttpCode(HttpStatus.OK) // Acknowledge receipt immediately handleIncomingMessage(@Body() incomingDto: IncomingWhatsappDto, @Req() req: Request) { // Inject Request object this.logger.log(`Webhook received: ${JSON.stringify(incomingDto)}`); // TODO: IMPORTANT SECURITY STEP - Implement signature verification here // Use the secret/token from Infobip portal to verify the request authenticity. // This requires access to the raw request body. Configure NestJS for raw body parsing if needed. // Example conceptual check: // const signature = req.headers['x-infobip-signature'] as string; // Get signature header (actual name may vary) // const rawBody = (req as any).rawBody; // Access raw body (requires middleware setup) // if (!rawBody || !this.whatsappService.verifyWebhookSignature(rawBody, signature)) { // this.logger.warn('Invalid webhook signature attempt.'); // throw new ForbiddenException('Invalid webhook signature'); // } // this.logger.log('Webhook signature verified successfully.'); // Hand off processing to the service to keep the controller thin this.whatsappService.processIncomingMessages(incomingDto.results); // Return 200 OK quickly to Infobip to prevent retries return { message: 'Webhook received successfully.' }; } }
- Why
HttpStatus.OK
? Webhook providers like Infobip expect a quick 2xx response to acknowledge receipt. Long processing should happen asynchronously. - Why hand off to service? Keeps the controller focused on request/response handling and validation. Business logic belongs in the service.
- Webhook Security: Implementing signature verification (as noted in the TODO comment) is essential to prevent malicious actors from sending fake data to your endpoint. This typically requires accessing the raw request body before it's parsed by NestJS, which might involve adding middleware (e.g.,
bodyParser.raw({ type: 'application/json' })
) inmain.ts
.
- Why
-
Implement Processing Logic in Service: Add a method in
WhatsappService
to handle the incoming message data. You'll also need to add the verification logic stubbed in the controller if you implement it.// src/whatsapp/whatsapp.service.ts import { Injectable, Logger, InternalServerErrorException, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Infobip, AuthType } from '@infobip-api/sdk'; import { IncomingWhatsappDto } from './dto/incoming-message.dto'; // Import the DTO type import * as crypto from 'crypto'; // Import crypto for verification // Define the type for a single message result based on DTO for clarity type IncomingMessageResult = IncomingWhatsappDto['results'][0]; @Injectable() export class WhatsappService { private readonly logger = new Logger(WhatsappService.name); private infobipClient: Infobip; private whatsappSender: string; constructor(private configService: ConfigService) { // ... (constructor logic) ... } // ... (sendTextMessage method) ... // Conceptual method for webhook signature verification (implement based on Infobip docs) verifyWebhookSignature(rawBody: Buffer, signatureHeader: string): boolean { const secret = this.configService.get<string>('INFOBIP_WEBHOOK_SECRET'); if (!secret || !signatureHeader || !rawBody) { this.logger.warn('Webhook signature verification failed: Missing secret, signature, or body.'); return false; } // Implement actual HMAC SHA comparison based on Infobip's method // Example (pseudo-code, consult Infobip docs for exact algorithm and header format): // Assuming signatureHeader is just the hex digest try { const computedSignature = crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); const receivedSignature = Buffer.from(signatureHeader); const expectedSignature = Buffer.from(computedSignature); if (receivedSignature.length !== expectedSignature.length) { this.logger.warn('Webhook signature length mismatch.'); return false; } // Use timingSafeEqual to prevent timing attacks return crypto.timingSafeEqual(receivedSignature, expectedSignature); } catch (error) { this.logger.error(`Error during webhook signature verification: ${error.message}`, error.stack); return false; } // this.logger.warn('Webhook signature verification logic needs confirmation with Infobip documentation.'); // return true; // Placeholder - REMOVE THIS IN PRODUCTION AFTER VERIFYING LOGIC } async processIncomingMessages(messages: IncomingMessageResult[]): Promise<void> { for (const message of messages) { try { this.logger.log(`Processing incoming message from ${message.from}: ""${message.content?.text}"" (ID: ${message.messageId})`); // Example: Log message details // Optional: Persist incoming message to database // await this.messageRepository.save({ from: message.from, body: message.content?.text, ... }); // Example: Implement basic auto-reply logic if (message.content?.text?.toLowerCase().includes('hello')) { await this.sendTextMessage(message.from, 'Hi there! How can I help?'); } // Add more complex routing or processing logic here based on message content, context, etc. } catch (error) { this.logger.error(`Error processing incoming message ${message.messageId}: ${error.message}`, error.stack); // Implement error tracking or alerting here } } } }
- Why process asynchronously (conceptually)? For high volume, you'd typically push incoming messages onto a queue (like BullMQ) and process them with background workers to avoid blocking the webhook response and handle load spikes. For this guide, we process directly but acknowledge the pattern.
- Webhook Secret: Ensure you add
INFOBIP_WEBHOOK_SECRET
to your.env
file and retrieve it viaConfigService
when implementing signature verification.
-
Testing the Webhook:
- Ensure your application and
ngrok
tunnel are running. - Send a WhatsApp message from your personal phone to your registered Infobip WhatsApp sender number.
- Check your application logs. You should see the ""Webhook received"" log in the controller and the ""Processing incoming message"" log in the service. If you implemented the auto-reply, you should receive it back on your phone.
- Ensure your application and
5. Implementing Proper Error Handling, Logging, and Retry Mechanisms
Robust error handling and logging are crucial for production.
-
Error Handling Strategy:
- Validation Errors: Handled by
ValidationPipe
-> 400 Bad Request. - Service Logic Errors: Use specific
BadRequestException
,NotFoundException
, etc., where applicable within the service. - Infobip API Errors: Catch errors during SDK calls (
try...catch
). Log detailed error information (includingerror.response?.data
if available). ThrowInternalServerErrorException
or a more specific custom exception if you can map Infobip errors. - Webhook Signature Errors: Throw
ForbiddenException
(403) if signature verification fails. - Unhandled Errors: NestJS's default exception filter catches unhandled errors and returns a generic 500 Internal Server Error. Customize this filter for production logging/reporting.
- Validation Errors: Handled by
-
Logging:
- Use NestJS's built-in
Logger
. Inject it where needed (private readonly logger = new Logger(ClassName.name)
). - Log Levels:
log
: General information (request received, action started, webhook received).warn
: Potential issues (invalid format detected but handled, retry attempt, signature failure).error
: Failures (API call failed, processing error, configuration missing). Include stack traces.debug
: Detailed information for troubleshooting (full API responses/payloads) – disable in production unless needed.
- Production Logging: Consider using a more advanced library like
pino
orwinston
for structured logging (JSON format), log rotation, and transport to external logging services (e.g., Datadog, ELK Stack). Configure log levels via environment variables.
- Use NestJS's built-in
-
Retry Mechanisms (Conceptual):
- Outgoing Messages: If an Infobip API call fails due to transient network issues or temporary Infobip unavailability (e.g., 429, 5xx errors), implement a retry strategy.
- Simple: A
for
loop with delays (shown below - use with caution). - Better: Use libraries like
async-retry
or integrate with a queue system (e.g., BullMQ) that has built-in retry logic with exponential backoff. Queues are strongly recommended for production reliability.
- Simple: A
- Webhook Processing: If processing an incoming message fails temporarily (e.g., database connection issue), a queue system with retries is the best approach. Infobip may retry sending the webhook if it doesn't receive a 2xx response quickly, but relying on this isn't ideal for application-level processing failures.
- Outgoing Messages: If an Infobip API call fails due to transient network issues or temporary Infobip unavailability (e.g., 429, 5xx errors), implement a retry strategy.
// Example: Conceptual retry in service (simple version - prefer queues for production)
async sendTextMessageWithRetry(to: string, text: string, retries = 3): Promise<any> {
for (let i = 0; i < retries; i++) {
try {
// Note: This calls the original sendTextMessage which includes its own try/catch.
// You might refactor to have a core sending logic method called by both.
return await this.sendTextMessage(to_ text);
} catch (error) {
// WARNING: This is a basic check. Refine based on specific Infobip error codes or types.
// Check for specific status codes (e.g._ 429_ 5xx) or network error types.
const isRetryable = error instanceof InternalServerErrorException || (error.response && [429_ 500_ 502_ 503_ 504].includes(error.response.status)); // Example refinement
if (isRetryable && i < retries - 1) {
const delay = Math.pow(2_ i) * 1000; // Exponential backoff (1s_ 2s_ 4s...)
this.logger.warn(`Retrying message send to ${to} (attempt ${i + 1}/${retries}) after ${delay}ms delay. Error: ${error.message}`);
await new Promise(resolve => setTimeout(resolve, delay)); // Wait before retrying
} else {
this.logger.error(`Final attempt failed or error not retryable for message to ${to}. Error: ${error.message}`);
throw error; // Re-throw the error after final attempt or if not retryable
}
}
}
}