This guide provides a comprehensive, step-by-step walkthrough for building a production-ready service to send Multimedia Messaging Service (MMS) messages using the Sinch SMS API, Node.js, and the Fastify web framework. We'll cover everything from initial project setup and core API integration to security, error handling, testing, and deployment.
Project Overview and Goals
What We're Building:
We will create a simple but robust Fastify API endpoint that accepts requests to send an MMS message (containing an image or other media hosted at a public URL) to a specified recipient via Sinch.
Problem Solved:
This service provides a straightforward way to integrate MMS sending capabilities into larger applications, abstracting the direct interaction with the Sinch API into a dedicated microservice or API layer. This is useful for applications needing to send media-rich notifications, alerts, or marketing messages.
Technologies Used:
- Node.js: A JavaScript runtime built on Chrome's V8 engine, ideal for building fast, scalable network applications.
- Fastify: A high-performance, low-overhead web framework for Node.js, chosen for its speed, extensive plugin ecosystem, and developer-friendly features like built-in validation.
- Sinch SMS API (REST): We'll use Sinch's reliable REST API to handle the actual sending of MMS messages. While Sinch offers SDKs, using the REST API directly with
axios
provides clarity and control for this specific task. axios
: A popular promise-based HTTP client for making requests to the Sinch API.dotenv
: To manage environment variables securely for API credentials and configuration.pino-pretty
: To enhance development logging readability.@fastify/rate-limit
: To protect the API endpoint from abuse.tap
: A simple, effective testing framework often used with Fastify.
System Architecture:
+-------------+ +-----------------+ +-------------+ +-----------+
| Client | -----> | Fastify API | -----> | Sinch API | -----> | Recipient |
| (e.g., App, | | (/send-mms) | | (SMS /batches)| | (Mobile) |
| Frontend) | +-----------------+ +-------------+ +-----------+
| | | | ^ ^
| | | v | |
| | | Node.js | (MMS)
| | | (axios) |
| | +-----------------+ |
| | |
| | <------------------------------------------------------------+
| | (Optional: Delivery Status via Webhook -> DB -> API)
+-------------+
Final Outcome & Prerequisites:
By the end of this guide, you will have a running Fastify application with a single API endpoint (POST /send-mms
) capable of sending MMS messages via Sinch.
Prerequisites:
- Node.js and npm: Installed on your system (LTS version recommended).
- Sinch Account: A registered account at Sinch.com.
- Sinch Service Plan ID and API Token: Found in your Sinch Customer Dashboard under SMS -> APIs. Select your API and look for the REST configuration.
- Sinch Phone Number: An MMS-capable number purchased or configured within your Sinch account, associated with the Service Plan ID above.
- Basic Command Line/Terminal Familiarity.
- (Optional)
curl
or Postman: For testing the API endpoint. - (Crucial for US Traffic) A2P 10DLC Registration: If sending MMS to US numbers, ensure your Sinch number is associated with a registered Application-to-Person (A2P) 10-digit long code (10DLC) campaign. Consult Sinch documentation for details on registration.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies using npm
.
Step 1: Create Project Directory
Open your terminal and create a new directory for the project, then navigate into it.
mkdir sinch-mms-fastify
cd sinch-mms-fastify
Step 2: Initialize npm Project
This creates a package.json
file to manage project dependencies and scripts. The -y
flag accepts default settings.
npm init -y
Step 3: Install Dependencies
We need Fastify, axios
for HTTP requests, dotenv
for environment variables, and @fastify/rate-limit
for security.
npm install fastify axios dotenv @fastify/rate-limit
Step 4: Install Development Dependencies
We'll use pino-pretty
for readable logs during development and tap
for testing.
npm install --save-dev pino-pretty tap
Step 5: Create Project Structure
Set up a basic structure for clarity:
mkdir src
mkdir src/routes
mkdir src/services
touch src/app.js
touch src/server.js
touch src/routes/mms.js
touch src/services/sinchService.js
touch .env
touch .env.example
touch .gitignore
Your structure should look like this:
sinch-mms-fastify/
├── node_modules/
├── src/
│ ├── app.js # Fastify app configuration
│ ├── server.js # Server startup logic
│ ├── routes/
│ │ └── mms.js # MMS sending route handler
│ └── services/
│ └── sinchService.js # Logic for interacting with Sinch API
├── .env # Local environment variables (DO NOT COMMIT)
├── .env.example # Example environment variables (Commit this)
├── .gitignore # Files/folders ignored by Git
├── package.json
└── package-lock.json
Step 6: Configure .gitignore
Prevent sensitive files and unnecessary folders from being committed to version control. Add the following to .gitignore
:
# Node dependencies
node_modules/
# Environment variables
.env*
!.env.example
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Build output
dist/
build/
# OS generated files
.DS_Store
Thumbs.db
Step 7: Configure Environment Variables
Populate .env.example
with the required variable names. This serves as a template.
.env.example
:
# Sinch API Credentials (Find in Sinch Dashboard -> SMS -> APIs -> Your API -> REST Configuration)
SINCH_SERVICE_PLAN_ID=YOUR_SERVICE_PLAN_ID
SINCH_API_TOKEN=YOUR_API_TOKEN
# Sinch Number associated with the Service Plan ID (E.164 format, e.g., +12025550181)
SINCH_FROM_NUMBER=YOUR_SINCH_NUMBER
# Sinch API Region Base URL (e.g., us, eu, ca, au, br, jp) - determines the base URL
# Examples: https://us.sms.api.sinch.com, https://eu.sms.api.sinch.com
# Defaulting to US here. Change if needed.
SINCH_REGION=us
# Server Configuration
PORT=3000
HOST=0.0.0.0 # Listen on all available network interfaces
LOG_LEVEL=info # Pino log levels: 'fatal', 'error', 'warn', 'info', 'debug', 'trace' or 'silent'
Now, create your local .env
file by copying .env.example
and fill in your actual credentials obtained from the Sinch dashboard. Never commit your .env
file.
.env
:
# --- COPY from .env.example and fill with REAL values ---
SINCH_SERVICE_PLAN_ID=your_actual_service_plan_id_here
SINCH_API_TOKEN=your_actual_api_token_here
SINCH_FROM_NUMBER=+1xxxxxxxxxx # Your actual Sinch number
SINCH_REGION=us # Or your region
PORT=3000
HOST=0.0.0.0
LOG_LEVEL=info
# --- END COPY ---
Step 8: Add Run Scripts to package.json
Add scripts for starting the server in development (with pretty logging) and production modes.
package.json
:
{
// ... other package.json content ...
""main"": ""src/server.js"", // Specify entry point
""scripts"": {
""start"": ""node src/server.js"",
""dev"": ""node src/server.js | pino-pretty"", // Pipe output to pino-pretty for dev
""test"": ""tap src/**/*.test.js"" // Configure test script later
},
// ... other package.json content ...
}
Why this setup?
- Separation of Concerns: Dividing code into
routes
,services
,app.js
, andserver.js
makes the application easier to understand, maintain, and test. - Environment Variables: Using
.env
keeps sensitive credentials out of the codebase and allows for different configurations per environment (dev, staging, prod). .gitignore
: Essential for preventing accidental commits of secrets or large directories.pino-pretty
: Improves developer experience by making logs human-readable during development.npm scripts
: Standardizes common tasks like starting the server.
2. Implementing Core Functionality (Sinch Service)
Now, let's write the code that interacts directly with the Sinch API to send the MMS message.
File: src/services/sinchService.js
import axios from 'axios';
import dotenv from 'dotenv';
// Load environment variables early
dotenv.config();
const {
SINCH_SERVICE_PLAN_ID,
SINCH_API_TOKEN,
SINCH_FROM_NUMBER,
SINCH_REGION = 'us', // Default to 'us' if not set
} = process.env;
// Validate essential configuration
if (!SINCH_SERVICE_PLAN_ID || !SINCH_API_TOKEN || !SINCH_FROM_NUMBER) {
// Using console.error here as the main app logger might not be initialized yet
console.error('Error: Missing required Sinch environment variables (SERVICE_PLAN_ID, API_TOKEN, FROM_NUMBER).');
process.exit(1); // Exit if critical config is missing
}
// Construct base URL based on region
const SINCH_BASE_URL = `https://${SINCH_REGION}.sms.api.sinch.com/xms/v1/${SINCH_SERVICE_PLAN_ID}`;
// Create an axios instance for Sinch API calls
const sinchApiClient = axios.create({
baseURL: SINCH_BASE_URL,
headers: {
'Authorization': `Bearer ${SINCH_API_TOKEN}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
timeout: 15000, // 15 second timeout
});
/**
* Sends an MMS message via the Sinch API.
* Note: Uses console.log/error for simplicity in this standalone module.
* A more sophisticated setup might involve injecting a logger instance.
*
* @param {string} to The recipient phone number in E.164 format (e.g., +1xxxxxxxxxx).
* @param {string} mediaUrl The publicly accessible URL of the media file (JPEG, PNG, GIF supported).
* @param {string} [text] Optional text message to accompany the media. Max 1600 chars.
* @returns {Promise<object>} The response data from the Sinch API on success.
* @throws {Error} If the API call fails or returns an error status.
*/
async function sendMms(to, mediaUrl, text = '') {
const endpoint = '/batches'; // Using the batches endpoint
// Construct the payload for sending a single MMS message within a batch
const payload = {
to: [to], // Recipient needs to be in an array
from: SINCH_FROM_NUMBER,
body: {
url: mediaUrl,
// You can optionally add a message/caption to the media body itself
// message: ""Media Caption Example"",
},
// Add the text part if provided
...(text && { parameters: { // Standard text goes in parameters for media messages
// Note: The exact parameter key and carrier handling for text alongside MMS can vary.
// 'text_message' is a plausible custom parameter.
// Consult the latest Sinch API documentation for the /batches endpoint and test with target carriers
// if precise text display behavior is critical.
text_message: { default: text }
}}),
// Explicitly defining type might be needed for some scenarios, but often inferred
// type: 'mt_media',
// delivery_report: 'full' // Optional: Request detailed delivery reports (requires webhook setup)
};
try {
console.info(`[Sinch Service] Attempting to send MMS to ${to}...`); // Basic logging
const response = await sinchApiClient.post(endpoint, payload);
// Sinch API usually returns 200 or 201 on success for batch sends
if (response.status === 200 || response.status === 201) {
console.info(`[Sinch Service] Sinch API Success Response Status: ${response.status}`);
console.info('[Sinch Service] Sinch Response Data:', response.data); // Log successful response data
// The response.data often contains a batch_id which can be used for tracking
return response.data;
} else {
// This case might not be hit often if axios throws on non-2xx status, but good practice
console.error(`[Sinch Service] Sinch API returned non-success status: ${response.status}`, response.data);
throw new Error(`Sinch API Error: Status ${response.status}`);
}
} catch (error) {
console.error('[Sinch Service] Error sending MMS via Sinch:', error.message);
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error('[Sinch Service] Sinch Error Response Data:', error.response.data);
console.error('[Sinch Service] Sinch Error Response Status:', error.response.status);
console.error('[Sinch Service] Sinch Error Response Headers:', error.response.headers);
// Rethrow a more specific error including details from Sinch if available
const sinchError = error.response.data?.request_error?.service_exception?.text
|| error.response.data?.text
|| `Sinch API request failed with status ${error.response.status}`;
throw new Error(`Failed to send MMS: ${sinchError}`);
} else if (error.request) {
// The request was made but no response was received
console.error('[Sinch Service] Sinch Error Request:', error.request);
throw new Error('Failed to send MMS: No response received from Sinch API.');
} else {
// Something happened in setting up the request that triggered an Error
console.error('[Sinch Service] Sinch Error Config:', error.config);
throw new Error(`Failed to send MMS: Error setting up request - ${error.message}`);
}
}
}
export default { sendMms };
Explanation:
- Environment Variables: We load credentials using
dotenv
and immediately check if the essential ones (SERVICE_PLAN_ID
,API_TOKEN
,SINCH_FROM_NUMBER
) are present. The application exits if they are missing, preventing runtime errors later. - Base URL: The Sinch API base URL is constructed dynamically based on the
SINCH_REGION
environment variable, defaulting tous
. axios
Instance: We create a dedicatedaxios
instance (sinchApiClient
) pre-configured with thebaseURL
,Authorization
header (using the Bearer token), and standard JSON headers. Setting a timeout prevents requests from hanging indefinitely.sendMms
Function:- Takes the recipient number (
to
), themediaUrl
, and optionaltext
as arguments. - Defines the Sinch
/batches
endpoint. While sending a single message, the/batches
endpoint is commonly used and supports MMS. - Constructs the
payload
according to the Sinch API specification for sending MMS via the batches endpoint:to
: An array containing the recipient number(s).from
: The Sinch number loaded from environment variables.body
: An object containing theurl
of the media.parameters
: Iftext
is provided, it's added within theparameters
object. This is a common way to include text alongside media in the/batches
endpoint. (Note: Carrier handling of text alongside MMS can vary, consult latest Sinch docs).
- Uses
async/await
to callsinchApiClient.post
. - Includes basic
console.info
/console.error
logging for this service module. The Fastify route handler uses the framework's structured logger (fastify.log
). - Performs robust error handling:
- Checks the response status code for success (200 or 201).
- Catches various
axios
error types (response error, request error, setup error). - Logs detailed error information from the Sinch response (
error.response.data
) if available. - Throws a new, more informative error message for the calling function to handle.
- Takes the recipient number (
3. Building the API Layer (Fastify Route)
Now, let's create the Fastify route that will receive requests and use our sinchService
to send the MMS.
File: src/routes/mms.js
import sinchService from '../services/sinchService.js';
// Define the JSON schema for the request body for validation
const sendMmsBodySchema = {
type: 'object',
required: ['to', 'mediaUrl'],
properties: {
to: {
type: 'string',
description: 'Recipient phone number in E.164 format (e.g., +12025550181)',
// Basic pattern for E.164 format (must start with +, followed by digits)
pattern: '^\\+[1-9]\\d{1,14}$'
},
mediaUrl: {
type: 'string',
description: 'Publicly accessible URL of the media file (JPEG, PNG, GIF)',
format: 'url' // Use Fastify's built-in format validation
},
text: {
type: 'string',
description: 'Optional text message to accompany the media (max 1600 chars)',
maxLength: 1600,
nullable: true // Allow null or missing
}
},
additionalProperties: false // Disallow properties not defined in the schema
};
// Define the schema for the success response
const successResponseSchema = {
type: 'object',
properties: {
message: { type: 'string' },
batch_id: { type: 'string', description: 'Sinch batch ID for tracking' },
// Include other relevant fields from Sinch response if needed
}
};
// Define the schema for error responses
const errorResponseSchema = {
type: 'object',
properties: {
statusCode: { type: 'integer' },
error: { type: 'string' },
message: { type: 'string' },
}
};
/**
* Fastify route handler plugin for sending MMS.
* @param {import('fastify').FastifyInstance} fastify The Fastify instance.
* @param {object} options Plugin options.
*/
async function mmsRoutes(fastify, options) {
fastify.post('/send-mms', {
schema: {
description: 'Send an MMS message via Sinch',
tags: ['MMS'], // Useful for OpenAPI documentation generation
summary: 'Sends an MMS message',
body: sendMmsBodySchema,
response: {
200: successResponseSchema, // Successful send
// Sinch might return 201 Created for batches, map to 200 for consistency or keep 201
// 201: successResponseSchema,
// Add schemas for 4xx and 5xx errors for OpenAPI documentation
400: errorResponseSchema, // Validation error
500: errorResponseSchema // Server/Sinch API error
}
}
}, async (request, reply) => {
// Access the logger instance decorated onto the request object
const log = request.log;
const { to, mediaUrl, text } = request.body;
log.info(`Received request to send MMS to ${to}`);
try {
// Pass the logger instance to the service if it were refactored to accept it
// const sinchResponse = await sinchService.sendMms(to, mediaUrl, text, log);
const sinchResponse = await sinchService.sendMms(to, mediaUrl, text);
// Sinch batch API usually returns 200 or 201 on success
// Standardize API response to 200 OK for simplicity client-side.
const replyPayload = {
message: 'MMS sent successfully via Sinch.',
// Pass relevant info back, like the batch_id for tracking
batch_id: sinchResponse?.id || 'N/A',
};
reply.code(200).send(replyPayload);
} catch (error) {
log.error({ err: error }, `Error processing /send-mms request: ${error.message}`);
// Determine appropriate status code based on error type
// Check if it's likely a client-side issue passed up from the service
if (error.message.includes('Failed to send MMS:') &&
(error.message.includes('400') || // Rough check for Sinch 400 errors
error.message.includes('Invalid') ||
error.message.includes('format'))) {
reply.code(400).send({
statusCode: 400,
error: 'Bad Request',
// Provide a clearer message if possible, else the Sinch error
message: error.message.replace('Failed to send MMS: ', '')
});
} else {
// Assume internal server error or downstream Sinch API error (5xx, network, etc.)
reply.code(500).send({
statusCode: 500,
error: 'Internal Server Error',
// Avoid leaking potentially sensitive details from Sinch 5xx errors
message: 'An unexpected error occurred while attempting to send the MMS.'
});
}
}
});
}
export default mmsRoutes;
Explanation:
- Schema Validation: We define detailed JSON schemas (
sendMmsBodySchema
,successResponseSchema
,errorResponseSchema
) for the request body and possible responses. Fastify uses these schemas to:- Automatically validate incoming requests: If a request doesn't match
sendMmsBodySchema
, Fastify automatically sends back a 400 Bad Request error before our handler code even runs. This is efficient and secure. We validateto
(E.164 pattern),mediaUrl
(must be a valid URL format), andtext
(optional, max length).additionalProperties: false
prevents unexpected fields. - Serialize responses: Ensures the outgoing response matches the defined success or error schema.
- (Optional) Generate OpenAPI documentation: These schemas are essential if you later use plugins like
@fastify/swagger
to generate API docs.
- Automatically validate incoming requests: If a request doesn't match
- Route Definition:
fastify.post('/send-mms', { schema }, async (request, reply) => { ... })
defines thePOST
endpoint.- The
schema
option attaches our validation and response schemas to the route.
- Handler Logic:
- Extracts validated data (
to
,mediaUrl
,text
) fromrequest.body
. - Uses
request.log.info
(Fastify's request-bound logger) for logging within the handler context. - Calls
sinchService.sendMms
within atry...catch
block. - On success, sends a 200 OK response with a success message and the
batch_id
received from Sinch. - On error, logs the error using
request.log.error
(including the error object for more context) and sends an appropriate error response (attempting to distinguish between 400 Bad Request for likely client/Sinch input errors and 500 Internal Server Error for other issues) with a standardized error payload.
- Extracts validated data (
4. Integrating Third-Party Services (Sinch - Already Done) & Configuring Fastify App
The core Sinch integration was handled in src/services/sinchService.js
. Now let's configure our main Fastify application (app.js
) to load environment variables, set up logging, register plugins (like rate limiting), and register our MMS route.
File: src/app.js
import Fastify from 'fastify';
import dotenv from 'dotenv';
import rateLimit from '@fastify/rate-limit';
import mmsRoutes from './routes/mms.js';
// Load environment variables first
dotenv.config();
const {
LOG_LEVEL = 'info', // Default to 'info' if not set
} = process.env;
/**
* Builds and configures the Fastify application instance.
* @param {object} opts - Options for Fastify constructor (e.g., for testing)
* @returns {Promise<import('fastify').FastifyInstance>} The configured Fastify app instance.
*/
async function buildApp(opts = {}) {
// Initialize Fastify
// Pass logger options based on environment (pretty print in dev/test)
const app = Fastify({
logger: {
level: LOG_LEVEL,
...(process.env.NODE_ENV !== 'production' && { // Enable pretty printing only outside production
transport: {
target: 'pino-pretty',
options: {
translateTime: 'HH:MM:ss Z', // Human-readable time
ignore: 'pid,hostname', // Hide less relevant fields like process ID and hostname
},
}
}),
},
...opts, // Pass any additional options (e.g., from tests)
});
// --- Register Plugins ---
// 1. Rate Limiting
// Protects against brute-force attacks and abuse
await app.register(rateLimit, {
max: 100, // Max requests per window (adjust as needed)
timeWindow: '1 minute', // Time window
// Optional: Add Redis for distributed rate limiting across multiple instances
// redis: new Redis({ host: '127.0.0.1', port: 6379 }),
// Optional: Customize error response
// errorResponseBuilder: function (req, context) {
// return {
// statusCode: 429,
// error: 'Too Many Requests',
// message: `Rate limit exceeded. You are allowed ${context.max} requests per ${context.after}. Please try again later.`,
// }
// }
});
app.log.info('Rate limit plugin registered.');
// Add other plugins here if needed (e.g., @fastify/cors, @fastify/swagger, @fastify/helmet)
// --- Register Routes ---
await app.register(mmsRoutes, { prefix: '/api/v1' }); // Prefix routes with /api/v1
app.log.info('MMS routes registered under /api/v1.');
// --- Add Hooks (optional) ---
// Example: A global hook to log every request start/end
// app.addHook('onRequest', async (request, reply) => {
// request.log.info({ req: { method: request.method, url: request.url, id: request.id } }, 'Incoming request');
// });
// app.addHook('onResponse', async (request, reply) => {
// request.log.info({ res: { statusCode: reply.statusCode }, reqId: request.id }, 'Request completed');
// });
// --- Graceful Shutdown ---
// Handle SIGINT (Ctrl+C) and SIGTERM (used by Docker/Kubernetes)
const shutdown = async (signal) => {
app.log.warn(`Received ${signal}. Shutting down gracefully...`);
try {
await app.close();
app.log.info('Server closed successfully.');
process.exit(0);
} catch (err) {
app.log.error({ err }, 'Error during server shutdown.');
process.exit(1);
}
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
app.log.info('Application setup complete.');
return app;
}
export default buildApp;
File: src/server.js
import buildApp from './app.js';
const {
PORT = 3000, // Default to 3000 if not set
HOST = '0.0.0.0' // Default to listen on all interfaces
} = process.env;
/**
* Starts the Fastify server.
*/
async function startServer() {
let app;
try {
app = await buildApp();
// Start listening and log the address
await app.listen({ port: PORT, host: HOST });
// Note: Accessing app.server.address() might return null if listening on a pipe/socket
// or if called before the 'listening' event. Fastify's internal log usually handles this.
// For explicit logging, ensure it's done after await app.listen() resolves.
const address = app.server.address();
const addressString = typeof address === 'string' ? address : `${address?.address}:${address?.port}`;
app.log.info(`Server listening on ${addressString}`);
} catch (err) {
// Log error using console.error before app.log might be available or if app init failed
console.error('Error starting server:', err);
// If app exists and has log, use it, otherwise stick to console
if (app && app.log) {
app.log.error({ err }, 'Server startup failed.');
}
process.exit(1);
}
}
startServer();
Explanation:
src/app.js
(Configuration):- Imports: Brings in Fastify,
dotenv
, therateLimit
plugin, and ourmmsRoutes
. buildApp
Function: Encapsulates app creation and configuration. This pattern is useful for testing, allowing tests to import and build the app without automatically starting the server.- Fastify Instance: Creates the
app
instance.- Logger: Configured with
LOG_LEVEL
from environment variables. It smartly enablespino-pretty
only whenNODE_ENV
is notproduction
, ensuring structured JSON logs in production (better for log aggregation tools) and readable logs in development/testing.
- Logger: Configured with
- Plugin Registration:
@fastify/rate-limit
: Registered usingapp.register
. We set a basic limit (e.g., 100 requests per minute). Comments indicate how to use Redis for distributed limiting or customize the error response.
- Route Registration:
app.register(mmsRoutes, { prefix: '/api/v1' })
: Registers all routes defined inmms.js
under the/api/v1
path prefix. This versions the API.
- Graceful Shutdown: Implements handlers for
SIGINT
andSIGTERM
signals. This ensures that when the server is asked to stop (e.g., by Ctrl+C, Docker, or Kubernetes), it attempts to finish processing existing requests and close connections cleanly usingapp.close()
before exiting.
- Imports: Brings in Fastify,
src/server.js
(Startup):- Imports: Imports the
buildApp
function. startServer
Function:- Calls
buildApp()
to get the configured Fastify instance. - Calls
app.listen()
to start the HTTP server, listening on thePORT
andHOST
defined in environment variables (or defaults). - Logs the listening address using
app.log.info
after the server has successfully started listening. It correctly handles getting the address information. - Includes robust error handling for startup failures, attempting to use
app.log
if available, otherwise falling back toconsole.error
.
- Calls
- Imports: Imports the
5. Implementing Error Handling, Logging, and Retry Mechanisms
- Error Handling: We've already implemented significant error handling:
- Fastify's automatic request validation via schemas (
mms.js
). try...catch
blocks in the route handler (mms.js
) and the service layer (sinchService.js
).- Detailed error logging (using
request.log
in the route,console.error
in the service), including Sinch API responses when available in the service layer logs. - Returning appropriate HTTP status codes (400, 500) with consistent JSON error payloads from the API route.
- Fastify's automatic request validation via schemas (
- Logging:
- Fastify's built-in Pino logger is configured in
app.js
. - Uses
pino-pretty
for readable development logs. - Produces structured JSON logs in production (controlled by
NODE_ENV
). - Logs are written at different levels (
info
,error
,warn
) providing context. Request-bound logging (request.log
) in the route handler automatically includes request IDs. - Troubleshooting: Analyze logs (e.g., using
grep
or log management tools like Datadog, Splunk in production) to diagnose issues. Look for error messages, stack traces, and context like request IDs.
- Fastify's built-in Pino logger is configured in
- Retry Mechanisms:
- Sending SMS/MMS can sometimes experience transient network issues or brief downstream service unavailability (e.g., Sinch returning 5xx errors). Implementing retries can improve reliability for such cases.
- Strategy: Use a library like
axios-retry
integrated with thesinchApiClient
insinchService.js
.- Install:
npm install axios-retry
- Example Integration (in
src/services/sinchService.js
):import axios from 'axios'; import dotenv from 'dotenv'; import axiosRetry from 'axios-retry'; // Import // ... Load env vars ... // Construct base URL ... // Create axios instance ... const sinchApiClient = axios.create({ /* ... */ }); // Configure retries on the instance axiosRetry(sinchApiClient, { retries: 3, // Number of retry attempts retryDelay: (retryCount, error) => { console.warn(`[Sinch Service] Retry attempt ${retryCount} for ${error.config.url}: ${error.message}`); return axiosRetry.exponentialDelay(retryCount, error, 1000); // Exponential backoff starting at 1s }, retryCondition: (error) => { // Retry on network errors or specific HTTP status codes (e.g., 5xx from Sinch) return axiosRetry.isNetworkOrIdempotentRequestError(error) || (error.response && error.response.status >= 500 && error.response.status <= 599); }, shouldResetTimeout: true, // Reset timeout on retries }); // ... rest of sinchService.js (sendMms function) ...
- Install:
- Considerations: Retries are generally safe for the Sinch
/batches
endpoint, especially if using a uniqueclient_reference
(not shown here) for idempotency. Avoid retrying on non-recoverable errors like 4xx client errors (e.g., invalid number format).