code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

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:

mermaid
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:

bash
mkdir fastify-sns-whatsapp
cd fastify-sns-whatsapp
npm init -y

1.2. Install Dependencies:

We need Fastify, the AWS SDK v3 SNS client, and dotenv for managing environment variables.

bash
npm install fastify @aws-sdk/client-sns dotenv
  • fastify: The core web framework.
  • @aws-sdk/client-sns: AWS SDK v3 module for interacting with SNS.
  • dotenv: Loads environment variables from a .env file into process.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 file
    • package.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.

  1. Navigate to IAM: In the AWS Management Console, go to the IAM service.
  2. Create User: Go to Users and click Add users.
  3. User Details: Enter a username (e.g., fastify-sns-app-user) and select Provide user access to the AWS Management Console (optional) if needed, but ensure Programmatic access (Access key - ID and secret access key) is selected. Click Next.
  4. Permissions: Choose Attach policies directly. Search for and select the AmazonSNSFullAccess policy (for simplicity in this guide) or create a custom policy granting only sns:Publish permissions to your specific topic ARN for better security. Click Next.
  5. Tags (Optional): Add any desired tags. Click Next.
  6. Review and Create: Review the details and click Create user.
  7. 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:

  1. Navigate to SNS: In the AWS Management Console, go to the Simple Notification Service (SNS).
  2. Create Topic: Go to Topics and click Create topic.
  3. Type: Choose Standard. FIFO topics have different considerations not covered here.
  4. Name: Enter a name (e.g., whatsapp-outgoing-messages).
  5. Leave Defaults: Keep other settings as default for now.
  6. Create Topic: Click Create topic.
  7. 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:

plaintext
#.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-key

Replace 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:

plaintext
#.gitignore

# Dependencies
/node_modules

# Environment variables
.env

# Log files
*.log

# OS generated files
.DS_Store
Thumbs.db

2. 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.

javascript
// 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 .env early.
  • fastify({ logger: ... }): Initializes Fastify with Pino logging. Conditionally uses pino-pretty for development readability (install pino-pretty as 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 /ping endpoint.
  • 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.

javascript
// 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 ensure to (in E.164 format) and message are 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 of return reply... to correctly stop processing on failure.
  • Route Definition: Defines a POST /api/v1/send endpoint.
  • SNS Client: The SNSClient is 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 a PublishCommand and sends it using the pre-initialized snsClient.
  • Response Handling: Returns 202 Accepted on success, or appropriate error codes (500/503) on failure.
  • Environment Variable Check: Ensures SNS_TOPIC_ARN is 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:

bash
# 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-pretty

Example cURL Request:

Replace your-super-secret-api-key and the phone number/message.

bash
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):

json
{
  "messageId": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
  "status": "Message queued successfully via SNS."
}

Example Error Response (400 Bad Request - Invalid Phone):

json
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "body/to must match pattern \"^\\\\+[1-9]\\\\d{1,14}$\""
}

Example Error Response (401 Unauthorized):

json
{
  "error": "Unauthorized"
}

Example Error Response (500 Internal Server Error - SNS Publish Failed):

json
{
  "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 by dotenv and automatically used by the AWS SDK.
  • SNS Topic ARN: Provided via the SNS_TOPIC_ARN environment variable, used in the PublishCommand.

4.2. Secure Handling of Secrets:

  • .env File: Store sensitive keys (AWS_SECRET_ACCESS_KEY, API_KEY) only in the .env file.
  • .gitignore: Ensure .env is listed in your .gitignore file.
  • Production Environments: In production, avoid using .env files. 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:

  1. 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:Publish action or the AmazonSNSFullAccess managed policy for simplicity). Securely store the generated Access Key ID and Secret Access Key.
  2. 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:

VariablePurposeFormatHow to Obtain
AWS_ACCESS_KEY_IDAWS credential for programmatic access.String (e.g., AKIAIOSFODNN7EXAMPLE)AWS IAM console after creating a user with programmatic access.
AWS_SECRET_ACCESS_KEYAWS 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_REGIONThe 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_ARNThe 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.
PORTThe network port the Fastify server will listen on.Number (e.g., 3000)Choose an available port.
HOSTThe 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_KEYA secret key for basic API authentication (example purposes).String (e.g., your-super-secret-api-key)Generate a secure random string.
LOG_LEVELControls 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_ENVSets 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 onRequest hook, returning 401 Unauthorized.
  • SNS Publish Errors: Caught in the try...catch block within the /send route 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.log provides request-specific logging with request IDs. fastify.log is 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-pretty for 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:

javascript
// 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 too

5.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: messages table: 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 dev to 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-limit to prevent abuse.
    bash
    npm install @fastify/rate-limit
    javascript
    // 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/helmet for security-related HTTP headers.
    bash
    npm install @fastify/helmet
    javascript
    // 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 audit regularly.

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 SNSClient once 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 /ping endpoint or add a /health check 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: Check AWS_ACCESS_KEY_ID.
    • SignatureDoesNotMatch: Check AWS_SECRET_ACCESS_KEY.
    • AccessDenied: Check IAM permissions (sns:Publish).
  • SNS Errors:
    • TopicNotFound: Incorrect SNS_TOPIC_ARN or 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 --from=prod-deps /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.
  • Serverless (Fastify on Lambda): Use @fastify/aws-lambda for sporadic workloads. See Fastify Serverless Guide.

12.2. CI/CD Pipeline (Example using GitHub Actions):

Create .github/workflows/deploy.yml:

yaml
# .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_ARN if 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):

javascript
// 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.