This guide details how to build a robust and scalable API service using Fastify and the Infobip Node.js SDK to send bulk SMS messages. We'll cover everything from project setup and core integration to error handling, security, and deployment best practices.
This service solves the common need for applications to send programmatic, high-volume SMS notifications, alerts, or marketing messages reliably. We chose Fastify for its exceptional performance and developer-friendly plugin architecture, making it ideal for high-throughput API applications. Infobip provides a powerful and globally reachable communication platform with a well-documented API and SDK, simplifying the integration process for sending SMS messages at scale.
Project Overview and Goals
Goal: Create a Fastify API endpoint that accepts a list of phone numbers and a message text, then uses the Infobip API to send that message to all recipients efficiently.
Key Features:
- Accepts POST requests with a message and multiple recipient phone numbers.
- Uses the Infobip API via the official Node.js SDK for sending SMS.
- Securely manages Infobip API credentials.
- Includes basic request validation and error handling.
- Provides logging for monitoring and troubleshooting.
- Offers guidance on testing, deployment, and common pitfalls.
Technology Stack:
- Runtime: Node.js (v18 or later recommended)
- Framework: Fastify
- Messaging API: Infobip SMS API
- Infobip SDK:
@infobip-api/sdk
- Environment Variables:
dotenv
- Rate Limiting:
fastify-rate-limit
(optional, but recommended)
System Architecture:
(Note: The following Mermaid diagram requires specific platform support or JavaScript libraries to render. It will appear as a raw code block in standard Markdown.)
graph LR
Client[Client Application] -->|POST /broadcast/sms| API{Fastify API Service};
API -->|Send SMS Request| Infobip[Infobip SMS API];
Infobip -->|SMS Delivery| Recipients[End User Phones];
Infobip -->|API Response/Status| API;
API -->|Response (e.g., 202 Accepted)| Client;
Prerequisites:
- Node.js and npm (or yarn) installed.
- An active Infobip account with API access. Create a free trial account here.
- Your Infobip API Key and Base URL (found in your Infobip account dashboard).
- Basic familiarity with Node.js, REST APIs, and terminal commands.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory:
mkdir fastify-infobip-sms cd fastify-infobip-sms
-
Initialize Node.js Project:
npm init -y
This creates a
package.json
file. -
Install Dependencies:
npm install fastify @infobip-api/sdk dotenv
fastify
: The core web framework.@infobip-api/sdk
: The official Infobip Node.js SDK for interacting with their APIs.dotenv
: To load environment variables from a.env
file for secure credential management.
-
Install Development Dependencies (Optional but Recommended):
npm install --save-dev nodemon pino-pretty
nodemon
: Automatically restarts the server during development when files change.pino-pretty
: Makes Fastify's default JSON logs more human-readable during development.
-
Configure
package.json
Scripts: Open yourpackage.json
file and add/modify thescripts
section:// package.json { // ... other configurations ""scripts"": { ""start"": ""node src/server.js"", ""dev"": ""nodemon src/server.js | pino-pretty"" }, // ... other configurations ""type"": ""module"" // Add this line to enable ES Module syntax }
start
: Runs the application directly with Node.dev
: Runs the application usingnodemon
for auto-reloading and pipes logs throughpino-pretty
.""type"": ""module""
: We'll use modern ES Module syntax (import
/export
).
-
Create Project Structure:
mkdir src mkdir src/routes mkdir src/plugins touch src/server.js touch src/routes/broadcast.js touch src/plugins/infobip.js touch .env touch .gitignore
src/
: Contains our main application code.src/server.js
: The main entry point for our Fastify server.src/routes/
: Holds route definitions.src/plugins/
: Contains Fastify plugins (like our Infobip client setup)..env
: Stores environment variables (API keys, etc.). Never commit this file!.gitignore
: Specifies intentionally untracked files that Git should ignore.
-
Configure
.gitignore
: Add the following lines to your.gitignore
file to prevent sensitive information and unnecessary files from being committed:# .gitignore node_modules .env npm-debug.log *.log
-
Configure Environment Variables (
.env
): Open the.env
file and add your Infobip credentials. You can find these in your Infobip account dashboard under API Keys management. The Base URL is specific to your account.# .env INFOBIP_API_KEY=YOUR_INFOBIP_API_KEY INFOBIP_BASE_URL=YOUR_INFOBIP_BASE_URL SENDER_ID=InfoSMS # Or your registered sender ID/number PORT=3000 # Optional: Port for the server to listen on LOG_LEVEL=info # Optional: Set log level (debug, info, warn, error)
INFOBIP_API_KEY
: Your secret API key.INFOBIP_BASE_URL
: The specific API endpoint URL provided by Infobip for your account.SENDER_ID
: The phone number or alphanumeric ID that will appear as the sender of the SMS. This often needs to be registered/approved by Infobip depending on the destination country regulations.PORT
: The port your Fastify server will listen on. Defaults to 3000 if not set.LOG_LEVEL
: Controls the logging verbosity.
Why Environment Variables? Storing credentials directly in code is a major security risk. Environment variables allow you to configure the application differently for development, testing, and production without changing the code, and keep secrets out of version control.
dotenv
loads these variables intoprocess.env
during development (when called early inserver.js
). In production, these variables are typically set directly in the deployment environment.
2. Implementing Core Functionality (Infobip Plugin)
We'll create a Fastify plugin to initialize and encapsulate the Infobip SDK client, making it reusable across our application.
-
Create the Infobip Plugin: Open
src/plugins/infobip.js
and add the following code:// src/plugins/infobip.js import fp from 'fastify-plugin'; import { Infobip, AuthType } from '@infobip-api/sdk'; async function infobipPlugin(fastify, options) { // Environment variables should be loaded by dotenv in server.js const apiKey = process.env.INFOBIP_API_KEY; const baseUrl = process.env.INFOBIP_BASE_URL; if (!apiKey || !baseUrl) { throw new Error('Infobip API Key or Base URL missing in environment variables.'); } try { const infobipClient = new Infobip({ baseUrl: baseUrl, apiKey: apiKey, authType: AuthType.ApiKey, // Specify API Key authentication }); // Decorate Fastify instance with the client fastify.decorate('infobip', infobipClient); fastify.log.info('Infobip client initialized successfully.'); } catch (error) { fastify.log.error('Failed to initialize Infobip client:', error); throw new Error('Infobip client initialization failed.'); } } // Export the plugin using fastify-plugin to avoid encapsulation issues // and make the decorator available globally export default fp(infobipPlugin, { name: 'infobip-client' // Optional: name for the plugin });
- Environment Variables: Relies on
dotenv
being called inserver.js
to populateprocess.env
. - Error Handling: Checks if required environment variables are present.
new Infobip(...)
: Initializes the SDK client using the API key and base URL.AuthType.ApiKey
explicitly tells the SDK how to authenticate.fastify.decorate('infobip', ...)
: Attaches the initializedinfobipClient
to the Fastify instance (and request/reply objects) under the nameinfobip
. This makes it accessible in our route handlers viafastify.infobip
orrequest.infobip
.fp(infobipPlugin, ...)
: Wraps the plugin function usingfastify-plugin
. This ensures that decorators added by this plugin (like.infobip
) are available globally within the Fastify instance, not just within the scope of the plugin itself.
- Environment Variables: Relies on
3. Building the API Layer (Broadcast Route)
Now, let's create the API endpoint that will receive broadcast requests and use our Infobip plugin.
-
Create the Broadcast Route Handler: Open
src/routes/broadcast.js
and add the following:// src/routes/broadcast.js // Sender ID is read from environment variables (loaded in server.js) const senderId = process.env.SENDER_ID || 'InfoSMS'; // Fallback sender ID // Define schema for request body validation const broadcastSchema = { body: { type: 'object', required: ['to', 'text'], properties: { to: { type: 'array', minItems: 1, items: { type: 'string', // Basic pattern for E.164 format (adjust if needed) // Example: +14155552671 - This is a basic check, Infobip performs stricter validation // Double backslashes are needed to escape \ in a JS string for the regex engine pattern: ""^\\+?[1-9]\\d{1,14}$"" } }, text: { type: 'string', minLength: 1, maxLength: 1600 // Max SMS length (consider multi-part messages) }, // Optional: Add other Infobip parameters like notifyUrl, callbackData, etc. // notifyUrl: { type: 'string', format: 'uri' }, // callbackData: { type: 'string', maxLength: 4000 } }, additionalProperties: false }, // Optional: Define response schema response: { 202: { description: 'Request accepted for processing', type: 'object', properties: { message: { type: 'string' }, bulkId: { type: 'string' }, messages: { type: 'array', items: { type: 'object', properties: { to: { type: 'string' }, status: { type: 'object', properties: { name: { type: 'string'}, description: { type: 'string'} } }, // Simplified status messageId: { type: 'string' } } } } } } // Add schemas for 4xx/5xx errors if desired for OpenAPI spec } }; async function broadcastRoutes(fastify, options) { fastify.post('/broadcast/sms', { schema: broadcastSchema }, async (request, reply) => { const { to, text /*, notifyUrl, callbackData */ } = request.body; // Map the 'to' array to the format expected by Infobip SDK const destinations = to.map(phoneNumber => ({ to: phoneNumber })); const payload = { messages: [ { from: senderId, destinations: destinations, text: text, // Optional parameters: // notifyUrl: notifyUrl, // If you want delivery reports pushed // callbackData: callbackData // Custom data for tracking }, ], // Optional: Add bulkId if you want to manage it // bulkId: `my-campaign-${Date.now()}` }; try { fastify.log.info(`Sending bulk SMS to ${destinations.length} recipients.`); // Access the Infobip client decorated in the plugin // Use the generic `send` method which maps to /sms/2/text/advanced const response = await fastify.infobip.channels.sms.send(payload); fastify.log.info({ bulkId: response.data.bulkId, recipients: destinations.length }, 'Infobip bulk SMS request accepted.'); // Infobip usually accepts the request quickly (2xx) // Actual delivery status comes later (via webhook if configured, or polling) // Return 202 Accepted to indicate the request was received // You might want to return the bulkId and initial message statuses from the response reply.code(202).send({ message: 'SMS broadcast request accepted by Infobip.', bulkId: response.data.bulkId, messages: response.data.messages // Initial statuses }); } catch (error) { fastify.log.error({ err: error?.response?.data || error.message }, 'Error sending SMS via Infobip'); // Check if the error is from Infobip API (AxiosError structure) if (error.response && error.response.data) { // Forward Infobip's error details reply.code(error.response.status || 500).send({ message: 'Infobip API error.', details: error.response.data }); } else { // Generic server error reply.code(500).send({ message: 'Internal Server Error while sending SMS.' }); } } }); } export default broadcastRoutes;
- Schema Validation: We define a
broadcastSchema
using Fastify's built-in AJV support. This automatically validates incoming request bodies. Thepattern
uses double backslashes (\\
) as required within a JavaScript string literal to represent literal backslashes for the regex engine. - Payload Mapping: We transform the
to
array into thedestinations
array structure required by the Infobip SDK'ssend
method. senderId
: We use theSENDER_ID
from the environment variables (loaded inserver.js
).- API Call:
fastify.infobip.channels.sms.send(payload)
makes the API call. - Response Handling: Returns
202 Accepted
along with thebulkId
and initial message statuses from Infobip's synchronous response. - Error Handling: The
try...catch
block handles potential errors, logging details and attempting to forward specific Infobip error information.
- Schema Validation: We define a
4. Integrating with Third-Party Services (Infobip Setup)
This section focuses on configuring the Fastify application to use the Infobip service correctly, building upon the initial setup in Section 1.
-
Obtain Infobip Credentials:
- Log in to your Infobip Portal: https://portal.infobip.com/
- API Key: Navigate to the ""Developers"" or ""API Keys"" section. Create or retrieve your API key. Copy the key value securely.
- Base URL: Find your account-specific Base URL, usually on the same API Keys page or in API documentation (e.g.,
xxxxxx.api.infobip.com
). - Sender ID: Determine the appropriate Sender ID (alphanumeric like ""MyCompany"" or a virtual number like
+14155550100
) based on your target countries and regulations. Check the ""Numbers"" or ""Senders"" section in the portal. Registration/approval might be required.
-
Configure Environment Variables:
- As detailed in Step 1.8, ensure your
.env
file (for local development) or your production environment variables include the correctINFOBIP_API_KEY
,INFOBIP_BASE_URL
, andSENDER_ID
.
- As detailed in Step 1.8, ensure your
-
Secure API Keys:
- Local: The
.gitignore
file (Step 1.7) prevents committing the.env
file. - Production: Crucially, do not deploy the
.env
file. Set the environment variables (INFOBIP_API_KEY
,INFOBIP_BASE_URL
,SENDER_ID
,PORT
,LOG_LEVEL
, etc.) directly through your hosting platform's configuration interface (e.g., Heroku Config Vars, AWS Secrets Manager, Kubernetes Secrets). This keeps secrets out of your codebase and version control.
- Local: The
-
Plugin and Route Usage:
- The Infobip plugin (
src/plugins/infobip.js
) readsINFOBIP_API_KEY
andINFOBIP_BASE_URL
fromprocess.env
to initialize the SDK. - The broadcast route (
src/routes/broadcast.js
) readsSENDER_ID
fromprocess.env
.
- The Infobip plugin (
-
Fallback Mechanisms (Consideration):
- For critical systems, consider adding retry logic (Section 5) or integrating a secondary SMS provider as a fallback if Infobip experiences issues.
5. Implementing Error Handling, Logging, and Retry Mechanisms
Fastify uses Pino for efficient logging. We've added basic error handling; let's refine it.
-
Logging:
- Default Logging: Fastify logs requests/responses. Our
fastify.log.info/error
calls add context. - Development:
pino-pretty
(vianpm run dev
) enhances readability. - Production: Use the default JSON format for log aggregation (Datadog, Splunk, ELK). Control verbosity via the
LOG_LEVEL
environment variable (info
,warn
,error
). - Server Start:
# Production start npm start # Or using PM2 for clustering/management # npm install -g pm2 # pm2 start src/server.js --name fastify-infobip-sms -i max
- Default Logging: Fastify logs requests/responses. Our
-
Error Handling Strategy:
- Validation Errors: Handled by Fastify schema (400 Bad Request).
- Infobip API Errors: The
catch
block insrc/routes/broadcast.js
logs Infobip's detailed error (error.response.data
) and forwards the status code and details to the client. - Other Server Errors: Generic
500 Internal Server Error
. - Consistent Error Format: Aim for a standard JSON error response.
-
Retry Mechanisms (Advanced):
- Recommended: Use a background job queue (e.g., BullMQ with Redis, RabbitMQ) for robust retries, especially for bulk operations.
- API endpoint validates, enqueues job, returns
202 Accepted
. - Worker process picks up job, calls Infobip.
- Worker handles retries (for 5xx, network errors, rate limits) with exponential backoff.
- Worker marks job complete or failed (for success or 4xx errors).
- API endpoint validates, enqueues job, returns
- Simple In-Handler Retry (Use with Caution): A small retry loop within the handler for transient errors (limit attempts/delay). Less robust than a queue.
- Recommended: Use a background job queue (e.g., BullMQ with Redis, RabbitMQ) for robust retries, especially for bulk operations.
-
Testing Error Scenarios:
- Test with invalid inputs (numbers, missing fields).
- Simulate Infobip errors (invalid key, rate limits) by temporarily changing
.env
or mocking the SDK in tests (Section 13).
6. Creating a Database Schema and Data Layer (Optional Enhancement)
A database (PostgreSQL, MongoDB, etc.) with an ORM (Prisma, Sequelize) can add features like storing recipient lists, tracking broadcast jobs (bulkId
, status), message templates, and delivery statuses (via webhooks). This adds complexity. For this guide, we focus on direct API interaction.
7. Adding Security Features
Protecting your API and credentials.
-
Environment Variables: Keep secrets out of code/git. Use platform secrets management in production (Section 4).
-
Input Validation: Implemented via Fastify schema (Section 3). Ensures data conforms to expectations (e.g., E.164 pattern, length limits).
-
Rate Limiting: Protect against abuse and control costs.
- Install:
npm install fastify-rate-limit
- Register in
src/server.js
:
// src/server.js (inside buildServer) // import rateLimit from 'fastify-rate-limit'; // Add import // await fastify.register(rateLimit, { // max: 100, // Adjust based on needs // timeWindow: '1 minute' // // Optional: Use Redis for distributed limiting // });
- Adjust
max
andtimeWindow
appropriately.
- Install:
-
HTTPS: Enforce HTTPS in production (usually handled by load balancers or PaaS).
-
Authentication/Authorization: Protect the
/broadcast/sms
endpoint itself.- Implement API key checking (
Authorization: Bearer YOUR_KEY
) usingfastify-auth
or similar. - Use JWT for user-based authentication if applicable.
- Implement API key checking (
-
Helmet: Set security-related HTTP headers.
- Install:
npm install fastify-helmet
- Register:
await fastify.register(helmet);
insrc/server.js
.
- Install:
-
Dependency Updates: Regularly run
npm audit
and update dependencies (npm update
). -
Least Privilege (Infobip Key): Use Infobip API keys with the minimum required permissions if possible.
8. Handling Special Cases Relevant to the Domain (SMS)
SMS specifics to consider:
- Character Limits/Encoding: GSM-7 (160 chars/segment) vs. UCS-2 (70 chars/segment for non-GSM chars). Long messages use multiple segments (cost more). Inform users. Infobip handles segmentation.
- Phone Number Formatting: Use E.164 (
+14155552671
). Rely on Infobip's validation but handle their specific errors (EC_INVALID_DESTINATION_ADDRESS
). - Sender ID Restrictions: Alphanumeric IDs not allowed everywhere (e.g., US). Numeric IDs often better for deliverability. Use appropriate, registered IDs. Handle
EC_ILLEGAL_SENDER
errors. - Delivery Reports (DLRs): Delivery is asynchronous. Use Infobip webhooks (
notifyUrl
parameter in API call) or poll the DLR endpoint (GET /sms/1/reports
) to get final status (DELIVERED, FAILED, etc.). Webhooks are preferred for real-time updates. - Opt-outs/Compliance: Handle STOP/UNSUBSCRIBE requests (often managed by Infobip if configured, otherwise implement yourself) per regulations (TCPA, GDPR).
- Time Zones/Scheduling: Use Infobip's
sendAt
parameter to schedule messages appropriately for recipient time zones, especially for marketing.
9. Implementing Performance Optimizations
Making the service handle high load:
- Batching API Calls: Our use of the
/sms/2/text/advanced
endpoint with thedestinations
array is batching. Break very large lists (millions) into multiple API calls (e.g., 1000 recipients per call), managed ideally via a job queue. - Asynchronous Processing: Use a message queue (BullMQ, RabbitMQ) to decouple API requests from Infobip calls. API returns
202 Accepted
quickly; workers handle Infobip calls/retries/rate limiting. This is the most impactful optimization for high throughput. - Efficient Logging: Pino is fast. Avoid excessive logging in handlers. Use JSON format in production, adjust
LOG_LEVEL
. - Node.js Event Loop Monitoring: Use
clinic.js
to detect event loop blocks. - Infrastructure Scaling: Run multiple instances (PM2 cluster, Docker/Kubernetes replicas) behind a load balancer. Scale databases/Redis if used.
- Infobip API Limits: Be aware of your account's MPS (Messages Per Second) limit. Implement outbound rate limiting in workers if needed to avoid
429 Too Many Requests
.
10. Adding Monitoring, Observability, and Analytics
Understanding service health and performance:
- Health Checks: Add a
/health
endpoint checking essential services (Infobip client init, DB connection if used). Use for liveness/readiness probes. (See example insrc/server.js
Section 13). - Performance Metrics: Use
fastify-metrics
to expose Prometheus metrics (/metrics
) for request rates, latency, error rates, queue size, custom metrics (infobip_sms_sent_total
). Visualize in Grafana. - Error Tracking: Integrate Sentry, Datadog APM, etc., using Fastify plugins (
@sentry/node
, etc.) for real-time error reporting and alerting. - Distributed Tracing: Use OpenTelemetry (
@autotelic/fastify-opentelemetry
) in complex microservice setups. - Infobip Analytics: Use the Infobip Portal's reporting features (correlate with
bulkId
). - Dashboards & Alerting: Create dashboards (Grafana, Datadog) and set alerts (Prometheus Alertmanager, Datadog Monitors) for key metrics (error rates, latency, health check failures, Infobip error codes, queue backlog).
11. Troubleshooting and Caveats
Common issues:
- Error:
Infobip API Key or Base URL missing...
: Check.env
or production environment variables. - Infobip 401
AUTHENTICATION_ERROR
: Invalid API key. Verify key and ensure it's active. - Infobip 400
BAD_REQUEST
: Invalid payload. Check details:EC_INVALID_DESTINATION_ADDRESS
(bad number format),EC_INVALID_SENDER_OR_FROM_FIELD
(bad sender ID), empty/long text. Use E.164 format. Verify sender ID registration. - Infobip 429
MESSAGE_LIMIT_EXCEEDED
: Exceeded account MPS. Slow down requests (implement outbound rate limiting in workers). - Infobip 5xx Server Error: Temporary Infobip issue. Retry with backoff (via queue). Check Infobip status page.
- Messages Sent but Not Received: Check DLRs (webhooks/polling) for errors (
EC_ABSENT_SUBSCRIBER
,EC_ANTI_SPAM_REJECTION
,EC_INSUFFICIENT_FUNDS
). Verify number, check balance, sender ID validity, country regulations. Contact Infobip support withmessageId
. - SDK Version Compatibility: Pin SDK version in
package.json
. Review changelogs before upgrading. - Cost Management: Monitor Infobip costs. Multi-part messages cost more. Clean lists.
- No Delivery Reports: Implement webhooks (
notifyUrl
) or polling if final status is needed.
12. Deployment and CI/CD
Getting your service into production:
-
Build Step: Not needed for our plain JS example unless using TypeScript (
npm run build
viatsc
) or bundling. -
Deployment Environments:
- PaaS (Heroku, Render, Fly.io): Simple. Push code, configure Env Vars via dashboard, define Procfile (
web: npm start
). Handles LB, HTTPS. - Containers (Docker): Build image, push to registry, deploy to Kubernetes, ECS, etc. Requires
Dockerfile
. EnsureNODE_ENV=production
,HOST=0.0.0.0
, and do not copy.env
into the image. Pass secrets via runtime environment. - VMs (EC2, GCE): Manual setup (Node, Nginx/proxy, PM2, firewall).
- PaaS (Heroku, Render, Fly.io): Simple. Push code, configure Env Vars via dashboard, define Procfile (
-
Process Management (PM2): Use PM2 for restarts, clustering, logging in VM/non-containerized environments.
npm install -g pm2
pm2 start src/server.js --name fastify-infobip-sms -i max
(cluster mode)pm2 list
,pm2 logs
,pm2 reload
-
CI/CD Pipeline (GitHub Actions Example):
- Create
.github/workflows/deploy.yml
to automate build, test, deploy on push tomain
. - Steps: Checkout code, setup Node,
npm ci
(clean install), lint, test (npm test
), deploy (e.g., trigger PaaS hookcurl -X POST ${{ secrets.RENDER_DEPLOY_HOOK_URL }}
,docker push
, etc.). - Store secrets (
RENDER_DEPLOY_HOOK_URL
,DOCKER_PASSWORD
) in GitHub Actions secrets.
- Create
-
Rollback Procedures: Use platform features (PaaS deploy history, Docker image tags, K8s rollbacks) or manual redeploy of previous versions. Consider blue-green/canary deployments.
13. Verification and Testing
Ensuring correctness and reliability:
-
Manual Verification (
curl
):- Start server:
npm run dev
- Send test requests (success case, validation errors like bad number/missing field) using
curl
. Verify expected responses (202 Accepted withbulkId
, 400 Bad Request with error details). - Check Infobip portal logs and test phones.
- Start server:
-
Automated Testing (Unit/Integration):
- Use
tap
(Fastify's default), Jest, or Node's test runner. - Install runner:
npm install --save-dev tap
- Add test script to
package.json
:""test"": ""tap test/**/*.test.js""
- Create test files (e.g.,
test/broadcast.test.js
). - Key Techniques:
- Export
build
function: Modifysrc/server.js
to export abuild
function that creates the Fastify instance without starting it (see finalserver.js
code below). - Mocking: In tests, replace
app.infobip.channels.sms.send
with a mock function (async (payload) => { ... return mockResponse; }
orthrow mockError;
) to isolate tests from the actual Infobip API and control scenarios. app.inject()
: Use Fastify's injection mechanism to simulate HTTP requests directly against the app instance.
- Export
- Use
-
Test Coverage:
- Configure runner for coverage reports (
tap --coverage-report=html
). Aim for high coverage.
- Configure runner for coverage reports (
-
Verification Checklist:
-
- Project setup OK?
-
- Infobip plugin loads?
-
-
/broadcast/sms
accepts valid POSTs?
-
-
- Validation rejects invalid payloads (400)?
-
- Success triggers (mock) Infobip call?
-
- Success returns 202 w/
bulkId
?
- Success returns 202 w/
-
- Simulated Infobip errors handled gracefully?
-
- Logs are informative?
-
- Env vars used correctly?
-
- (Manual) Messages logged in Infobip portal?
-
- (Manual) Messages received?
-
- Security measures active?
-
- Automated tests cover key scenarios?
-
- Test coverage sufficient?
-
Final Code Structure & Server Entry Point
Ensure your src/server.js
ties everything together:
// src/server.js
import Fastify from 'fastify';
import dotenv from 'dotenv';
import infobipPlugin from './plugins/infobip.js';
import broadcastRoutes from './routes/broadcast.js';
// Import other plugins if added
// import rateLimit from 'fastify-rate-limit';
// import helmet from 'fastify-helmet';
// Load environment variables early, once.
dotenv.config();
const buildServer = async () => {
const fastify = Fastify({
logger: {
level: process.env.LOG_LEVEL || 'info', // Default log level from .env or 'info'
// Consider pino-pretty transport only for development
// transport: process.env.NODE_ENV !== 'production'
// ? { target: 'pino-pretty' }
// : undefined,
},
});
// Register Plugins (Helmet, Rate Limit are examples, uncomment if used)
// await fastify.register(helmet);
// await fastify.register(rateLimit, { max: 100, timeWindow: '1 minute' });
await fastify.register(infobipPlugin);
// Register Routes
await fastify.register(broadcastRoutes);
// Basic health check route
fastify.get('/health', async (request, reply) => {
try {
if (!fastify.infobip) {
throw new Error('Infobip client not initialized');
}
// Add more checks if needed (e.g., DB ping)
return { status: 'ok', timestamp: new Date().toISOString() };
} catch (error) {
fastify.log.error({ err: error }, 'Health check failed');
reply.code(503).send({ status: 'error', message: error.message });
}
});
return fastify;
};
// Export build function for testing purposes
export const build = buildServer;
// Start the server only if this script is run directly (node src/server.js)
// Avoids starting the server when imported in tests
import { fileURLToPath } from 'url';
const currentPath = fileURLToPath(import.meta.url);
// Check if the executed script path matches the current module's path
if (process.argv[1] === currentPath) {
const start = async () => {
let server;
try {
server = await buildServer();
const port = process.env.PORT || 3000;
// Use '0.0.0.0' to listen on all available interfaces, needed for containers or VMs accessed externally.
const host = process.env.HOST || '127.0.0.1';
await server.listen({ port: parseInt(port, 10), host: host });
// Fastify's logger automatically logs the listening address on start
} catch (err) {
// Log the error using the logger if available, otherwise console.error
if (server && server.log) {
server.log.error(err, 'Error starting server');
} else {
console.error('Error starting server:', err);
}
process.exit(1);
}
};
start();
}