code examples
code examples
Developer Guide: Sending WhatsApp Messages via AWS SNS with Fastify
A guide on building a Node.js Fastify application to publish messages to AWS SNS for triggering WhatsApp message delivery via downstream services like AWS Lambda and the WhatsApp Business API.
This guide details how to build a production-ready system using Fastify and Node.js to publish messages to an AWS Simple Notification Service (SNS) topic. These SNS messages can then trigger downstream processes – typically an AWS Lambda function – to send WhatsApp messages via the official WhatsApp Business API (like Meta's Cloud API).
This approach decouples your main application from the direct interaction with the WhatsApp API, leveraging SNS for resilience and scalability. Your Fastify application focuses on validating requests and initiating the messaging workflow by publishing to SNS.
Project Goals:
- Create a Fastify API endpoint that accepts requests to send WhatsApp messages.
- Validate incoming requests for required parameters (phone number, message content).
- Securely publish validated message details to a designated AWS SNS topic using the AWS SDK.
- Provide a robust foundation for a decoupled messaging system.
Technology Stack:
- Node.js: Runtime environment.
- Fastify: High-performance web framework for Node.js.
- AWS SNS: Fully managed pub/sub messaging service.
- AWS SDK for JavaScript v3: Used for interacting with AWS SNS.
- Downstream Components (Implied): AWS Lambda, WhatsApp Business API (e.g., Meta Cloud API).
System Architecture:
graph LR
A[Client/User] -- HTTP POST --> B(Fastify App);
B -- Validate & Format --> B;
B -- Publish Message --> C(AWS SNS Topic);
C -- Trigger --> D(AWS Lambda Function);
D -- Send Request --> E(WhatsApp Business API);
E -- Deliver Message --> F(End User's WhatsApp);Prerequisites:
- Node.js (LTS version recommended) and npm/yarn installed.
- An AWS account with permissions to manage IAM and SNS.
- Basic familiarity with Node.js, Fastify, and AWS concepts.
- (For end-to-end testing) Access to configure a WhatsApp Business API sender and an AWS Lambda function. This guide focuses on the Fastify -> SNS part.
1. Setting up the project
Let's initialize the Node.js project, install dependencies, and configure the basic structure and environment.
1.1. Initialize Project:
Open your terminal and create a new project directory:
mkdir fastify-sns-whatsapp
cd fastify-sns-whatsapp
npm init -y1.2. Install Dependencies:
We need Fastify, the AWS SDK v3 SNS client, and dotenv for managing environment variables.
npm install fastify @aws-sdk/client-sns dotenvfastify: The core web framework.@aws-sdk/client-sns: AWS SDK v3 module for interacting with SNS.dotenv: Loads environment variables from a.envfile intoprocess.env.
1.3. Project Structure:
Create the following basic structure:
fastify-sns-whatsapp/src/routes/whatsapp.js# API routes for sending messages
server.js# Fastify server setup
.env# Environment variables (DO NOT COMMIT).gitignore# Git ignore filepackage.json
1.4. Configure AWS Credentials:
Your application needs AWS credentials to interact with SNS. The AWS SDK looks for credentials in the standard locations: environment variables, shared credential file (~/.aws/credentials), or IAM role (if running on EC2/ECS/Lambda).
Recommendation: Use an IAM User with programmatic access specifically for this application.
- Navigate to IAM: In the AWS Management Console, go to the IAM service.
- Create User: Go to
Usersand clickAdd users. - User Details: Enter a username (e.g.,
fastify-sns-app-user) and selectProvide user access to the AWS Management Console(optional) if needed, but ensureProgrammatic access(Access key - ID and secret access key) is selected. ClickNext. - Permissions: Choose
Attach policies directly. Search for and select theAmazonSNSFullAccesspolicy (for simplicity in this guide) or create a custom policy granting onlysns:Publishpermissions to your specific topic ARN for better security. ClickNext. - Tags (Optional): Add any desired tags. Click
Next. - Review and Create: Review the details and click
Create user. - Save Credentials: Crucially, copy the Access key ID and Secret access key. You won't be able to see the secret key again.
1.5. Configure SNS Topic:
- Navigate to SNS: In the AWS Management Console, go to the Simple Notification Service (SNS).
- Create Topic: Go to
Topicsand clickCreate topic. - Type: Choose
Standard. FIFO topics have different considerations not covered here. - Name: Enter a name (e.g.,
whatsapp-outgoing-messages). - Leave Defaults: Keep other settings as default for now.
- Create Topic: Click
Create topic. - Copy ARN: Once created, copy the Topic ARN. It will look something like
arn:aws:sns:us-east-1:123456789012:whatsapp-outgoing-messages.
1.6. Set Up Environment Variables:
Create a .env file in your project root:
#.env
# AWS Credentials - DO NOT COMMIT THIS FILE TO GIT
AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY_ID_HERE
AWS_SECRET_ACCESS_KEY=YOUR_SECRET_ACCESS_KEY_HERE
AWS_DEFAULT_REGION=your-aws-region # e.g., us-east-1
# SNS Configuration
SNS_TOPIC_ARN=YOUR_SNS_TOPIC_ARN_HERE
# Server Configuration
PORT=3000
HOST=0.0.0.0
# Security (Example API Key)
API_KEY=your-super-secret-api-keyReplace the placeholder values with your actual credentials, region, and topic ARN. For the API_KEY, it is strongly recommended to generate a cryptographically secure random string rather than using a simple placeholder, even for development.
1.7. Configure Git Ignore:
Create a .gitignore file to prevent committing sensitive information and build artifacts:
#.gitignore
# Dependencies
/node_modules
# Environment variables
.env
# Log files
*.log
# OS generated files
.DS_Store
Thumbs.db2. Implementing core functionality
Now, let's set up the Fastify server and define the core logic for publishing messages.
2.1. Fastify Server Setup (src/server.js):
This file initializes Fastify, loads environment variables, registers routes, and starts the server.
// src/server.js
'use strict'
require('dotenv').config() // Load .env variables
const fastify = require('fastify')({
logger: {
level: process.env.LOG_LEVEL || 'info', // Default to 'info'
// Use pino-pretty for development logging readability
...(process.env.NODE_ENV !== 'production' && {
transport: {
target: 'pino-pretty',
options: {
translateTime: 'HH:MM:ss Z',
ignore: 'pid,hostname',
},
},
}),
},
})
const start = async () => {
try {
// Register routes
await fastify.register(require('./routes/whatsapp'), { prefix: '/api/v1' })
// Basic health check route
fastify.get('/ping', async (request, reply) => {
return { pong: 'it worked!' }
})
// Start listening
const port = parseInt(process.env.PORT || '3000', 10)
const host = process.env.HOST || '0.0.0.0'
await fastify.listen({ port, host })
fastify.log.info(`Server listening on ${fastify.server.address().port}`)
} catch (err) {
fastify.log.error(err)
process.exit(1)
}
}
start()
// Graceful shutdown
const signals = {
SIGHUP: 1,
SIGINT: 2,
SIGTERM: 15,
}
Object.keys(signals).forEach((signal) => {
process.on(signal, async () => {
fastify.log.info(`Received ${signal}, closing server...`)
await fastify.close()
fastify.log.info('Server closed.')
process.exit(128 + signals[signal])
})
})dotenv.config(): Loads variables from.envearly.fastify({ logger: ... }): Initializes Fastify with Pino logging. Conditionally usespino-prettyfor development readability (installpino-prettyas a dev dependency:npm install --save-dev pino-pretty). In production, it defaults to JSON logging.- Route Registration: Loads the WhatsApp routes under
/api/v1. - Health Check: Provides a simple
/pingendpoint. - Server Start: Listens on the configured host and port.
- Graceful Shutdown: Handles OS signals for clean termination.
3. Building the API layer
We'll create the API endpoint to receive WhatsApp send requests, validate them, and trigger the SNS publish action.
3.1. Define API Route (src/routes/whatsapp.js):
This file defines the /send endpoint for initiating WhatsApp messages.
// src/routes/whatsapp.js
'use strict'
const { SNSClient, PublishCommand } = require('@aws-sdk/client-sns') // Use SDK v3
// Initialize SNS Client once (better performance than per-request)
const snsClient = new SNSClient({ region: process.env.AWS_DEFAULT_REGION })
// Define the validation schema for the request body
const sendBodySchema = {
type: 'object',
required: ['to', 'message'],
properties: {
to: {
type: 'string',
description: 'Recipient phone number in E.164 format (e.g., +14155552671)',
// Basic E.164 pattern - ensure it starts with + and digits
pattern: '^\\+[1-9]\\d{1,14}$',
},
message: {
type: 'string',
description: 'The text content of the message',
minLength: 1,
maxLength: 1600, // WhatsApp message limit
},
// Optional: Add more fields to pass via SNS if needed
// e.g., templateName, languageCode, userId etc.
metadata: { type: 'object' },
},
}
// Define the response schema
const sendResponseSchema = {
'2xx': { // Covers 200, 202 etc.
type: 'object',
properties: {
messageId: { type: 'string', description: 'AWS SNS Message ID' },
status: { type: 'string', description: 'Indicates message queued via SNS' },
},
},
// Add schemas for error responses (400, 401, 500) if desired
}
async function whatsappRoutes(fastify, options) {
// --- Authentication Hook ---
// Simple API Key check - Replace with a more robust method in production
fastify.addHook('onRequest', async (request, reply) => {
const apiKey = request.headers['x-api-key']
if (!apiKey || apiKey !== process.env.API_KEY) {
fastify.log.warn('Unauthorized attempt to access API')
// Use `return reply.code(...).send(...)` to stop processing and send response
return reply.code(401).send({ error: 'Unauthorized' })
}
// If execution reaches here, the hook passed
})
// --- Send Message Route ---
fastify.post(
'/send',
{
schema: {
description: 'Queues a WhatsApp message for sending via AWS SNS.',
tags: ['whatsapp'],
summary: 'Send WhatsApp message',
body: sendBodySchema,
response: sendResponseSchema,
headers: { // Document required headers
type: 'object',
properties: {
'x-api-key': { type: 'string' }
},
required: ['x-api-key']
}
},
},
async (request, reply) => {
const { to, message, metadata } = request.body
const topicArn = process.env.SNS_TOPIC_ARN
if (!topicArn) {
request.log.error('SNS_TOPIC_ARN environment variable is not set.')
return reply.code(500).send({ error: 'Internal server configuration error.' })
}
// Construct the message payload for SNS
// This payload will be received by the Lambda function
const snsPayload = {
// Standardize the payload structure
recipientPhoneNumber: to,
messageBody: message,
// Pass through any additional metadata
...(metadata && { metadata }),
}
// Prepare SNS publish command using AWS SDK v3
const command = new PublishCommand({
TopicArn: topicArn,
Message: JSON.stringify(snsPayload), // SNS message must be a string
// Optional: Add MessageAttributes for filtering/routing in SNS subscriptions
// MessageAttributes: {
// messageType: { DataType: 'String', StringValue: 'whatsapp' },
// },
})
try {
request.log.info(`Publishing message to SNS topic ${topicArn} for recipient ${to}`)
const publishResult = await snsClient.send(command)
request.log.info(
`Successfully published message to SNS. Message ID: ${publishResult.MessageId}`
)
// Accepted: The request is valid and queued via SNS.
// Downstream systems handle actual delivery.
reply.code(202).send({
messageId: publishResult.MessageId,
status: 'Message queued successfully via SNS.',
})
} catch (error) {
request.log.error(
{ err: error }, // Log the full error object
`Failed to publish message to SNS topic ${topicArn}`
)
// Determine if it's a client error (e.g., throttling) or server error
const statusCode = error.$metadata?.httpStatusCode || 500
reply
.code(statusCode >= 500 ? 500 : 503) // Use 503 for transient AWS issues
.send({
error: 'Failed to queue message via SNS.',
details: error.message, // Include details cautiously in prod
})
}
}
)
}
module.exports = whatsappRoutes- Schema Validation: Uses Fastify's built-in JSON schema validation (
sendBodySchema) to ensureto(in E.164 format) andmessageare present and valid before processing. - Authentication Hook: Implements a simple API key check using
fastify.addHook. Replace this with a proper authentication mechanism (e.g., JWT, OAuth) for production. Note the use ofreturn reply...to correctly stop processing on failure. - Route Definition: Defines a
POST /api/v1/sendendpoint. - SNS Client: The
SNSClientis initialized once when the module loads for better performance. - SNS Payload Construction: Creates a structured JSON payload (
snsPayload). - SNS Publishing: Uses the AWS SDK v3 (
@aws-sdk/client-sns) directly. Creates aPublishCommandand sends it using the pre-initializedsnsClient. - Response Handling: Returns
202 Acceptedon success, or appropriate error codes (500/503) on failure. - Environment Variable Check: Ensures
SNS_TOPIC_ARNis configured.
3.2. Testing the Endpoint:
Once the server is running (npm start), you can test the endpoint using curl or Postman.
Running the Server:
# For production mode (JSON logs)
npm start
# For development with pretty logs and auto-reload:
# npm install --save-dev nodemon pino-pretty
# npx nodemon src/server.js | npx pino-prettyExample cURL Request:
Replace your-super-secret-api-key and the phone number/message.
curl -X POST http://localhost:3000/api/v1/send \
-H "Content-Type: application/json" \
-H "x-api-key: your-super-secret-api-key" \
-d '{
"to": "+14155552671",
"message": "Hello from Fastify via SNS!"
}'Example Success Response (202 Accepted):
{
"messageId": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
"status": "Message queued successfully via SNS."
}Example Error Response (400 Bad Request - Invalid Phone):
{
"statusCode": 400,
"error": "Bad Request",
"message": "body/to must match pattern \"^\\\\+[1-9]\\\\d{1,14}$\""
}Example Error Response (401 Unauthorized):
{
"error": "Unauthorized"
}Example Error Response (500 Internal Server Error - SNS Publish Failed):
{
"error": "Failed to queue message via SNS.",
"details": "The security token included in the request is invalid." // Example detail
}4. Integrating with AWS SNS
This section focuses on the specifics of the SNS integration.
4.1. Configuration Recap:
- AWS Credentials: Provided via environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_DEFAULT_REGION) loaded bydotenvand automatically used by the AWS SDK. - SNS Topic ARN: Provided via the
SNS_TOPIC_ARNenvironment variable, used in thePublishCommand.
4.2. Secure Handling of Secrets:
.envFile: Store sensitive keys (AWS_SECRET_ACCESS_KEY,API_KEY) only in the.envfile..gitignore: Ensure.envis listed in your.gitignorefile.- Production Environments: In production, avoid using
.envfiles. Inject secrets directly as environment variables through your deployment mechanism (e.g., ECS Task Definitions, Lambda Environment Variables, Kubernetes Secrets). Use tools like AWS Secrets Manager or HashiCorp Vault.
4.3. Fallback Mechanisms & Retries:
- SNS Publish Retries (Client-Side): The AWS SDK v3 has built-in retry logic for transient network errors or throttled requests when communicating with the SNS API endpoint. This is generally sufficient for the Fastify app's interaction with SNS.
- SNS Delivery Retries (Server-Side): Once a message is successfully published to SNS, SNS itself handles retries for delivering the message to its subscribers (like your Lambda function). Configure these retry policies and Dead-Letter Queues (DLQs) on the SNS subscription in the AWS console. This ensures resilience if the downstream consumer fails.
4.4. AWS Console Setup Summary:
To configure the necessary AWS resources:
- IAM: Create an IAM user with programmatic access. Grant it permissions to publish to your specific SNS topic ARN (e.g., using a custom policy with the
sns:Publishaction or theAmazonSNSFullAccessmanaged policy for simplicity). Securely store the generated Access Key ID and Secret Access Key. - SNS: Create a Standard SNS topic. Note its ARN (Amazon Resource Name).
These credentials and the Topic ARN are then used in your .env file.
4.5. Environment Variables Summary:
| Variable | Purpose | Format | How to Obtain |
|---|---|---|---|
AWS_ACCESS_KEY_ID | AWS credential for programmatic access. | String (e.g., AKIAIOSFODNN7EXAMPLE) | AWS IAM console after creating a user with programmatic access. |
AWS_SECRET_ACCESS_KEY | AWS credential secret key. Treat like a password. | String (e.g., wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY) | AWS IAM console after creating a user with programmatic access (only shown once). |
AWS_DEFAULT_REGION | The AWS region where your SNS topic resides. | String (e.g., us-east-1, eu-west-2) | Choose the AWS region for your SNS topic. |
SNS_TOPIC_ARN | The unique Amazon Resource Name identifying your SNS topic. | String (e.g., arn:aws:sns:us-east-1:123...:my-topic) | AWS SNS console after creating the topic. |
PORT | The network port the Fastify server will listen on. | Number (e.g., 3000) | Choose an available port. |
HOST | The network interface the server binds to (0.0.0.0 for all). | String (e.g., 0.0.0.0, 127.0.0.1) | 0.0.0.0 is typical for containers/servers. |
API_KEY | A secret key for basic API authentication (example purposes). | String (e.g., your-super-secret-api-key) | Generate a secure random string. |
LOG_LEVEL | Controls the verbosity of application logs (optional, defaults info). | String (fatal, error, warn, info, debug, trace) | Set based on environment (e.g., debug in dev, info in prod). |
NODE_ENV | Sets the environment mode (e.g., development, production). | String (development, production) | Controls features like pretty logging. Set to production in deployments. |
5. Error Handling, Logging, and Retry Mechanisms
Robust error handling and logging are essential for production systems.
5.1. Error Handling Strategy:
- Validation Errors: Handled automatically by Fastify's schema validation, returning 400 Bad Request.
- Authentication Errors: Handled by the
onRequesthook, returning 401 Unauthorized. - SNS Publish Errors: Caught in the
try...catchblock within the/sendroute handler.- Log the detailed error server-side (including the error object from the SDK).
- Return appropriate HTTP status codes (500 for general failures, 503 Service Unavailable for potentially transient AWS issues).
- Avoid leaking sensitive internal error details to the client.
- Configuration Errors: Check for essential environment variables (like
SNS_TOPIC_ARN) before use, returning a 500 Internal Server Error if missing. - Global Error Handler (Optional):
fastify.setErrorHandler()can catch unhandled exceptions, but specific handling within routes is preferred.
5.2. Logging:
- Fastify Logger (Pino): Used by default.
request.logprovides request-specific logging with request IDs.fastify.logis for general application logging. - Log Levels: Control verbosity via
LOG_LEVEL. - Log Format: JSON format in production (default when
NODE_ENV=production) for log aggregation systems.pino-prettyfor development. - Key Information to Log:
- Incoming request basics (method, URL).
- Validation failures.
- SNS publish attempts (topic ARN, recipient identifier).
- SNS publish success (
MessageId). - SNS publish failures (error details, AWS request ID if available
error.$metadata?.requestId). - Configuration issues.
Example Logging in Route:
// Inside the /send route handler
request.log.info({ recipient: to }, `Processing send request`);
// ... later ...
request.log.info({ recipient: to, messageId: publishResult.MessageId }, `SNS publish successful`);
// ... on error ...
request.log.error({ recipient: to, error: error.message, awsRequestId: error.$metadata?.requestId, err: error }, `SNS publish failed`); // Log full error object too5.3. Retry Mechanisms (Recap):
- SNS Publish Call: Handled by the AWS SDK's default retry strategy.
- SNS Message Delivery: Handled by SNS subscription retry policies configured in AWS (outside the Fastify app). Configure an SNS DLQ on the subscription to capture messages that fail delivery repeatedly.
6. Database Schema and Data Layer (Optional)
Integrating a database allows tracking message status or implementing features like rate limiting.
- Potential Schema:
messagestable:id,recipient_phone,message_body,sns_message_id,status('queued', 'sent', 'failed'),status_timestamp,created_at,updated_at. - Data Access: Use an ORM (Prisma, Sequelize) or query builder (Knex.js) with your chosen database (PostgreSQL, MySQL, etc.).
- Migrations: Use tools like
prisma migrate devto manage schema changes. - Considerations: Index fields used in queries (e.g.,
sns_message_id,recipient_phone).
7. Adding Security Features
Enhance security beyond the basic API key:
- Input Validation: Provided by Fastify schemas.
- Authentication/Authorization: Replace the example API key with JWT (
@fastify/jwt) or OAuth 2.0. - Rate Limiting: Use
@fastify/rate-limitto prevent abuse.bashnpm install @fastify/rate-limitjavascript// In server.js or a plugin await fastify.register(require('@fastify/rate-limit'), { max: 100, // Example: Max 100 requests per IP per minute timeWindow: '1 minute' }) - Helmet: Use
@fastify/helmetfor security-related HTTP headers.bashnpm install @fastify/helmetjavascript// In server.js or a plugin await fastify.register(require('@fastify/helmet')) - HTTPS: Enforce HTTPS in production (typically via load balancer/API Gateway).
- Dependency Audits: Run
npm auditregularly.
8. Handling Special Cases
- Phone Number Formatting: The E.164 regex is basic. For more robust validation/parsing, consider
libphonenumber-js. - Message Content: Adhere to WhatsApp policies. Ensure proper UTF-8 handling (usually managed by SNS/WhatsApp API).
- Idempotency: To handle client retries, consider adding an optional
idempotencyKey(client-generated UUID) to requests. Cache recent keys (e.g., in Redis) to detect and reject duplicates. The downstream consumer might also need duplicate detection logic.
9. Implementing Performance Optimizations
- Fastify's Speed: Leverage Fastify's performance by writing non-blocking, efficient route handlers.
- AWS SDK Client: The example initializes the
SNSClientonce per module load, which is efficient. - Logging: Pino is asynchronous. Avoid excessive logging or overly verbose levels in production.
- Payload Size: Keep SNS payloads reasonably small.
- Downstream Optimization: Performance often depends on downstream components (Lambda, WhatsApp API).
10. Adding Monitoring, Observability, and Analytics
- Health Checks: Enhance the
/pingendpoint or add a/healthcheck for dependencies. - Metrics:
- CloudWatch: Leverage built-in SNS metrics and Lambda metrics.
- Application Metrics: Create CloudWatch Metric Filters from structured logs (JSON) to track custom metrics (e.g., messages queued, errors).
- Error Tracking: Integrate services like Sentry or Datadog Error Tracking.
- Distributed Tracing: Use AWS X-Ray or OpenTelemetry for tracing requests across services.
- Dashboards: Visualize key metrics (request rate, error rate, latency, SNS/Lambda stats) in CloudWatch, Grafana, etc.
11. Troubleshooting and Caveats
- CRITICAL CAVEAT: SNS Does Not Send Directly to WhatsApp: This Fastify app only publishes to SNS. A separate component (e.g., Lambda) must subscribe to the SNS topic and use the WhatsApp Business API to send the actual message.
- AWS Credentials Errors:
InvalidClientTokenId: CheckAWS_ACCESS_KEY_ID.SignatureDoesNotMatch: CheckAWS_SECRET_ACCESS_KEY.AccessDenied: Check IAM permissions (sns:Publish).
- SNS Errors:
TopicNotFound: IncorrectSNS_TOPIC_ARNor region mismatch.ThrottlingException: Publishing too fast. Rely on SDK retries; consider limit increases if sustained.
- Fastify Validation Errors: Check request body against the schema; error messages indicate the violation.
- WhatsApp API Limitations: The downstream process is subject to Meta's rules (templates, rate limits, costs, etc.).
12. Deployment and CI/CD
12.1. Deployment Options:
-
Container (Recommended): Package using Docker.
- Dockerfile Example:
dockerfile
# Dockerfile FROM node:18-alpine AS base WORKDIR /app COPY package*.json ./ # Install production dependencies only FROM base AS prod-deps RUN npm ci --omit=dev # Build stage (if you have one, e.g., TypeScript) # FROM base AS build # COPY . . # RUN npm run build # Final production stage FROM base ENV NODE_ENV=production WORKDIR /app COPY /app/node_modules ./node_modules COPY ./src ./src COPY package.json . # Copy essential files # Copy build artifacts if needed # COPY --from=build /app/dist ./dist EXPOSE 3000 # Load PORT and HOST from runtime environment variables CMD [""node"", ""src/server.js""] # Or your built output, e.g., dist/server.js - Deployment Platforms: AWS App Runner, AWS Fargate, EC2, Google Cloud Run, Azure Container Apps.
- Configuration: Inject environment variables securely (Task Definitions, Secrets Manager). Do not bake secrets into the image.
- Dockerfile Example:
-
Serverless (Fastify on Lambda): Use
@fastify/aws-lambdafor sporadic workloads. See Fastify Serverless Guide.
12.2. CI/CD Pipeline (Example using GitHub Actions):
Create .github/workflows/deploy.yml:
# .github/workflows/deploy.yml
name: Deploy Fastify SNS WhatsApp App
on:
push:
branches: [ main ] # Trigger on push to main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for configure-aws-credentials using OIDC
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
# Add linting/testing steps
# - name: Lint
# run: npm run lint
# - name: Test
# run: npm test
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/YourGitHubActionsRole # Replace with your IAM Role ARN
aws-region: us-east-1 # Replace with your AWS region
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push image to Amazon ECR
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: your-ecr-repo-name # Replace with your ECR repo name
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
# Add deployment steps (e.g., update ECS service, deploy to App Runner)
# - name: Deploy to AWS App Runner
# run: aws apprunner start-deployment --service-arn ${{ secrets.APP_RUNNER_SERVICE_ARN }}- AWS Credentials: Use OIDC (OpenID Connect) with an IAM Role for secure, keyless authentication from GitHub Actions (recommended over storing long-lived keys).
- Secrets: Store necessary secrets (like
APP_RUNNER_SERVICE_ARNif used) in GitHub Actions secrets. - ECR Repository: Create an ECR repository in AWS.
- Deployment Step: Adapt based on your platform.
13. Verification and Testing
13.1. Unit Tests:
Test components in isolation, mocking external services like SNS. Use tap, Jest, etc. proxyquire is useful for mocking dependencies.
- Install testing dependencies:
npm install --save-dev tap proxyquire - Add test script to
package.json:"test": "tap test/**/*.test.js"
Example Unit Test (test/routes/whatsapp.test.js):
// test/routes/whatsapp.test.js
'use strict'
const { test } = require('tap')
const Fastify = require('fastify')
const proxyquire = require('proxyquire') // To mock dependencies
// Mock the AWS SDK v3 client and command
const mockSNSClientInstance = {
send: async (command) => {
// Basic validation of command input for testing
if (!command.input.TopicArn || !command.input.Message) {
throw new Error('Missing TopicArn or Message')
}
if (command.input.TopicArn !== 'arn:aws:sns:us-east-1:123456789012:test-topic') {
throw new Error('Incorrect TopicArn')
}
// Simulate successful SNS publish
return { MessageId: 'mock-message-id-123', $metadata: { httpStatusCode: 200 } }
}
}
const MockSNSClient = function() { return mockSNSClientInstance }
// Use proxyquire to inject the mock SDK into the routes module
const whatsappRoutes = proxyquire('../../src/routes/whatsapp', {
'@aws-sdk/client-sns': {
SNSClient: MockSNSClient, // Replace real client with mock constructor
PublishCommand: function(input) { this.input = input } // Mock command constructor
}
})
// --- Test Suite ---
test('POST /api/v1/send - success', async (t) => {
const fastify = Fastify()
// Set up necessary environment variables for the test context
process.env.API_KEY = 'test-api-key'
process.env.SNS_TOPIC_ARN = 'arn:aws:sns:us-east-1:123456789012:test-topic'
process.env.AWS_DEFAULT_REGION = 'us-east-1' // Needed for SNSClient mock setup
fastify.register(whatsappRoutes, { prefix: '/api/v1' })
const response = await fastify.inject({
method: 'POST',
url: '/api/v1/send',
headers: {
'content-type': 'application/json',
'x-api-key': 'test-api-key'
},
payload: {
to: '+15551234567',
message: 'Test message'
}
})
t.equal(response.statusCode, 202, 'should return status code 202')
const body = JSON.parse(response.payload)
t.ok(body.messageId, 'should return a messageId')
t.equal(body.messageId, 'mock-message-id-123', 'should return the mocked messageId')
t.equal(body.status, 'Message queued successfully via SNS.', 'should return correct status message')
// Clean up env vars if necessary, though tap runs tests in separate processes
delete process.env.API_KEY
delete process.env.SNS_TOPIC_ARN
delete process.env.AWS_DEFAULT_REGION
})
test('POST /api/v1/send - unauthorized (missing API key)', async (t) => {
const fastify = Fastify()
process.env.API_KEY = 'test-api-key' // API key is set
process.env.SNS_TOPIC_ARN = 'arn:aws:sns:us-east-1:123456789012:test-topic'
process.env.AWS_DEFAULT_REGION = 'us-east-1'
fastify.register(whatsappRoutes, { prefix: '/api/v1' })
const response = await fastify.inject({
method: 'POST',
url: '/api/v1/send',
headers: {
'content-type': 'application/json'
// No x-api-key header
},
payload: {
to: '+15551234567',
message: 'Test message'
}
})
t.equal(response.statusCode, 401, 'should return status code 401')
const body = JSON.parse(response.payload)
t.same(body, { error: 'Unauthorized' }, 'should return unauthorized error')
delete process.env.API_KEY
delete process.env.SNS_TOPIC_ARN
delete process.env.AWS_DEFAULT_REGION
})
test('POST /api/v1/send - validation error (invalid phone)', async (t) => {
const fastify = Fastify()
process.env.API_KEY = 'test-api-key'
process.env.SNS_TOPIC_ARN = 'arn:aws:sns:us-east-1:123456789012:test-topic'
process.env.AWS_DEFAULT_REGION = 'us-east-1'
fastify.register(whatsappRoutes, { prefix: '/api/v1' })
const response = await fastify.inject({
method: 'POST',
url: '/api/v1/send',
headers: {
'content-type': 'application/json',
'x-api-key': 'test-api-key'
},
payload: {
to: 'invalid-phone-number', // Invalid format
message: 'Test message'
}
})
t.equal(response.statusCode, 400, 'should return status code 400')
const body = JSON.parse(response.payload)
t.equal(body.error, 'Bad Request', 'should return Bad Request error')
t.ok(body.message.includes('body/to must match pattern'), 'should indicate pattern mismatch for phone')
delete process.env.API_KEY
delete process.env.SNS_TOPIC_ARN
delete process.env.AWS_DEFAULT_REGION
})
// Add more tests for SNS publish errors, missing config, etc.Frequently Asked Questions
How to send WhatsApp messages using AWS SNS?
You can send WhatsApp messages using AWS SNS by publishing message details to an SNS topic, which then triggers a Lambda function to interact with the WhatsApp Business API. This decouples your main application from the WhatsApp API, enhancing scalability and resilience. Your application publishes messages to SNS, while a separate process handles the actual WhatsApp interaction.
What is the role of Fastify in sending WhatsApp messages?
Fastify acts as a high-performance web framework to create an API endpoint that receives WhatsApp message requests. It validates incoming requests for required parameters like phone number and message content before securely publishing to AWS SNS. This setup maintains a decoupled architecture.
Why use AWS SNS for sending WhatsApp messages?
AWS SNS provides a managed pub/sub service for decoupling and scaling message delivery. By using SNS, your core application doesn't need to directly interact with the WhatsApp Business API. This improves resilience and allows for easier management of message workflows.
When should I use this Fastify and AWS SNS approach for WhatsApp?
This architecture is ideal for applications requiring scalable and reliable WhatsApp messaging. Decoupling with SNS becomes particularly beneficial with higher message volumes and complex workflows where direct WhatsApp API integration within the core app would introduce overhead and complexity.
Can I send WhatsApp messages directly from Fastify?
This guide focuses on using SNS as an intermediary; the Fastify application doesn't interact with the WhatsApp API directly. A downstream service, typically an AWS Lambda function, subscribes to the SNS topic and handles direct communication with the WhatsApp Business API.
What AWS credentials are needed for the setup?
You'll need an IAM user with programmatic access, specifically the Access Key ID and Secret Access Key. These credentials are used by the AWS SDK to authorize your Fastify application to publish messages to the SNS topic. It's recommended to create a user with least privilege access - permissions only to publish to the relevant SNS topic.
What is the project structure for a Fastify WhatsApp SNS setup?
A typical structure includes 'src/routes/whatsapp.js' for API routes, 'src/server.js' for the Fastify server, '.env' for environment variables, and '.gitignore' to exclude sensitive data. The 'routes/whatsapp.js' file contains the core logic for handling incoming requests and publishing to SNS.
How to handle security with Fastify and AWS SNS for WhatsApp?
Secure your setup by using environment variables for sensitive data, implementing robust authentication beyond the example API key (e.g., JWT), using HTTPS, and leveraging tools like Helmet. Regularly audit dependencies for vulnerabilities using 'npm audit'.
How to validate WhatsApp message requests in Fastify?
Fastify's built-in schema validation is used to ensure 'to' (phone number in E.164 format) and 'message' fields are present and valid. This prevents invalid requests from reaching the SNS publish stage and helps maintain data integrity.
How to publish message details to AWS SNS from Fastify?
The AWS SDK v3 for JavaScript, specifically the '@aws-sdk/client-sns' module, is used. Initialize the SNSClient and use the PublishCommand with the SNS topic ARN and the message payload (JSON stringified) to publish messages to the SNS topic.
What should the WhatsApp message payload structure look like?
The message payload sent to SNS should be a JSON object containing at least 'recipientPhoneNumber' and 'messageBody'. Additional metadata can be included as needed for downstream processing by the Lambda function or other consumer.
How to set up logging for the Fastify WhatsApp SNS application?
The Fastify app uses Pino logging by default. For development, use pino-pretty for readable logs. In production, set NODE_ENV=production for JSON formatted logs suitable for log aggregation systems. Log levels are controlled with LOG_LEVEL.
What are common errors when integrating with AWS SNS?
Common errors include incorrect AWS credentials (InvalidClientTokenId, SignatureDoesNotMatch), insufficient IAM permissions (AccessDenied), invalid topic ARN (TopicNotFound), and throttling from SNS if publishing rates are too high (ThrottlingException).
How to deploy a Fastify WhatsApp SNS application?
Containerization with Docker is recommended. Build a Docker image with your application code and deploy to platforms like AWS App Runner, AWS Fargate, or other container services. For serverless deployments (for infrequent usage), consider Fastify on AWS Lambda with '@fastify/aws-lambda'.
How to troubleshoot WhatsApp messages not being delivered?
Remember, SNS only queues the messages. Verify the downstream service (e.g., AWS Lambda) is correctly subscribed to the SNS topic and functioning as expected. Check Lambda logs for errors related to WhatsApp Business API integration.