Production-Ready Vonage WhatsApp Integration with Fastify and Node.js
This guide provides a complete walkthrough for building a production-ready Node.js application using the Fastify framework to send and receive WhatsApp messages via the Vonage Messages API. We'll cover everything from initial project setup and Vonage configuration to implementing core messaging logic, handling webhooks securely, adding essential production considerations like logging and error handling, and finally, testing and deployment concepts.
Project Goal: To create a robust Fastify backend service capable of:
- Receiving incoming WhatsApp messages sent to a Vonage number via webhooks.
- Replying to those incoming messages automatically.
- Securely handling webhook validation using JWT signatures.
- Providing a foundation for more complex WhatsApp bot interactions.
Why This Approach?
- Fastify: A high-performance, low-overhead Node.js web framework focused on developer experience and speed. Its plugin architecture and built-in features like logging and schema validation make it excellent for building robust APIs.
- Vonage Messages API: A unified API for sending and receiving messages across multiple channels, including WhatsApp, SMS, MMS, Facebook Messenger, and Viber. We'll use its WhatsApp capabilities via the official Vonage Node SDK.
- Vonage WhatsApp Sandbox: Enables rapid development and testing without needing a dedicated WhatsApp Business Account initially.
- Webhook Architecture: The standard mechanism for real-time communication from platforms like Vonage to your application.
System Architecture:
graph LR
User[WhatsApp User] -- 1. Sends Message --> WhatsApp
WhatsApp -- 2. Forwards to Vonage --> Vonage[Vonage Platform]
Vonage -- 3. Sends Inbound Webhook --> Ngrok[(ngrok Tunnel)]
Ngrok -- 4. Forwards Request --> App[Fastify App]
App -- 5. Processes & Sends Reply --> Vonage
Vonage -- 6. Sends Status Webhook --> Ngrok
Ngrok -- 7. Forwards Request --> App
Vonage -- 8. Delivers Reply --> WhatsApp
WhatsApp -- 9. Displays Reply --> User
style App fill:#f9f,stroke:#333,stroke-width:2px
style Ngrok fill:#ccf,stroke:#333,stroke-width:2px
style Vonage fill:#aaf,stroke:#333,stroke-width:2px
Prerequisites:
- Node.js: Version 20 or higher installed.
- npm or yarn: Node.js package manager.
- Vonage API Account: Sign up for free at vonage.com if you don't have one. You'll get free credit to start.
- WhatsApp Account: A personal WhatsApp account on a smartphone for testing.
- ngrok: A tool to expose your local development server to the internet. Download and install it from ngrok.com. You'll need to sign up for a free account to get longer-lived tunnel URLs.
Final Outcome: A running Fastify application on your local machine, connected to the Vonage WhatsApp Sandbox via ngrok, capable of receiving WhatsApp messages and sending automated replies.
1. Setting Up the Project
Let's initialize our Node.js project using Fastify.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
mkdir fastify-vonage-whatsapp cd fastify-vonage-whatsapp
-
Initialize Node.js Project: Create a
package.json
file.npm init -y
-
Install Dependencies: We need Fastify, its environment variable handler (
@fastify/env
), sensible defaults (@fastify/sensible
), and the Vonage SDK components.npm install fastify @fastify/env @fastify/sensible @vonage/server-sdk @vonage/messages @vonage/jwt
fastify
: The core web framework.@fastify/env
: For loading and validating environment variables from a.env
file.@fastify/sensible
: Provides sensible defaults for error handling, security headers, etc.@vonage/server-sdk
: The main Vonage SDK package.@vonage/messages
: Specifically for using the Messages API.@vonage/jwt
: For verifying webhook signatures.
-
Project Structure: Create the following basic structure:
fastify-vonage-whatsapp/ ├── src/ │ ├── hooks/ │ ├── routes/ │ ├── services/ │ └── server.js ├── .env ├── .gitignore ├── package.json └── private.key # Added via Vonage setup, ensure it's gitignored
src/
: Contains our application source code.src/hooks/
: For Fastify request lifecycle hooks (like authentication/validation).src/routes/
: To define our API endpoints (webhooks).src/services/
: For business logic, like interacting with the Vonage API.src/server.js
: The main application entry point where the Fastify server is configured and started..env
: Stores sensitive configuration and API keys (will be created later)..gitignore
: Specifies files/directories Git should ignore.private.key
: The private key downloaded from Vonage (ensure it's listed in.gitignore
).
-
Create
.gitignore
: Add common Node.js ignores, plus our sensitive files:# .gitignore # Node node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* pids/ *.pid *.seed *.log *.log.* # Environment Variables .env* !/.env.example # Vonage Private Key private.key # OS generated files .DS_Store Thumbs.db
-
Basic Fastify Server (
src/server.js
): Create a minimal Fastify server to ensure the setup works.// src/server.js 'use strict'; const Fastify = require('fastify'); // Instantiate Fastify with basic logging const fastify = Fastify({ logger: true, // Use true for basic Pino logging, or configure logger options }); // Declare a simple health check route fastify.get('/health', async (request, reply) => { return { status: 'ok' }; }); // Run the server const start = async () => { try { // For now, hardcode the port. We'll use environment variables later. await fastify.listen({ port: 8000, host: '0.0.0.0' }); fastify.log.info(`Server listening on ${fastify.server.address().port}`); } catch (err) { fastify.log.error(err); process.exit(1); } }; start();
-
Run the Basic Server: Execute the server file.
node src/server.js
You should see log output indicating the server is listening on port 8000. You can test the health check endpoint using
curl http://localhost:8000/health
in another terminal window. Stop the server withCtrl+C
.
2. Vonage Configuration
Now, let's configure your Vonage account and application to work with the Messages API and WhatsApp Sandbox.
-
Create a Vonage Application:
- Log in to your Vonage API Dashboard.
- Navigate to
`Applications`
->`Create a new application`
. - Give your application a name (e.g.,
Fastify WhatsApp Demo
). - Generate Public/Private Key Pair: Click the
`Generate public and private key`
button. This will download aprivate.key
file. Save this file securely. For this guide, place it in the root directory (fastify-vonage-whatsapp/private.key
). We addedprivate.key
to.gitignore
to prevent accidentally committing it. Warning: While gitignored, storing private keys directly within your application's directory structure is generally discouraged for security reasons, even locally. A better practice is to store it outside the project folder if your local setup allows. Under no circumstances should this key ever be committed to version control. The public key is automatically associated with your application in the dashboard. - Enable Capabilities: Scroll down to
`Capabilities`
and toggle`Messages`
ON. You'll see fields for`Inbound URL`
and`Status URL`
. We'll fill these in the next section after setting up ngrok. - Click
`Create application`
. - Note Your Application ID: After creation, you'll be taken to the application's page. Copy the
Application ID
- you'll need it for your environment variables.
-
Set Up Vonage WhatsApp Sandbox:
- In the Vonage Dashboard sidebar, navigate to
`Messages API`
->`Sandbox`
. - Read the instructions. You need to add the Vonage Sandbox contact number to your phone's contacts and send it a specific message via WhatsApp to opt-in your number for testing.
- Add the Sandbox Number: The number is displayed prominently (it's often
+14157386102
, but always verify the current number shown on your specific Sandbox page as it can sometimes change). Save it as a contact (e.g.,Vonage Sandbox
). - Send Opt-in Message: Open WhatsApp on your phone, find the
Vonage Sandbox
contact, and send it the exact opt-in phrase shown on the Sandbox page (it includes a unique keyword). - Allowlist Your Number: Once Vonage receives your message, your number will appear in the
`Allowlisted testing numbers`
section on the Sandbox page. Only numbers listed here can interact with the Sandbox. - Configure Sandbox Webhooks: Similar to the application, the Sandbox page also has fields for
`Inbound webhook URL`
and`Status webhook URL`
. We will configure these along with the application webhooks in the next step. For the Sandbox to work correctly, both the Application and the Sandbox webhooks need to be set.
- In the Vonage Dashboard sidebar, navigate to
-
Configure Environment Variables (
.env
): Create a file named.env
in the root of your project (fastify-vonage-whatsapp/.env
). Add the following variables, replacing the placeholders with your actual credentials:# .env # Server Configuration PORT=8000 HOST=0.0.0.0 # Listen on all available network interfaces # Vonage API Credentials # Found on the Vonage API Dashboard homepage VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET # Vonage Application Details # Found in Your Applications -> [Your Application Name] VONAGE_APPLICATION_ID=YOUR_APPLICATION_ID # Path relative to the project root where you saved the private key VONAGE_PRIVATE_KEY=./private.key # Vonage WhatsApp Sandbox Number # Found on the Messages API -> Sandbox page. **VERIFY THE NUMBER ON YOUR DASHBOARD**. Do not include '+'. VONAGE_WHATSAPP_NUMBER=YOUR_SANDBOX_NUMBER_FROM_DASHBOARD # Vonage Signature Secret # Found in Vonage Dashboard -> Settings -> API settings -> Default Signing Secret for API Key VONAGE_API_SIGNATURE_SECRET=YOUR_SIGNATURE_SECRET # Vonage API Host (Use Sandbox URL for testing) VONAGE_API_HOST=https://messages-sandbox.nexmo.com
Explanation of Variables:
PORT
,HOST
: Configure where your Fastify server listens.0.0.0.0
is important for containerized environments or VMs.VONAGE_API_KEY
,VONAGE_API_SECRET
: Your main Vonage account credentials. Found directly on the dashboard landing page after logging in.VONAGE_APPLICATION_ID
: The unique ID for the Vonage Application you created.VONAGE_PRIVATE_KEY
: The path to theprivate.key
file you downloaded. The path./private.key
assumes you run thenode src/server.js
command from the project's root directory (fastify-vonage-whatsapp/
). If running from a different directory, adjust the path accordingly or consider using an absolute path during development (though this is less portable). See Section 9 for how this is handled in deployment.VONAGE_WHATSAPP_NUMBER
: The specific phone number assigned to the Vonage WhatsApp Sandbox. Important: Verify the current number on your Vonage dashboard and use it without a leading+
or00
.VONAGE_API_SIGNATURE_SECRET
: Used by Vonage to sign webhook requests with a JWT. You verify this signature to ensure the request genuinely came from Vonage. Find this in your main dashboard Settings page under API settings.VONAGE_API_HOST
: Specifies the API endpoint URL. For testing with the Sandbox, usehttps://messages-sandbox.nexmo.com
. For production with a WhatsApp Business Account, you'd use the standard production URL (https://api.nexmo.com
).
3. Setting Up Webhooks with ngrok
Vonage needs a publicly accessible URL to send webhook events (like incoming messages and status updates) to your application running locally. ngrok
creates a secure tunnel from the internet to your machine. While other tunneling services like localtunnel
or Cloudflare Tunnels exist, this guide uses ngrok
due to its popularity and ease of use for development.
-
Start ngrok: Open a new terminal window (keep your Fastify server terminal separate). Run ngrok, telling it to forward traffic to the port your Fastify app is listening on (Port 8000 in our case).
# Make sure you are logged into ngrok for longer sessions: ngrok config add-authtoken YOUR_TOKEN ngrok http 8000
-
Copy the ngrok Forwarding URL: ngrok will display output similar to this:
Forwarding https://<random-string>.ngrok-free.app -> http://localhost:8000
Copy the
https://...
URL (yours will have a different random string). This is your public URL. Note: Free ngrok URLs change every time you restart ngrok. -
Configure Vonage Webhooks:
- Go back to your Vonage Application settings in the dashboard (Applications -> Your Application).
- Under the
`Messages`
capability:- Set Inbound URL:
YOUR_NGROK_URL/webhooks/inbound
(e.g.,https://<random-string>.ngrok-free.app/webhooks/inbound
) - Set Status URL:
YOUR_NGROK_URL/webhooks/status
(e.g.,https://<random-string>.ngrok-free.app/webhooks/status
)
- Set Inbound URL:
- Click
`Save changes`
. - Now, go to the Vonage Messages API Sandbox page in the dashboard.
- Enter the exact same URLs in the webhook fields there:
- Inbound webhook URL:
YOUR_NGROK_URL/webhooks/inbound
- Status webhook URL:
YOUR_NGROK_URL/webhooks/status
- Inbound webhook URL:
- Click
`Save webhooks`
.
Why
/webhooks/inbound
and/webhooks/status
? These are the specific endpoints we will create in our Fastify application to handle these two types of events. It's crucial that both the Application and Sandbox webhooks point to your ngrok tunnel.
4. Implementing Core Functionality (Fastify Adaptation)
Now, let's write the Fastify code to handle the webhooks and send replies.
-
Load Environment Variables (
src/server.js
): Use@fastify/env
to load and validate variables from.env
.// src/server.js 'use strict'; const Fastify = require('fastify'); const sensible = require('@fastify/sensible'); const fastifyEnv = require('@fastify/env'); const path = require('path'); // Needed for path resolution // Define schema for environment variables const schema = { type: 'object', required: [ 'PORT', 'HOST', 'VONAGE_API_KEY', 'VONAGE_API_SECRET', 'VONAGE_APPLICATION_ID', 'VONAGE_PRIVATE_KEY', // Path or content 'VONAGE_WHATSAPP_NUMBER', 'VONAGE_API_SIGNATURE_SECRET', 'VONAGE_API_HOST', ], properties: { PORT: { type: 'string', default: 8000 }, HOST: { type: 'string', default: '0.0.0.0' }, VONAGE_API_KEY: { type: 'string' }, VONAGE_API_SECRET: { type: 'string' }, VONAGE_APPLICATION_ID: { type: 'string' }, VONAGE_PRIVATE_KEY: { type: 'string' }, // Can be path or key content VONAGE_WHATSAPP_NUMBER: { type: 'string' }, VONAGE_API_SIGNATURE_SECRET: { type: 'string' }, VONAGE_API_HOST: { type: 'string' }, }, }; const envOptions = { confKey: 'config', // Access environment variables via `fastify.config` schema: schema, dotenv: true, // Load .env file }; const fastify = Fastify({ logger: { level: 'info', // Default log level // Pretty print logs in development, use structured JSON in production transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty', options: { translateTime: 'HH:MM:ss Z', ignore: 'pid,hostname', }, } : undefined, }, }); // Register plugins fastify .register(fastifyEnv, envOptions) .register(sensible) .register(require('./routes/webhooks')); // Register routes AFTER env // Start function remains the same, but uses config const start = async () => { try { // Wait for plugins to load, especially fastifyEnv await fastify.ready(); await fastify.listen({ port: fastify.config.PORT, host: fastify.config.HOST, }); // Note: fastify.server.address() might be null until listen completes // We log the port from config directly now fastify.log.info( `Server listening on host ${fastify.config.HOST} port ${fastify.config.PORT}` ); } catch (err) { fastify.log.error(err); process.exit(1); } }; start(); // Export fastify instance if needed for testing module.exports = fastify;
- We define a JSON schema for expected environment variables.
@fastify/env
will throw an error on startup if required variables are missing. - We configure Pino (Fastify's logger) for pretty printing during development (
NODE_ENV !== 'production'
). fastify.ready()
ensures plugins like@fastify/env
are loaded before we accessfastify.config
.- Routes are registered after core plugins like
fastifyEnv
.
- We define a JSON schema for expected environment variables.
-
Vonage Service (
src/services/vonageService.js
): Create a service to encapsulate Vonage client initialization and message sending logic, handling private key as path or content.// src/services/vonageService.js 'use strict'; const { Vonage } = require('@vonage/server-sdk'); const { WhatsAppText } = require('@vonage/messages'); const path = require('path'); const fs = require('fs'); let vonageClient; function initializeVonageClient(config, logger) { if (!vonageClient) { let privateKeyContent = config.VONAGE_PRIVATE_KEY; // Check if the variable content looks like a path vs the actual key const looksLikePath = /[\\/]|(\.key$)/.test(privateKeyContent); const looksLikePEMKey = privateKeyContent.startsWith('-----BEGIN PRIVATE KEY-----'); if (looksLikePath && !looksLikePEMKey) { // Assume it's a path, try to read the file const privateKeyPath = path.resolve(privateKeyContent); // Resolve relative to CWD try { privateKeyContent = fs.readFileSync(privateKeyPath, 'utf8'); logger.info(`Read private key from path: ${privateKeyPath}`); } catch (err) { logger.error(`Failed to read private key from configured path: ${privateKeyPath}`); logger.error(err); // If reading fails, check if maybe it WAS the key content after all but looked like a path if (config.VONAGE_PRIVATE_KEY.startsWith('-----BEGIN PRIVATE KEY-----')) { logger.warn('VONAGE_PRIVATE_KEY looked like a path but failed to read; attempting to use content directly.'); // Reset to use original content privateKeyContent = config.VONAGE_PRIVATE_KEY; } else { throw new Error('Could not read VONAGE_PRIVATE_KEY file and content does not look like a PEM key. Check path/permissions or provide key content directly.'); } } } else if (looksLikePEMKey) { // Assume it's the key content itself logger.info('Using private key directly from VONAGE_PRIVATE_KEY environment variable content.'); } else { // Content is ambiguous or invalid logger.error('VONAGE_PRIVATE_KEY content is neither a readable path nor does it look like a PEM private key.'); throw new Error('Invalid VONAGE_PRIVATE_KEY configuration.'); } // Now initialize Vonage with the resolved privateKeyContent vonageClient = new Vonage( { apiKey: config.VONAGE_API_KEY, apiSecret: config.VONAGE_API_SECRET, applicationId: config.VONAGE_APPLICATION_ID, privateKey: privateKeyContent, // Use the resolved key content here }, { apiHost: config.VONAGE_API_HOST, // Use the sandbox or production host logger: logger.child({ service: 'vonage-sdk' }), // Pass a child logger for context } ); logger.info('Vonage client initialized.'); } return vonageClient; } async function sendWhatsAppReply(to_number, text, config, logger) { const client = initializeVonageClient(config, logger); // Ensure client is initialized const from_number = config.VONAGE_WHATSAPP_NUMBER; logger.info(`Attempting to send WhatsApp message from ${from_number} to ${to_number}`); try { const response = await client.messages.send( new WhatsAppText({ from: from_number, to: to_number, text: text, }) ); logger.info( { msgUuid: response.messageUUID }, `Message sent successfully` ); return response; } catch (error) { logger.error('Error sending WhatsApp message via Vonage:'); // Log detailed error information if available if (error.response && error.response.data) { logger.error( { vonageError: error.response.data, statusCode: error.response.status }, `Vonage API Error` ); } else { logger.error(error); } // Re-throw or handle appropriately for the caller throw new Error('Failed to send WhatsApp message.'); } } module.exports = { initializeVonageClient, sendWhatsAppReply, };
- Detects if
VONAGE_PRIVATE_KEY
is a path or the key content. - Reads the file content if it looks like a path.
- Uses the content directly if it looks like a PEM key.
- Initializes the
Vonage
client using configuration values and the resolved key content. - Uses the specific
apiHost
from the config. - Includes detailed error logging.
- Passes a child logger to the Vonage SDK for better context.
- Detects if
-
JWT Signature Verification Hook (
src/hooks/verifyVonageSignature.js
): Create a Fastify hook to verify theAuthorization
header on incoming status webhooks.// src/hooks/verifyVonageSignature.js 'use strict'; const { verifySignature } = require('@vonage/jwt'); const fp = require('fastify-plugin'); async function verifyVonageSignatureHook(fastify, options) { // Decorate fastify instance with the verification function // so it can be easily used in route options (preHandler) fastify.decorate('verifyVonageSignature', async (request, reply) => { fastify.log.trace('Attempting Vonage JWT signature verification...'); // Use trace for verbose steps const authHeader = request.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { fastify.log.warn('Missing or invalid Authorization header for webhook'); reply.code(401).send({ error: 'Unauthorized: Missing or invalid JWT Bearer token' }); return; // Stop further execution } const token = authHeader.split(' ')[1]; const secret = fastify.config.VONAGE_API_SIGNATURE_SECRET; if (!secret) { fastify.log.error('VONAGE_API_SIGNATURE_SECRET is not configured. Cannot verify webhook signature.'); reply.code(500).send({ error: 'Internal Server Error: Signature secret not configured' }); return; } try { // Note: verifySignature throws on error in some versions, returns bool in others. // Let's assume it returns boolean for safety. const isValid = verifySignature(token, secret); if (!isValid) { fastify.log.warn('Invalid Vonage JWT signature received.'); reply.code(403).send({ error: 'Forbidden: Invalid signature' }); return; // Stop further execution } fastify.log.info('Vonage JWT signature verified successfully.'); } catch (err) { // Catch potential errors from the verifySignature function itself fastify.log.error({ err }, 'Error during JWT signature verification process'); reply.code(500).send({ error: 'Internal Server Error during JWT verification' }); return; // Stop further execution } }); } // Export as a plugin module.exports = fp(verifyVonageSignatureHook);
- Uses
fastify-plugin
to properly encapsulate the hook/decorator. - Defines a decorator
verifyVonageSignature
which contains the verification logic. - Extracts the JWT from the
Authorization: Bearer <token>
header. - Uses
@vonage/jwt
'sverifySignature
function with the secret fromfastify.config
. - Sends appropriate error responses (
401
,403
,500
) if verification fails or config is missing.
- Uses
-
Webhook Routes (
src/routes/webhooks.js
): Define the routes to handle/webhooks/inbound
and/webhooks/status
.// src/routes/webhooks.js 'use strict'; const { sendWhatsAppReply } = require('../services/vonageService'); async function webhookRoutes(fastify, options) { // Register the JWT verification hook/decorator plugin // This makes fastify.verifyVonageSignature available fastify.register(require('../hooks/verifyVonageSignature')); // --- Inbound Message Webhook --- // No JWT verification applied here by default, as per common practice fastify.post('/webhooks/inbound', async (request, reply) => { fastify.log.info('Received /webhooks/inbound POST request'); fastify.log.debug({ body: request.body }, 'Inbound webhook payload:'); // Use debug for full payload // Basic validation: Check if it's a WhatsApp message with necessary fields if ( !request.body || request.body.channel !== 'whatsapp' || !request.body.from?.number || // Use optional chaining !request.body.message?.content?.text // Check for text content specifically ) { fastify.log.warn({ body: request.body }, 'Invalid, non-WhatsApp, or non-text inbound payload received.'); // Send 200 OK to Vonage even for invalid payloads we don't process, // to prevent retries for malformed requests. Log the issue. return reply.code(200).send(); } const requesterNumber = request.body.from.number; const messageText = request.body.message.content.text; const messageUuid = request.body.message_uuid; // Log the message UUID fastify.log.info( { from: requesterNumber, msgUuid: messageUuid }, `Incoming WhatsApp message: ""${messageText}""` ); // Simple auto-reply logic const replyText = `Received your message: ""${messageText}""`; try { await sendWhatsAppReply( requesterNumber, replyText, fastify.config, // Pass config fastify.log // Pass logger ); fastify.log.info({ to: requesterNumber }, `Reply sent successfully`); // Acknowledge receipt to Vonage reply.code(200).send(); } catch (error) { fastify.log.error( { err: error, to: requesterNumber }, `Failed to process inbound message or send reply` ); // Send 500 Internal Server Error if our processing fails reply.code(500).send({ error: 'Failed to process message' }); } }); // --- Status Webhook --- const statusRouteOptions = { // Apply the JWT verification hook specifically to this route's preHandler phase preHandler: [fastify.verifyVonageSignature], // Use the decorated function }; fastify.post('/webhooks/status', statusRouteOptions, async (request, reply) => { fastify.log.info('Received /webhooks/status POST request'); fastify.log.debug({ body: request.body }, 'Status webhook payload:'); // Use debug for payload // Process the status update (e.g., log it, update database) const { message_uuid, status, timestamp, error, usage } = request.body; if (message_uuid && status) { const logData = { msgUuid: message_uuid, status, timestamp, usage }; if (error) { logData.error = error; fastify.log.warn(logData, `Message status update: ${status} with error`); } else { fastify.log.info(logData, `Message status update: ${status}`); } // In a real app: await updateMessageStatusInDB(message_uuid, status, error); } else { fastify.log.warn({ body: request.body }, 'Received status webhook with missing message_uuid or status'); } // Acknowledge receipt to Vonage reply.code(200).send(); }); } module.exports = webhookRoutes;
- Registers the
verifyVonageSignature
plugin. - Inbound Route:
- Logs incoming payload (using
debug
level). - Performs basic validation for WhatsApp text messages.
- Extracts sender number, message text, and UUID.
- Calls
sendWhatsAppReply
. - Handles errors and returns
200 OK
or500
.
- Logs incoming payload (using
- Status Route:
- Uses
preHandler: [fastify.verifyVonageSignature]
to apply the JWT check before the main handler runs. - Logs the received status payload details.
- Returns
200 OK
.
- Uses
- Registers the
-
Register Routes in
src/server.js
: This was already corrected in step 1 of this section to ensure routes are registered after core plugins. The code in step 1 is final.
5. Error Handling and Logging
Fastify provides excellent built-in logging via Pino and error handling capabilities.
- Logging:
- We configured Pino with
pino-pretty
for readable development logs insrc/server.js
. In production (NODE_ENV=production
), it logs structured JSON, suitable for log aggregation services (like Datadog, Splunk, ELK stack). - We pass
fastify.log
(or child loggers) to our services (vonageService.js
) for consistent, contextual logging. - We log key events: server start, incoming requests (with debug for payload), message content, status updates, successful replies, and crucially, errors with context (including relevant IDs like
message_uuid
or phone numbers). Different log levels (info
,warn
,error
,debug
,trace
) are used appropriately.
- We configured Pino with
- Error Handling:
@fastify/sensible
provides standard error handling and utility error constructors.- Our JWT hook (
verifyVonageSignature.js
) explicitly sends401
,403
or500
errors upon verification failure or configuration issues. - Route handlers use
try...catch
blocks:- To catch errors during Vonage API calls (
sendWhatsAppReply
). - To handle unexpected issues during request processing.
- To catch errors during Vonage API calls (
- When our server fails internally (e.g., cannot send reply, error during JWT verification), we log the error and return a
500 Internal Server Error
to Vonage. - For invalid incoming webhook payloads (e.g., wrong format, missing fields on
/webhooks/inbound
), we log the issue and return a200 OK
to Vonage to prevent unnecessary retries, while ensuring our logs capture the problem.