This guide provides a comprehensive walkthrough for building a production-ready WhatsApp integration using Node.js, the NestJS framework, and the Twilio API. We will cover everything from initial project setup to deployment and monitoring, enabling your application to send and receive WhatsApp messages, including media.
By following this tutorial, you will create a NestJS application capable of handling incoming WhatsApp messages via webhooks, replying dynamically, and initiating outbound messages programmatically. This solves the common need for businesses to engage with customers on a widely used platform like WhatsApp for notifications, support, and conversational interactions.
Technologies Used:
- Node.js: The underlying JavaScript runtime environment.
- NestJS: A progressive Node.js framework for building efficient, reliable, and scalable server-side applications. Its modular architecture and use of TypeScript make it ideal for production systems.
- Twilio API for WhatsApp: The third-party service providing the infrastructure to connect with the WhatsApp Business Platform.
- TypeScript: Adds static typing to JavaScript, improving code quality and maintainability.
- (Optional) Prisma/TypeORM: For database interactions to store message history or state.
- (Optional) Docker: For containerizing the application for deployment.
Prerequisites:
- Node.js (v16 or later recommended) and npm/yarn installed.
- A Twilio account with an activated WhatsApp Sandbox or a registered WhatsApp Business Sender.
- Basic understanding of TypeScript, NestJS concepts (modules, controllers, services), and REST APIs.
- A tool to expose your local development server to the internet (e.g.,
ngrok
). - Access to a WhatsApp-enabled mobile device for testing.
System Architecture:
The basic flow involves:
- Outbound: Your NestJS application calls the Twilio API to send a message. Twilio delivers it to the user's WhatsApp.
- Inbound: A user sends a message to your Twilio WhatsApp number. Twilio sends an HTTP POST request (webhook) to a predefined endpoint in your NestJS application. Your application processes the request and can optionally send a TwiML response back to Twilio to dictate a reply.
This guide will walk you through building the NestJS Application
component and configuring its interaction with Twilio
.
1. Setting up the NestJS project
Let's initialize a new NestJS project and install the necessary dependencies.
-
Create a new NestJS project: Open your terminal and run the Nest CLI command:
# Install Nest CLI if you haven't already npm i -g @nestjs/cli nest new nestjs-whatsapp-twilio cd nestjs-whatsapp-twilio
Choose your preferred package manager (npm or yarn) when prompted. This creates a standard NestJS project structure.
-
Install Dependencies: We need the official Twilio helper library, a configuration module to handle environment variables securely, and a rate limiter for security.
# Using npm npm install twilio @nestjs/config @nestjs/throttler # Or using yarn yarn add twilio @nestjs/config @nestjs/throttler
twilio
: The official Node.js SDK for interacting with the Twilio API.@nestjs/config
: For managing environment variables.@nestjs/throttler
: To add rate limiting to our webhook endpoint, protecting against abuse.
-
Configure Environment Variables: Create a
.env
file in the project root directory. Never commit this file to version control.# .env # Twilio Credentials - Find these in your Twilio Console # https://www.twilio.com/console TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token_xxxxxxxxxxxxxx # Your Twilio WhatsApp Number (Sandbox or Registered Sender) # Must include the 'whatsapp:' prefix and be in E.164 format # Example Sandbox Number: whatsapp:+14155238886 TWILIO_WHATSAPP_NUMBER=whatsapp:+1xxxxxxxxxx # (Optional) Auth token for securing your webhook endpoint WEBHOOK_AUTH_TOKEN=a_very_secure_random_string # (Optional) Application Port PORT=3000
TWILIO_ACCOUNT_SID
/TWILIO_AUTH_TOKEN
: Found on your main Twilio Console dashboard. These authenticate your API requests.TWILIO_WHATSAPP_NUMBER
: The specific Twilio number enabled for WhatsApp (either your Sandbox number or a purchased/registered number). Find this in the Twilio Console under Messaging > Senders > WhatsApp Senders or the WhatsApp Sandbox settings. It must start withwhatsapp:
followed by the number in E.164 format (e.g.,whatsapp:+14155238886
).WEBHOOK_AUTH_TOKEN
: A secret token you define, used later to verify that incoming webhook requests genuinely come from Twilio.PORT
: The port your NestJS application will listen on.
-
Load Environment Variables: Modify
src/app.module.ts
to import and configureConfigModule
.// 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 other modules here later (e.g., TwilioModule, WebhookModule) @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, // Make ConfigService available globally envFilePath: '.env', // Specify the env file path }), // Add other modules here ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
This setup makes environment variables accessible throughout the application via NestJS's
ConfigService
.
2. Implementing core functionality
We'll create a dedicated module and service for handling Twilio interactions and another module/controller for the webhook.
2.1 Twilio Service
This service will encapsulate the logic for initializing the Twilio client and sending messages.
-
Generate the Twilio module and service:
nest generate module twilio nest generate service twilio
-
Implement the
TwilioService
:// src/twilio/twilio.service.ts import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import twilio, { Twilio } from 'twilio'; @Injectable() export class TwilioService implements OnModuleInit { private readonly logger = new Logger(TwilioService.name); private client: Twilio; private twilioWhatsAppNumber: string; constructor(private readonly configService: ConfigService) {} onModuleInit() { const accountSid = this.configService.get<string>('TWILIO_ACCOUNT_SID'); const authToken = this.configService.get<string>('TWILIO_AUTH_TOKEN'); this.twilioWhatsAppNumber = this.configService.get<string>( 'TWILIO_WHATSAPP_NUMBER', ); if (!accountSid || !authToken || !this.twilioWhatsAppNumber) { this.logger.error( 'Twilio credentials missing in environment variables.', ); throw new Error( 'Twilio credentials missing. Ensure TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_WHATSAPP_NUMBER are set.', ); } this.client = twilio(accountSid, authToken); this.logger.log('Twilio client initialized.'); } async sendWhatsAppMessage(to: string, body: string): Promise<string | null> { // Ensure 'to' number is in E.164 format with whatsapp: prefix const formattedTo = to.startsWith('whatsapp:') ? to : `whatsapp:${to}`; try { const message = await this.client.messages.create({ from: this.twilioWhatsAppNumber, to: formattedTo, body: body, }); this.logger.log(`WhatsApp message sent to ${formattedTo}: SID ${message.sid}`); return message.sid; } catch (error) { this.logger.error(`Failed to send WhatsApp message to ${formattedTo}: ${error.message}`); // Consider more specific error handling or re-throwing return null; } } async sendWhatsAppMediaMessage(to: string, mediaUrl: string, body?: string): Promise<string | null> { // Ensure 'to' number is in E.164 format with whatsapp: prefix const formattedTo = to.startsWith('whatsapp:') ? to : `whatsapp:${to}`; // WhatsApp doesn't support body with most media types, except potentially images. // It's safer to omit body generally when sending media unless specifically needed and tested. // See Twilio/WhatsApp docs for specifics. const messageOptions: any = { from: this.twilioWhatsAppNumber, to: formattedTo, mediaUrl: [mediaUrl], // Must be an array }; if (body) { this.logger.warn('Sending body with media might be ignored by WhatsApp depending on media type.'); messageOptions.body = body; } try { const message = await this.client.messages.create(messageOptions); this.logger.log(`WhatsApp media message sent to ${formattedTo}: SID ${message.sid}`); return message.sid; } catch (error) { this.logger.error(`Failed to send WhatsApp media message to ${formattedTo}: ${error.message}`); return null; } } // Helper to get the initialized client if needed elsewhere getClient(): Twilio { return this.client; } // Helper to get the configured WhatsApp number getWhatsAppNumber(): string { return this.twilioWhatsAppNumber; } }
- We use
OnModuleInit
to initialize the client when the module loads. ConfigService
is injected to securely retrieve credentials from environment variables.- Error handling is included for missing credentials and failed API calls.
- Methods ensure the
to
number includes thewhatsapp:
prefix. - Media URLs must be provided as an array. A warning is logged about potential issues sending a body with media.
- Added
getWhatsAppNumber()
helper.
- We use
-
Update
TwilioModule
: Make sureTwilioService
is provided and exported, and importConfigModule
.// src/twilio/twilio.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { TwilioService } from './twilio.service'; @Module({ imports: [ConfigModule], // Import ConfigModule here too providers: [TwilioService], exports: [TwilioService], // Export service for use in other modules }) export class TwilioModule {}
-
Import
TwilioModule
intoAppModule
:// 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 { TwilioModule } from './twilio/twilio.module'; // Import TwilioModule // Import other modules here later @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', }), TwilioModule, // Add TwilioModule // Add other modules here ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
2.2 Webhook Controller
This controller will handle incoming messages from Twilio.
-
Generate the Webhook module and controller:
nest generate module webhook nest generate controller webhook
-
Implement the
WebhookController
:// src/webhook/webhook.controller.ts import { Controller, Post, Body, Res, Logger, Req, Headers, ForbiddenException, HttpCode, HttpStatus } from '@nestjs/common'; import { Response, Request } from 'express'; import { twiml } from 'twilio'; // Correct import import { ConfigService } from '@nestjs/config'; import * as crypto from 'crypto'; @Controller('webhooks') export class WebhookController { private readonly logger = new Logger(WebhookController.name); private readonly webhookAuthToken: string; constructor(private readonly configService: ConfigService) { this.webhookAuthToken = this.configService.get<string>('WEBHOOK_AUTH_TOKEN'); if (!this.webhookAuthToken) { this.logger.warn('SECURITY WARNING: WEBHOOK_AUTH_TOKEN is not set. Twilio signature validation will be skipped. This is unsafe for production!'); } } @Post('twilio/whatsapp') @HttpCode(HttpStatus.OK) // Default success code, TwiML overrides if sent async handleIncomingWhatsApp( @Body() body: any, // Twilio sends form-urlencoded data @Res() res: Response, @Req() req: Request, @Headers('X-Twilio-Signature') twilioSignature: string, ) { // --- Security: Validate Twilio Signature --- if (this.webhookAuthToken && !this.validateTwilioRequest(req, twilioSignature)) { this.logger.warn('Invalid Twilio signature received.'); // Respond with Forbidden, don't proceed res.status(HttpStatus.FORBIDDEN).send('Invalid Twilio Signature'); return; // Alternatively, throw new ForbiddenException('Invalid Twilio Signature'); // but sending raw response gives more control here. } else if (!this.webhookAuthToken && !twilioSignature) { // If token isn't set even in dev, log that validation is truly skipped this.logger.log('No WEBHOOK_AUTH_TOKEN set and no X-Twilio-Signature header received. Proceeding without validation (unsafe for production).'); } else if (!this.webhookAuthToken && twilioSignature) { // If token isn't set but signature IS present, warn that validation *could* have run this.logger.warn('Received X-Twilio-Signature, but skipping validation because WEBHOOK_AUTH_TOKEN is not set (unsafe for production).'); } const sender = body.From; // e.g., whatsapp:+15551234567 const messageBody = body.Body; const numMedia = parseInt(body.NumMedia || '0', 10); const messageSid = body.MessageSid; this.logger.log(`Received WhatsApp message SID: ${messageSid} from ${sender}`); if (messageBody) { this.logger.log(`Message Body: ${messageBody}`); } // --- Handle Media --- if (numMedia > 0) { const mediaUrl = body.MediaUrl0; // WhatsApp typically sends one media item const mediaContentType = body.MediaContentType0; this.logger.log(`Received media (${mediaContentType}) from ${sender} at ${mediaUrl}`); // Add your media processing logic here if needed // Example: await this.mediaService.process(mediaUrl, mediaContentType, sender); } // --- Generate TwiML Reply (Example: Echo Bot) --- const twimlResponse = new twiml.MessagingResponse(); if (numMedia > 0) { twimlResponse.message(`Thanks for the media! We received ${numMedia} item(s).`); // Optionally add media to the reply: // twimlResponse.message().media('URL_TO_YOUR_REPLY_IMAGE.jpg'); } else if (messageBody?.toLowerCase().trim() === 'hello') { // Added safe navigation for body twimlResponse.message(`Hi there! You said: ${messageBody}`); } else if (messageBody) { twimlResponse.message(`Echo: ${messageBody}`); } else { // Handle case where there's no body and no media (e.g., location message - not handled here) twimlResponse.message('Received your message.'); } // --- Send Response --- res.setHeader('Content-Type', 'text/xml'); res.send(twimlResponse.toString()); // If no reply is needed, send an empty 200 OK or 204 No Content // res.status(HttpStatus.NO_CONTENT).send(); } // --- Twilio Signature Validation Helper --- private validateTwilioRequest(req: Request, twilioSignature: string): boolean { // This function assumes webhookAuthToken is set; the check is done in the handler. if (!this.webhookAuthToken) { // Should not happen if called correctly from handler logic this.logger.error('validateTwilioRequest called unexpectedly without webhookAuthToken being set.'); return false; } if (!twilioSignature) { this.logger.warn('Validation failed: X-Twilio-Signature header missing.'); return false; // No signature provided cannot be valid } // Construct the full URL Twilio used for the request // Use X-Forwarded-Proto if behind a proxy that sets it, otherwise use req.protocol const protocol = req.headers['x-forwarded-proto'] || req.protocol; const fullUrl = `${protocol}://${req.get('host')}${req.originalUrl}`; // Twilio calculates the signature based on the URL and the POST parameters // sorted alphabetically and concatenated without separators. // Ensure req.body is used as parsed by NestJS (usually object) const params = req.body || {}; const sortedKeys = Object.keys(params).sort(); const paramString = sortedKeys.reduce((acc, key) => acc + key + params[key], ''); const expectedSignature = crypto .createHmac('sha1', this.webhookAuthToken) .update(Buffer.from(fullUrl + paramString, 'utf-8')) .digest('base64'); // Compare signatures using timing-safe comparison let isValid = false; try { isValid = crypto.timingSafeEqual( Buffer.from(twilioSignature), Buffer.from(expectedSignature) ); } catch (e) { // Handle potential errors if buffers have different lengths this.logger.warn(`Error during timingSafeEqual comparison: ${e.message}`); isValid = false; } if (!isValid) { this.logger.warn(`Validation failed: Signature mismatch. Expected: ${expectedSignature}, Received: ${twilioSignature}`); } return isValid; } }
- The controller listens for POST requests at
/webhooks/twilio/whatsapp
. - Crucially, it includes
validateTwilioRequest
to verify theX-Twilio-Signature
header. Warning: Skipping validation by not settingWEBHOOK_AUTH_TOKEN
is unsafe for production. This token must be set with a strong, unique secret. The code logs explicit warnings if the token is missing. Validation uses a timing-safe comparison. - It parses standard Twilio webhook parameters.
- It uses the
twilio
library'sMessagingResponse
to generate TwiML for replies. - The response
Content-Type
is correctly set totext/xml
. - If no reply is needed, you should send an empty
204 No Content
response instead of TwiML.
- The controller listens for POST requests at
-
Update
WebhookModule
:// src/webhook/webhook.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; // Import ConfigModule import { WebhookController } from './webhook.controller'; // Import PrismaModule if you inject PrismaService into the controller // import { PrismaModule } from '../prisma/prisma.module'; @Module({ imports: [ ConfigModule, // Make ConfigService available // PrismaModule // Import if needed by controller/services ], controllers: [WebhookController], providers: [], // Add any webhook-specific services here if needed }) export class WebhookModule {}
-
Import
WebhookModule
intoAppModule
:// 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 { TwilioModule } from './twilio/twilio.module'; import { WebhookModule } from './webhook/webhook.module'; // Import WebhookModule // Other imports... // import { PrismaModule } from './prisma/prisma.module'; // Import if using Prisma // import { MessageModule } from './message/message.module'; // Import if using API layer @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', }), TwilioModule, WebhookModule, // Add WebhookModule // PrismaModule, // Add if using Prisma // MessageModule, // Add if using API layer // Other modules... ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
3. Building a complete API layer (Optional)
While the core functionality is often driven by the webhook, you might want an API endpoint to trigger outbound messages from other parts of your system or external clients.
-
Generate a Message module, controller, and service:
nest generate module message nest generate controller message nest generate service message
-
Create Data Transfer Objects (DTOs) for validation: Install class-validator and class-transformer:
npm install class-validator class-transformer # or yarn add class-validator class-transformer
Enable
ValidationPipe
globally insrc/main.ts
:// src/main.ts import { NestFactory, HttpAdapterHost } from '@nestjs/core'; // Import HttpAdapterHost if using filter import { AppModule } from './app.module'; import { ConfigService } from '@nestjs/config'; import { Logger, ValidationPipe } from '@nestjs/common'; // Import ValidationPipe // import { AllExceptionsFilter } from './common/filters/http-exception.filter'; // Import if using global filter async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); const port = configService.get<number>('PORT', 3000); // Default to 3000 if not set const logger = new Logger('Bootstrap'); // Enable global validation pipe app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Strip properties not defined in DTO transform: true, // Automatically transform payloads to DTO instances forbidNonWhitelisted: true, // Throw error if unknown properties are present transformOptions: { enableImplicitConversion: true, // Allow automatic type conversion (e.g., string -> number) }, })); // Apply global exception filter (optional, but recommended) // const httpAdapter = app.get(HttpAdapterHost); // app.useGlobalFilters(new AllExceptionsFilter(httpAdapter)); // Pass adapter if needed by filter await app.listen(port); logger.log(`Application listening on port ${port}`); } bootstrap();
Create DTO files:
// src/message/dto/send-message.dto.ts import { IsNotEmpty, IsString, Matches, IsOptional, IsUrl, ValidateIf } from 'class-validator'; export class SendMessageDto { @IsNotEmpty() @IsString() // E.164 format allows optional '+' and 1-15 digits @Matches(/^\+?[1-9]\d{1,14}$/, { message: 'to phone number must be in E.164 format (e.g., +15551234567)' }) to: string; // E.164 format (e.g., +15551234567) @ValidateIf(o => !o.mediaUrl) // body is required if mediaUrl is not provided @IsNotEmpty({ message: 'Either body or mediaUrl must be provided' }) @IsString() @IsOptional() // Mark as optional since it's conditionally required body?: string; @ValidateIf(o => !o.body) // mediaUrl is required if body is not provided @IsNotEmpty({ message: 'Either body or mediaUrl must be provided' }) @IsString() @IsUrl({}, { message: 'mediaUrl must be a valid URL' }) @IsOptional() // Mark as optional since it's conditionally required mediaUrl?: string; }
-
Implement
MessageService
: This service will useTwilioService
to send messages.// src/message/message.service.ts import { Injectable, Logger, BadRequestException, InternalServerErrorException } from '@nestjs/common'; import { TwilioService } from '../twilio/twilio.service'; import { SendMessageDto } from './dto/send-message.dto'; // Import PrismaService if saving outbound messages // import { PrismaService } from '../prisma/prisma.service'; // import { Direction } from '@prisma/client'; // Assuming Prisma schema defines this enum @Injectable() export class MessageService { private readonly logger = new Logger(MessageService.name); constructor( private readonly twilioService: TwilioService, // private readonly prisma: PrismaService // Inject if saving messages // private readonly configService: ConfigService // Inject if needed for accountSid ) {} async sendWhatsApp(sendMessageDto: SendMessageDto): Promise<{ sid: string }> { const { to, body, mediaUrl } = sendMessageDto; // DTO validation should prevent both being empty, but double-check for safety if (!body && !mediaUrl) { // This case should ideally be caught by ValidationPipe based on DTO rules throw new BadRequestException('Either body or mediaUrl must be provided.'); } let sid: string | null = null; let errorOccurred = false; try { if (mediaUrl) { // Note: TwilioService adds the 'whatsapp:' prefix internally sid = await this.twilioService.sendWhatsAppMediaMessage(to, mediaUrl, body); } else if (body) { // Note: TwilioService adds the 'whatsapp:' prefix internally sid = await this.twilioService.sendWhatsAppMessage(to, body); } } catch (error) { // Log the detailed error from TwilioService or Twilio client this.logger.error(`Error sending WhatsApp message to ${to}: ${error.message}`, error.stack); errorOccurred = true; // sid remains null } if (!sid || errorOccurred) { // Throw an internal server error after logging if Twilio call failed throw new InternalServerErrorException(`Failed to send WhatsApp message to ${to} via Twilio.`); } this.logger.log(`Successfully queued WhatsApp message to ${to}. SID: ${sid}`); // --- Optional: Save Outbound Message to DB --- /* try { const savedMessage = await this.prisma.message.create({ data: { twilioSid: sid, // accountSid: this.configService.get('TWILIO_ACCOUNT_SID'), // Need ConfigService injected from: this.twilioService.getWhatsAppNumber(), // Use helper from TwilioService to: `whatsapp:${to}`, // Ensure prefix for consistency in DB body: body, numMedia: mediaUrl ? 1 : 0, mediaUrl: mediaUrl, // mediaType: Needs more info, maybe fetch status callback? Complex. direction: Direction.outbound, // Assumes Direction enum exists status: 'queued', // Initial status from Twilio API response // Add other relevant fields like userId, correlationId etc. }, }); this.logger.log(`Saved outbound message ${sid} to database with ID ${savedMessage.id}.`); } catch (dbError) { this.logger.error(`Failed to save outbound message ${sid} to database after sending: ${dbError.message}`, dbError.stack); // Decide if this should cause the request to fail or just log the error. // Generally, failing the DB save after a successful send is undesirable. } */ return { sid }; } }
-
Implement
MessageController
:// src/message/message.controller.ts import { Controller, Post, Body, UseGuards, Logger, HttpCode, HttpStatus } from '@nestjs/common'; import { MessageService } from './message.service'; import { SendMessageDto } from './dto/send-message.dto'; // Import an AuthGuard if you implement authentication // import { JwtAuthGuard } from '../auth/jwt-auth.guard'; // Example @Controller('messages') export class MessageController { private readonly logger = new Logger(MessageController.name); constructor(private readonly messageService: MessageService) {} // Example: Protect this endpoint with authentication if needed // @UseGuards(JwtAuthGuard) // Uncomment and configure if using auth @Post('whatsapp/send') @HttpCode(HttpStatus.ACCEPTED) // Use 202 Accepted as sending is often asynchronous async sendWhatsAppMessage(@Body() sendMessageDto: SendMessageDto): Promise<{ message: string; sid: string }> { this.logger.log(`Received request to send WhatsApp message to ${sendMessageDto.to}`); // Validation is handled by the global ValidationPipe via SendMessageDto const result = await this.messageService.sendWhatsApp(sendMessageDto); return { message: 'WhatsApp message accepted for delivery.', sid: result.sid, }; } }
- The controller uses the DTO for request body validation.
- It delegates the sending logic to
MessageService
. - Consider adding authentication/authorization (
@UseGuards
) to protect this endpoint. - Returns
202 Accepted
as the message is queued, not necessarily delivered instantly.
-
Update Modules: Ensure
MessageService
is inproviders
andMessageController
is incontrollers
withinsrc/message/message.module.ts
. ImportTwilioModule
intoMessageModule
to makeTwilioService
available for injection. Finally, importMessageModule
intosrc/app.module.ts
.// src/message/message.module.ts import { Module } from '@nestjs/common'; import { MessageService } from './message.service'; import { MessageController } from './message.controller'; import { TwilioModule } from '../twilio/twilio.module'; // Import TwilioModule // Import PrismaModule if MessageService uses it // import { PrismaModule } from '../prisma/prisma.module'; // Import ConfigModule if MessageService uses it // import { ConfigModule } from '@nestjs/config'; @Module({ imports: [ TwilioModule, // Make TwilioService available // PrismaModule // Import if needed // ConfigModule // Import if needed ], controllers: [MessageController], providers: [MessageService], }) export class MessageModule {} // src/app.module.ts (ensure MessageModule is added) import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { TwilioModule } from './twilio/twilio.module'; import { WebhookModule } from './webhook/webhook.module'; import { MessageModule } from './message/message.module'; // Import MessageModule // import { PrismaModule } from './prisma/prisma.module'; // Import if using Prisma @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env', }), TwilioModule, WebhookModule, MessageModule, // Add MessageModule // PrismaModule, // Add if using Prisma ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
Testing the API Endpoint:
You can use curl
or Postman to test the /messages/whatsapp/send
endpoint (replace YOUR_PORT
and YOUR_PHONE_NUMBER
):
# Send a text message
curl -X POST http://localhost:YOUR_PORT/messages/whatsapp/send \
-H 'Content-Type: application/json' \
-d '{
""to"": ""+1YOUR_PHONE_NUMBER"",
""body"": ""Hello from NestJS API!""
}'
# Send a media message (replace URL)
curl -X POST http://localhost:YOUR_PORT/messages/whatsapp/send \
-H 'Content-Type: application/json' \
-d '{
""to"": ""+1YOUR_PHONE_NUMBER"",
""mediaUrl"": ""https://demo.twilio.com/owl.png"",
""body"": ""Optional caption for image""
}'