code examples
code examples
Building Scalable Marketing Campaigns with Fastify and AWS SNS
A guide on creating a Node.js API using Fastify and AWS SNS for managing and sending marketing messages (SMS/email) at scale.
Target Audience: Developers familiar with Node.js and REST APIs, looking to build a scalable system for sending marketing messages (primarily SMS and potentially email) via AWS SNS using the Fastify framework.
Prerequisites:
- Node.js (v18 or later recommended)
- npm or yarn
- An AWS account with permissions to manage SNS and IAM.
- Basic familiarity with command-line/terminal usage.
- Optional: Docker for containerization, Postman or
curlfor API testing.
Building Scalable Marketing Campaigns with Fastify and AWS SNS
This guide details how to build a robust API using Fastify to manage and send marketing messages through AWS Simple Notification Service (SNS). We'll cover everything from project setup and core SNS interactions to API design, security, deployment, and monitoring.
Project Goals:
- Create a Fastify API to manage SNS topics relevant to marketing campaigns.
- Implement functionality to subscribe users (via SMS or email) to these topics.
- Enable sending bulk messages to subscribed users via SNS topics.
- Enable sending direct SMS messages for targeted communications.
- Ensure the system is secure, scalable, handles errors gracefully, and is ready for production deployment.
Why these technologies?
- Fastify: A high-performance, low-overhead Node.js web framework ideal for building efficient APIs. Its plugin architecture makes integration straightforward.
- AWS SNS: A fully managed pub/sub messaging service that handles the complexities of message delivery across various protocols (SMS, email, push notifications, etc.) at scale. It's cost-effective and reliable.
fastify-aws-snsPlugin: Simplifies interaction with the AWS SNS API directly within the Fastify application context. (Note: Plugin functionality should be verified against its documentation, as wrappers can sometimes have limitations or lag behind the underlying SDK.)
System Architecture:
+-----------+ +-----------------+ +-----------+ +---------------------+
| Client | ----> | Fastify API | ---- | AWS SNS | ---- | User Endpoints |
| (Web/App) | | (Node.js/Docker)| | (Topics) | | (SMS, Email, etc.) |
+-----------+ +-----------------+ +-----------+ +---------------------+
| ^
| | (Optional: Store metadata)
v |
+-----------+
| Database |
| (Postgres)|
+-----------+Expected Outcome:
By the end of this guide, you will have a functional Fastify API capable of:
- Creating and listing SNS topics.
- Subscribing phone numbers and email addresses to topics.
- Handling the SNS subscription confirmation workflow.
- Publishing messages to topics.
- Sending direct SMS messages.
- Basic security, logging, and error handling implemented.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
1.1. Initialize Project
Open your terminal and create a new project directory:
mkdir fastify-sns-campaigns
cd fastify-sns-campaigns
npm init -y1.2. Install Dependencies
We need Fastify, the SNS plugin, a tool to load environment variables, and the core AWS SDK (which fastify-aws-sns uses under the hood, but explicitly installing ensures compatibility and allows direct SDK usage if needed).
npm install fastify fastify-aws-sns dotenv @aws-sdk/client-snsfastify: The core web framework.fastify-aws-sns: The plugin for easy SNS integration.dotenv: Loads environment variables from a.envfile intoprocess.env.@aws-sdk/client-sns: The official AWS SDK v3 for SNS (provides underlying functionality).
1.3. Project Structure
Create the following basic structure:
fastify-sns-campaigns/
├── node_modules/
├── routes/
│ └── index.js # Main route definitions
├── plugins/
│ └── sns.js # Plugin registration for SNS
├── .env # Environment variables (DO NOT COMMIT)
├── .gitignore
├── server.js # Main application entry point
└── package.json1.4. Configure .gitignore
Create a .gitignore file to prevent committing sensitive information and unnecessary files:
# .gitignore
node_modules
.env
npm-debug.log
*.log1.5. Environment Variables (.env)
Create a .env file in the project root. This file will hold your AWS credentials and other configuration. Never commit this file to version control.
# .env
# AWS Credentials - Obtain from IAM User (See Section 4)
AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY
AWS_REGION=us-east-1 # Or your preferred AWS region
# Default SNS Topic (Optional - can be overridden via API)
# AWS_TOPIC_NAME=my-marketing-topic
# API Configuration
API_PORT=3000
API_HOST=127.0.0.1
API_KEY=your-secret-api-key # Simple API key for auth (See Section 7 - Use robust auth in production!)- Why
.env? It keeps sensitive credentials and configuration separate from your code, making it easier to manage different environments (development, staging, production) and enhancing security.
1.6. Basic Server Setup (server.js)
This file initializes Fastify, loads environment variables, registers plugins and routes, and starts the server.
// server.js
'use strict';
// Load environment variables from .env file
require('dotenv').config();
// Import Fastify
const fastify = require('fastify')({
logger: true, // Enable Fastify's built-in logger
});
// Register custom plugins (SNS integration)
fastify.register(require('./plugins/sns'));
// Register API routes
fastify.register(require('./routes/index'), { prefix: '/api' }); // Prefix all routes with /api
// Health check route
fastify.get('/health', async (request, reply) => {
return { status: 'ok', timestamp: new Date().toISOString() };
});
// Run the server
const start = async () => {
try {
const port = process.env.API_PORT || 3000;
const host = process.env.NODE_ENV === 'production' ? '0.0.0.0' : process.env.API_HOST || '127.0.0.1';
await fastify.listen({ port: parseInt(port, 10), host });
fastify.log.info(`Server listening on ${fastify.server.address().port}`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();- Why
logger: true? Enables detailed logging of requests, responses, and errors, crucial for debugging. - Why
0.0.0.0in production? Necessary for containerized environments (like Docker) to accept connections from outside the container.
2. Implementing Core Functionality (SNS Plugin)
Now, let's integrate the fastify-aws-sns plugin.
2.1. Register the SNS Plugin (plugins/sns.js)
This file registers the fastify-aws-sns plugin, making SNS functions available on the Fastify instance (e.g., fastify.snsTopics, fastify.snsMessage). The plugin automatically picks up AWS credentials from environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION) if they follow the standard naming convention.
// plugins/sns.js
'use strict';
const fp = require('fastify-plugin');
const fastifyAwsSns = require('fastify-aws-sns');
async function snsPlugin(fastify, options) {
// The plugin automatically uses AWS credentials from environment variables
// (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION)
// or IAM roles if running on EC2/ECS/Lambda.
fastify.register(fastifyAwsSns);
fastify.log.info('AWS SNS Plugin registered');
}
module.exports = fp(snsPlugin, {
name: 'snsPlugin',
// Specify dependencies if needed, e.g., ['configPlugin']
});- Why
fastify-plugin? It prevents Fastify from creating separate encapsulated contexts for the plugin, ensuring that thefastify.sns*decorators are available globally across your application routes.
2.2. Understanding Plugin Methods
The fastify-aws-sns plugin exposes several methods categorized under namespaces attached to the fastify instance. These typically include:
fastify.snsTopics: For managing topics (create, list, delete, get/set attributes).fastify.snsMessage: For publishing messages to a topic.fastify.snsSubscriptions: For managing subscriptions (list, subscribe different protocols like email/SMS/HTTP, confirm, unsubscribe).fastify.snsSMS: For SMS-specific actions (publish direct SMS, check opt-out status, manage SMS attributes).
Important: The exact methods and their behavior depend on the plugin version. Always consult the fastify-aws-sns documentation or source code to confirm available functionality and parameters. If the plugin lacks a specific feature (e.g., advanced pagination, batching), you may need to use the AWS SDK directly (see Section 9.1 - Note: Section 9.1 is mentioned but not provided in the original text).
We will use these methods within our API routes in the next section, assuming they function as described.
3. Building the API Layer
We'll define RESTful endpoints to interact with SNS functionalities.
3.1. Basic Authentication Hook
For simplicity, we'll use a basic API key check. In production, use a more robust method like JWT or OAuth (see Section 7 - Note: Section 7 is mentioned but not provided in the original text).
Add this hook at the beginning of your main routes file (routes/index.js).
// routes/index.js (Start of file)
'use strict';
const API_KEY = process.env.API_KEY;
// Basic Authentication Hook
async function authenticate(request, reply) {
const apiKey = request.headers['x-api-key'];
if (!API_KEY || !apiKey || apiKey !== API_KEY) {
reply.code(401).send({ error: 'Unauthorized', message: 'Valid API key required' });
return; // Stop execution
}
}3.2. Route Definitions (routes/index.js)
Define the routes within an async function exported by the module.
// routes/index.js (Continued)
const { SNSClient, ListTopicsCommand, ListSubscriptionsByTopicCommand } = require(""@aws-sdk/client-sns""); // For direct SDK calls if needed
async function routes(fastify, options) {
// Apply the authentication hook to all routes defined in this plugin
fastify.addHook('preHandler', authenticate);
// Initialize SNS Client for direct SDK calls if needed
// Credentials and region are picked up from environment by default
const snsClient = new SNSClient({});
fastify.log.info('Registering API routes...');
// --- Topic Management ---
// POST /api/topics - Create a new SNS topic
fastify.post('/topics', {
schema: {
body: {
type: 'object',
required: ['topicName'],
properties: {
topicName: { type: 'string', minLength: 1, maxLength: 256, pattern: '^[a-zA-Z0-9_-]+$' },
isFifo: { type: 'boolean', default: false }, // Optional: For FIFO topics
tags: { type: 'object' } // Optional: { key: value, ... }
}
},
response: {
201: {
type: 'object',
properties: {
TopicArn: { type: 'string' }
}
}
}
}
}, async (request, reply) => {
const { topicName, isFifo, tags } = request.body;
const attributes = {};
if (isFifo) {
attributes.FifoTopic = 'true';
// FIFO topics require .fifo suffix
if (!topicName.endsWith('.fifo')) {
reply.code(400).send({ error: 'Bad Request', message: 'FIFO topic names must end with .fifo' });
return;
}
// ContentBasedDeduplication is often useful for FIFO
attributes.ContentBasedDeduplication = 'true';
}
const params = {
topic: topicName, // Plugin uses 'topic' for name
attributes: attributes,
tags: tags ? Object.entries(tags).map(([Key, Value]) => ({ Key, Value })) : undefined
};
try {
fastify.log.info(`Creating SNS topic: ${topicName}`);
// Assuming fastify.snsTopics.create exists and works as expected
const result = await fastify.snsTopics.create(params);
reply.code(201).send({ TopicArn: result.TopicArn });
} catch (error) {
fastify.log.error({ error, params }, 'Error creating SNS topic');
reply.code(500).send({ error: 'Failed to create topic', message: error.message });
}
});
// GET /api/topics - List existing SNS topics
fastify.get('/topics', {
schema: {
query: { // Add query schema for pagination token
type: 'object',
properties: {
nextToken: { type: 'string' }
}
},
response: {
200: {
type: 'object',
properties: {
Topics: {
type: 'array',
items: {
type: 'object',
properties: {
TopicArn: { type: 'string' }
}
}
},
NextToken: { type: ['string', 'null'] } // For pagination
}
}
}
}
}, async (request, reply) => {
const { nextToken } = request.query;
try {
fastify.log.info(`Listing SNS topics (nextToken: ${nextToken})`);
// Use direct SDK for reliable pagination
const command = new ListTopicsCommand({ NextToken: nextToken });
const result = await snsClient.send(command);
reply.send({
Topics: result.Topics || [],
NextToken: result.NextToken || null
});
// --- Alternative using plugin (if pagination is not needed or verified to work): ---
// const result = await fastify.snsTopics.list({}); // Check plugin docs for pagination support
// reply.send(result);
} catch (error) {
fastify.log.error({ error }, 'Error listing SNS topics');
reply.code(500).send({ error: 'Failed to list topics', message: error.message });
}
});
// --- Subscription Management ---
// POST /api/topics/:topicArn/subscriptions - Subscribe an endpoint
fastify.post('/topics/:topicArn/subscriptions', {
schema: {
params: {
type: 'object',
required: ['topicArn'],
properties: { topicArn: { type: 'string', pattern: '^arn:aws:sns:.*:.*:.*$' } }
},
body: {
type: 'object',
required: ['protocol', 'endpoint'],
properties: {
protocol: { type: 'string', enum: ['sms', 'email', 'email-json'] },
endpoint: { type: 'string' } // Phone number (E.164) or email address
}
},
response: {
// SNS subscription requires confirmation (except sometimes SMS depending on region/settings)
202: {
type: 'object',
properties: {
message: { type: 'string' },
SubscriptionArn: { type: 'string' } // Often 'pending confirmation'
}
}
}
}
}, async (request, reply) => {
const { topicArn } = request.params;
const { protocol, endpoint } = request.body;
let subscribePromise;
// Plugin parameter names might differ slightly from SDK - check plugin docs
const params = { topicArn };
try {
fastify.log.info(`Subscribing ${endpoint} (${protocol}) to topic ${topicArn}`);
switch (protocol) {
case 'sms':
// Validate E.164 format (+ followed by country code and number)
if (!/^\+[1-9]\d{1,14}$/.test(endpoint)) {
reply.code(400).send({ error: 'Bad Request', message: 'Invalid phone number format. Use E.164 (e.g., +12125551234).' });
return;
}
params.phoneNumber = endpoint; // Assuming plugin uses 'phoneNumber'
// Verify method name and parameters with plugin documentation
subscribePromise = fastify.snsSubscriptions.setBySMS(params);
break;
case 'email':
params.email = endpoint; // Assuming plugin uses 'email'
// Verify method name and parameters with plugin documentation
subscribePromise = fastify.snsSubscriptions.setByEMail(params);
break;
case 'email-json':
params.email = endpoint; // Assuming plugin uses 'email'
// Verify method name and parameters with plugin documentation
subscribePromise = fastify.snsSubscriptions.setByEMailJSON(params);
break;
default:
reply.code(400).send({ error: 'Bad Request', message: `Unsupported protocol: ${protocol}` });
return;
}
const result = await subscribePromise;
// Note: Email subscriptions return a SubscriptionArn of 'pending confirmation'
// and require confirmation via a link sent to the email.
// SMS might auto-confirm in some regions/setups or also pend.
reply.code(202).send({
message: `Subscription request sent for ${endpoint}. Confirmation might be required.`,
SubscriptionArn: result.SubscriptionArn || 'pending confirmation'
});
} catch (error) {
fastify.log.error({ error, params }, 'Error subscribing endpoint');
reply.code(500).send({ error: 'Failed to subscribe', message: error.message });
}
});
// GET /api/topics/:topicArn/subscriptions - List subscriptions for a topic
fastify.get('/topics/:topicArn/subscriptions', {
schema: {
params: {
type: 'object',
required: ['topicArn'],
properties: { topicArn: { type: 'string', pattern: '^arn:aws:sns:.*:.*:.*$' } }
},
query: { // Add query schema for pagination token
type: 'object',
properties: {
nextToken: { type: 'string' }
}
},
response: {
200: {
type: 'object',
properties: {
Subscriptions: {
type: 'array',
items: {
type: 'object',
properties: {
SubscriptionArn: { type: 'string' },
Owner: { type: 'string' },
Protocol: { type: 'string' },
Endpoint: { type: 'string' },
TopicArn: { type: 'string' }
}
}
},
NextToken: { type: ['string', 'null'] }
}
}
}
}
}, async (request, reply) => {
const { topicArn } = request.params;
const { nextToken } = request.query;
try {
fastify.log.info(`Listing subscriptions for topic ${topicArn} (nextToken: ${nextToken})`);
// Use direct SDK for reliable pagination
const command = new ListSubscriptionsByTopicCommand({ TopicArn: topicArn, NextToken: nextToken });
const result = await snsClient.send(command);
reply.send({
Subscriptions: result.Subscriptions || [],
NextToken: result.NextToken || null
});
// --- Alternative using plugin (if pagination is not needed or verified to work): ---
// const result = await fastify.snsSubscriptions.list({ topicArn }); // Check plugin docs for pagination support
// reply.send(result);
} catch (error) {
fastify.log.error({ error, topicArn }, 'Error listing subscriptions');
reply.code(500).send({ error: 'Failed to list subscriptions', message: error.message });
}
});
// --- Message Publishing ---
// POST /api/topics/:topicArn/publish - Publish a message to a topic
fastify.post('/topics/:topicArn/publish', {
schema: {
params: {
type: 'object',
required: ['topicArn'],
properties: { topicArn: { type: 'string', pattern: '^arn:aws:sns:.*:.*:.*$' } }
},
body: {
type: 'object',
required: ['message'],
properties: {
message: { type: 'string', minLength: 1 },
subject: { type: 'string', maxLength: 100 }, // Primarily for email
messageAttributes: { type: 'object' } // Optional: { key: { DataType: 'String', StringValue: 'value' }, ...}
}
},
response: {
200: {
type: 'object',
properties: {
MessageId: { type: 'string' }
}
}
}
}
}, async (request, reply) => {
const { topicArn } = request.params;
const { message, subject, messageAttributes } = request.body;
// Plugin parameter names might differ slightly from SDK - check plugin docs
const params = {
topicArn,
message,
subject, // Subject is ignored by SMS
messageAttributes
};
try {
fastify.log.info(`Publishing message to topic ${topicArn}`);
// Assuming fastify.snsMessage.publish exists and works as expected
const result = await fastify.snsMessage.publish(params);
reply.send({ MessageId: result.MessageId });
} catch (error) {
fastify.log.error({ error, params }, 'Error publishing message to topic');
reply.code(500).send({ error: 'Failed to publish message', message: error.message });
}
});
// POST /api/sms/publish - Send a direct SMS message
fastify.post('/sms/publish', {
schema: {
body: {
type: 'object',
required: ['phoneNumber', 'message'],
properties: {
phoneNumber: { type: 'string', pattern: '^\+[1-9]\d{1,14}$' }, // E.164 format
message: { type: 'string', minLength: 1, maxLength: 1600 }, // Check SNS limits
senderId: { type: 'string', maxLength: 11 }, // Optional: Custom Sender ID (if supported)
messageType: { type: 'string', enum: ['Promotional', 'Transactional'], default: 'Promotional' }
}
},
response: {
200: {
type: 'object',
properties: {
MessageId: { type: 'string' }
}
}
}
}
}, async (request, reply) => {
const { phoneNumber, message, senderId, messageType } = request.body;
// Plugin parameter names might differ slightly from SDK - check plugin docs
const params = {
phoneNumber,
message,
messageAttributes: {} // Note: Verify if plugin handles attributes directly in publish or requires separate setAttributes call
};
if (senderId) {
params.messageAttributes['AWS.SNS.SMS.SenderID'] = {
DataType: 'String',
StringValue: senderId
};
}
params.messageAttributes['AWS.SNS.SMS.SMSType'] = {
DataType: 'String',
StringValue: messageType
};
try {
fastify.log.info(`Sending direct SMS to ${phoneNumber}`);
// Assuming fastify.snsSMS.publish exists and handles messageAttributes correctly.
// If not, you might need to call a separate setAttributes method via plugin or SDK first.
const result = await fastify.snsSMS.publish(params);
reply.send({ MessageId: result.MessageId });
} catch (error) {
fastify.log.error({ error, params }, 'Error sending direct SMS');
reply.code(500).send({ error: 'Failed to send SMS', message: error.message });
}
});
fastify.log.info('API routes registered.');
}
module.exports = routes;- Why Schema Validation? Ensures incoming requests have the correct structure and data types before your handler logic runs, preventing errors and improving security. Fastify handles this efficiently.
- Why
async/await? Simplifies handling asynchronous operations like calling the AWS API. - Why log errors? Essential for debugging production issues. Logging includes the error object and relevant parameters.
3.3. API Testing Examples (curl)
Replace placeholders like YOUR_API_KEY, YOUR_TOPIC_ARN, +12223334444 with actual values. Use single quotes around JSON data for curl on most shells.
-
Create Topic:
bashcurl -X POST http://localhost:3000/api/topics \ -H ""Content-Type: application/json"" \ -H ""x-api-key: YOUR_API_KEY"" \ -d '{ ""topicName"": ""my-new-campaign"" }' # Expected: 201 Created with { ""TopicArn"": ""arn:aws:sns:..."" } -
List Topics:
bashcurl http://localhost:3000/api/topics -H ""x-api-key: YOUR_API_KEY"" # Expected: 200 OK with { ""Topics"": [...] } -
Subscribe SMS:
bashcurl -X POST http://localhost:3000/api/topics/YOUR_TOPIC_ARN/subscriptions \ -H ""Content-Type: application/json"" \ -H ""x-api-key: YOUR_API_KEY"" \ -d '{ ""protocol"": ""sms"", ""endpoint"": ""+12223334444"" }' # Expected: 202 Accepted with { ""message"": ""..."", ""SubscriptionArn"": ""..."" } -
Subscribe Email:
bashcurl -X POST http://localhost:3000/api/topics/YOUR_TOPIC_ARN/subscriptions \ -H ""Content-Type: application/json"" \ -H ""x-api-key: YOUR_API_KEY"" \ -d '{ ""protocol"": ""email"", ""endpoint"": ""user@example.com"" }' # Expected: 202 Accepted with { ""message"": ""..."", ""SubscriptionArn"": ""pending confirmation"" } # Check user@example.com for a confirmation email. -
Publish to Topic:
bashcurl -X POST http://localhost:3000/api/topics/YOUR_TOPIC_ARN/publish \ -H ""Content-Type: application/json"" \ -H ""x-api-key: YOUR_API_KEY"" \ -d '{ ""message"": ""Hello subscribers!"", ""subject"": ""Campaign Update"" }' # Expected: 200 OK with { ""MessageId"": ""..."" } # Check subscribed endpoints (SMS/Email) -
Publish Direct SMS:
bashcurl -X POST http://localhost:3000/api/sms/publish \ -H ""Content-Type: application/json"" \ -H ""x-api-key: YOUR_API_KEY"" \ -d '{ ""phoneNumber"": ""+12223334444"", ""message"": ""Direct message test"" }' # Expected: 200 OK with { ""MessageId"": ""..."" } # Check the phone number
4. Integrating with AWS SNS (Credentials & IAM)
Securely connecting to AWS is paramount.
4.1. Create an IAM User
It's best practice to create a dedicated IAM user with least privilege access.
- Go to the AWS Management Console -> IAM.
- Navigate to Users -> Add users.
- Enter a User name (e.g.,
fastify-sns-api-user). - Select Provide user access to the AWS Management Console - Optional. If selected, set a password.
- Select Access key - Programmatic access. This is crucial for API interaction. Click Next.
- Set permissions: Choose Attach policies directly.
- Search for and select the
AmazonSNSFullAccesspolicy. Note: For stricter security, create a custom policy granting only the specific SNS actions needed (e.g.,sns:CreateTopic,sns:ListTopics,sns:Subscribe,sns:Publish,sns:ListSubscriptionsByTopic,sns:SetSMSAttributes,sns:CheckIfPhoneNumberIsOptedOut,sns:GetSMSSandboxAccountStatusetc.). Start withAmazonSNSFullAccessfor simplicity during development, but refine for production. - Click Next: Tags (Optional: Add tags for organization).
- Click Next: Review.
- Click Create user.
- IMPORTANT: On the final screen, copy the Access key ID and Secret access key. Store them securely (e.g., in your
.envfile locally, or a secrets manager in production). You won't be able to see the secret key again.
4.2. Configure Credentials
Store the copied credentials securely in your .env file for local development:
# .env
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE # Replace with your Access Key ID
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY # Replace with your Secret Access Key
AWS_REGION=us-east-1 # Ensure this matches the region you want to use SNS inAWS_ACCESS_KEY_ID: Identifies the IAM user.AWS_SECRET_ACCESS_KEY: The secret password for the user. Treat this like a password.AWS_REGION: The AWS region where your SNS topics and resources will reside (e.g.,us-east-1,eu-west-1). SNS is region-specific.
The fastify-aws-sns plugin (and the underlying AWS SDK) will automatically detect and use these environment variables. If deploying to AWS services like EC2, ECS, or Lambda, you should use IAM Roles instead, which is more secure as credentials aren't hardcoded or stored in files.
5. Error Handling, Logging, and Retry Mechanisms
Robust applications need to handle failures gracefully.
5.1. Consistent Error Handling
Fastify allows defining a global error handler. We'll augment the existing logging within routes.
Add this to server.js before start():
// server.js (before start())
fastify.setErrorHandler(function (error, request, reply) {
// Log the error
fastify.log.error({
request: {
method: request.method,
url: request.url,
headers: request.headers,
// Avoid logging sensitive data from body in production
body: process.env.NODE_ENV !== 'production' ? request.body : undefined,
query: request.query,
params: request.params,
},
error: {
message: error.message,
stack: error.stack,
code: error.code, // AWS SDK errors often have codes
statusCode: error.statusCode,
},
}, 'Unhandled error occurred');
// Send generic error response in production
if (process.env.NODE_ENV === 'production' && (!error.statusCode || error.statusCode >= 500)) {
reply.status(500).send({ error: 'Internal Server Error', message: 'An unexpected error occurred' });
return;
}
// Send detailed error response in development or for client errors (4xx)
const statusCode = error.statusCode >= 400 ? error.statusCode : 500;
reply.status(statusCode).send({
error: error.name || 'Internal Server Error',
message: error.message || 'An unexpected error occurred',
code: error.code, // Include code for debugging
});
});- Why
setErrorHandler? Catches unhandled errors thrown within route handlers or plugins, ensuring a consistent error response format and logging. - Why log request details? Helps immensely in debugging by showing the exact request that caused the error (be careful with sensitive body data).
- AWS Error Codes: AWS SDK errors often include specific codes (e.g.,
InvalidParameterValue,AuthorizationError,ThrottlingException) which are useful for diagnostics.
5.2. Logging
Fastify's built-in Pino logger (logger: true) is efficient.
- Levels: By default, it logs
infoand above. You can configure the level:logger: { level: 'debug' }. - Formats: Logs are typically JSON, suitable for log aggregation systems (CloudWatch Logs, Datadog, Splunk).
- Context: We added specific logging within routes (
fastify.log.info,fastify.log.error) to show application-specific actions and errors.
5.3. Retry Mechanisms
-
SNS Delivery Retries: AWS SNS automatically handles retries for delivering messages to subscribed endpoints (e.g., if an email server is temporarily down or an SMS gateway is busy). This is configured within SNS itself (Delivery policy).
-
API Call Retries: What if the API call to AWS SNS fails due to network issues or transient AWS problems (like throttling)?
- Simple Approach: For critical operations like publishing, you could wrap the
fastify.sns*.publish()or direct SDK call in a simple retry loop with exponential backoff. - Library Approach: Use a library like
async-retryfor more sophisticated retry logic.
Example using
async-retry(installnpm i async-retry):javascript// Example within a route handler (e.g., publish) const retry = require('async-retry'); // ... inside POST /api/topics/:topicArn/publish handler ... try { const result = await retry(async (bail, attempt) => { fastify.log.info(`Publishing message to topic ${topicArn}, attempt ${attempt}`); try { // Use either the plugin or direct SDK call here return await fastify.snsMessage.publish(params); // OR: return await snsClient.send(new PublishCommand(sdkParams)); } catch (error) { // Don't retry on client errors (4xx) like invalid parameters // AWS SDK v3 errors might not have statusCode directly, check $metadata const statusCode = error.$metadata?.httpStatusCode || error.statusCode; if (statusCode >= 400 && statusCode < 500) { // Give up on client errors bail(new Error(`Non-retriable error (${statusCode}): ${error.message}`)); return; // Important: return after bail } // Throw other errors (like 5xx, network errors) to trigger retry throw error; } }, { retries: 3, // Number of retries factor: 2, // Exponential backoff factor minTimeout: 1000, // Initial delay ms onRetry: (error, attempt) => { fastify.log.warn(`Retrying publish operation (attempt ${attempt}) due to error: ${error.message}`); } }); reply.send({ MessageId: result.MessageId }); } catch (error) { // This catches errors after retries have failed or non-retriable errors fastify.log.error({ error, params }, 'Error publishing message to topic after retries'); // Use the status code from the bailed error if available const finalStatusCode = error.originalError?.$metadata?.httpStatusCode || error.originalError?.statusCode || 500; reply.code(finalStatusCode).send({ error: 'Failed to publish message', message: error.message }); } // ... rest of handler ... - Simple Approach: For critical operations like publishing, you could wrap the
Frequently Asked Questions
How to send SMS messages with Fastify?
You can send SMS messages using a Fastify API integrated with AWS SNS. This involves setting up routes within your Fastify application to handle requests, using the `fastify-aws-sns` plugin to interact with the SNS service, and configuring proper AWS credentials. The API can then send messages directly via SNS or publish them to topics for subscribers.
What is AWS SNS used for in marketing?
AWS SNS (Simple Notification Service) is used to manage the sending of marketing messages like SMS and email. It's a pub/sub messaging service that handles delivery across various protocols, making it a scalable and cost-effective solution for reaching users. The article details how to integrate SNS with a Fastify API for sending marketing campaigns.
Why use Fastify for building marketing APIs?
Fastify is a high-performance Node.js web framework ideal for building efficient APIs due to its low overhead and plugin architecture. The article demonstrates using Fastify with AWS SNS to create a scalable marketing message system, leveraging Fastify's speed and ease of integration.
When should I use the fastify-aws-sns plugin?
Use the `fastify-aws-sns` plugin when building a Fastify application that needs to interact with AWS SNS. This plugin simplifies interactions with the AWS SNS API within your Fastify application context. Be sure to check the plugin documentation for compatibility.
How to set up a Fastify project with AWS SNS?
First, initialize a Node.js project and install Fastify, `fastify-aws-sns`, `dotenv`, and `@aws-sdk/client-sns`. Create the recommended project structure with designated directories for routes and plugins. Configure AWS credentials and API settings in a `.env` file, and define the basic server setup in `server.js`.
What is the purpose of the .env file?
The `.env` file stores environment variables, including sensitive data like AWS credentials and API keys, separate from your codebase. This enhances security and makes managing different environments (development, staging, production) easier, since you shouldn't commit this file to version control.
How to create an AWS IAM user for Fastify SNS?
In the AWS Management Console, go to IAM, add a new user with programmatic access, and attach the `AmazonSNSFullAccess` policy (or a custom policy with least privilege for production). Save the generated Access Key ID and Secret Access Key securely, as these will be used in your `.env` file for authentication.
What AWS credentials are needed for fastify-aws-sns?
You'll need your AWS Access Key ID, Secret Access Key, and the AWS Region. These are stored in the `.env` file and automatically loaded by the `fastify-aws-sns` plugin and the AWS SDK. For production deployments on AWS services, using IAM roles is recommended over access keys.
How to handle errors in a Fastify SNS application?
Implement Fastify's `setErrorHandler` to catch unhandled errors, log detailed error information including request context and error codes, and send appropriate error responses. For transient AWS errors, use retry mechanisms with exponential backoff, potentially using a library like `async-retry`.
What are the benefits of schema validation in Fastify routes?
Schema validation ensures that incoming requests to your Fastify API conform to expected data structures and types, which improves security by preventing unexpected input and helps catch errors early in the request handling process. The provided code examples include route schemas for validation.
How to publish messages to SNS topics with Fastify?
Use the `/api/topics/:topicArn/publish` route with a POST request, providing the `topicArn` as a parameter and the message body in JSON format. Ensure your request headers include the API key for authentication. The message body can include the actual message content, an optional subject (mainly for emails), and custom message attributes.
How to subscribe users to SNS topics via Fastify?
Make a POST request to `/api/topics/:topicArn/subscriptions`, including the `topicArn`, protocol (sms, email, email-json), and the endpoint (phone number or email address). The phone numbers should be in E.164 format. Remember email subscriptions usually require confirmation via a link sent to the provided address.
What is the recommended Node.js version for Fastify?
The article recommends using Node.js version 18 or later when building Fastify applications for marketing campaigns with AWS SNS. This is to ensure compatibility with the latest features and best practices in Fastify and related libraries.
How to manage SNS topics in a Fastify application?
The provided code demonstrates creating topics with a POST request to `/api/topics` and listing topics with a GET request to the same endpoint. It also shows how to manage subscriptions by adding and listing users via specified routes, including handling confirmation workflows.
Why use IAM roles instead of access keys in production?
IAM roles are more secure than storing access keys in files like `.env` because they provide temporary credentials that are automatically rotated, reducing the risk of compromise. When deploying your Fastify application to AWS services like EC2, ECS, or Lambda, configure your instances to use IAM roles.