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 -y
1.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 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 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
Users
and 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 theAmazonSNSFullAccess
policy (for simplicity in this guide) or create a custom policy granting onlysns:Publish
permissions 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
Topics
and 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-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:
#.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.
// 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 usespino-pretty
for development readability (installpino-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.
// 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) andmessage
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 ofreturn 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 aPublishCommand
and sends it using the pre-initializedsnsClient
. - 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:
# 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.
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 bydotenv
and automatically used by the AWS SDK. - SNS Topic ARN: Provided via the
SNS_TOPIC_ARN
environment variable, used in thePublishCommand
.
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:
- 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 theAmazonSNSFullAccess
managed 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
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:
// 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.npm install @fastify/rate-limit
// 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.npm install @fastify/helmet
// 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
: CheckAWS_ACCESS_KEY_ID
.SignatureDoesNotMatch
: CheckAWS_SECRET_ACCESS_KEY
.AccessDenied
: Check IAM permissions (sns:Publish
).
- SNS Errors:
TopicNotFound
: IncorrectSNS_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 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-lambda
for 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_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
):
// 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.