code examples
code examples
How to Send MMS with Sinch, Node.js, and Fastify: Complete Tutorial
Learn how to send MMS messages using Sinch SMS API with Node.js and Fastify. Step-by-step tutorial covering project setup, API integration, error handling, rate limiting, and A2P 10DLC compliance for production-ready multimedia messaging.
Sinch Node.js Fastify MMS Multimedia Sending Guide
Learn how to build a production-ready MMS messaging service using the Sinch SMS API, Node.js, and Fastify. This comprehensive tutorial walks you through sending multimedia messages (images, GIFs, videos) with complete code examples covering project setup, Sinch API integration, error handling, rate limiting, and A2P 10DLC compliance requirements for US messaging.
What You'll Build: MMS Messaging API with Sinch and Fastify
<!-- DEPTH: Section lacks concrete use case examples and ROI justification (Priority: Medium) -->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.
<!-- GAP: Missing comparison of MMS vs SMS effectiveness and when to use each (Type: Substantive) -->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
axiosprovides 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)
+-------------+
<!-- DEPTH: Architecture diagram needs explanation of data flow and failure scenarios (Priority: High) -->
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: Node.js v20 or later required (v22 LTS "Jod" recommended for new projects, Active LTS until October 2025). Note: Node.js v18 reaches end-of-life April 30, 2025. Fastify v5 requires Node.js v20+.
- 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. Note: Contact your Sinch account manager to enable MMS support.
- Basic Command Line/Terminal Familiarity.
- (Optional)
curlor Postman: For testing the API endpoint. - (Mandatory for US Traffic) A2P 10DLC Registration: Effective February 1, 2025, all businesses sending SMS/MMS messages to US numbers must register with A2P 10DLC. Unregistered messages are blocked by US carriers. Registration requires Business EIN, campaign description, opt-in documentation, and typically takes 1–3 weeks. Work with Sinch to complete registration via The Campaign Registry (TCR).
Step 1: Setting Up Your Node.js MMS 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-fastifyStep 2: Initialize npm Project
This creates a package.json file to manage project dependencies and scripts. The -y flag accepts default settings.
npm init -yStep 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-limitStep 4: Install Development Dependencies
We'll use pino-pretty for readable logs during development and tap for testing.
npm install --save-dev pino-pretty tapStep 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 .gitignoreYour 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.dbStep 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.jsmakes the application easier to understand, maintain, and test. - Environment Variables: Using
.envkeeps 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.
Step 2: Implementing the Sinch MMS Service
Now, let's write the code that interacts directly with the Sinch SMS API to send MMS messages. This service layer handles authentication, request construction, and error handling when communicating with Sinch's REST API endpoints.
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.
* Supported formats: JPEG/JPG, PNG, GIF (static and animated).
* Recommended size: Keep under 500 KB for optimal deliverability (max 2 MB).
* Recommended dimensions: 640x1138 pixels (9:16 ratio) or 600x600 (square).
* @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
dotenvand 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_REGIONenvironment variable, defaulting tous. axiosInstance: We create a dedicatedaxiosinstance (sinchApiClient) pre-configured with thebaseURL,Authorizationheader (using the Bearer token), and standard JSON headers. Setting a timeout prevents requests from hanging indefinitely.sendMmsFunction:- Takes the recipient number (
to), themediaUrl, and optionaltextas arguments. - Defines the Sinch
/batchesendpoint. While sending a single message, the/batchesendpoint is commonly used and supports MMS. - Constructs the
payloadaccording 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 theurlof the media.parameters: Iftextis provided, it's added within theparametersobject. This is a common way to include text alongside media in the/batchesendpoint. (Note: Carrier handling of text alongside MMS can vary, consult latest Sinch docs).
- Uses
async/awaitto callsinchApiClient.post. - Includes basic
console.info/console.errorlogging 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
axioserror 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 (
Step 3: Creating the Fastify API Endpoint for MMS
Now, let's create the Fastify route handler that receives HTTP requests and uses our sinchService to send MMS messages. This endpoint includes automatic request validation, structured error handling, and comprehensive logging for production use.
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: falseprevents 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/swaggerto generate API docs.
- Automatically validate incoming requests: If a request doesn't match
- Route Definition:
fastify.post('/send-mms', { schema }, async (request, reply) => { ... })defines thePOSTendpoint.- The
schemaoption 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.sendMmswithin atry...catchblock. - On success, sends a 200 OK response with a success message and the
batch_idreceived 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 (
Step 4: Configuring the Fastify Application
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, therateLimitplugin, and ourmmsRoutes. buildAppFunction: 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
appinstance.- Logger: Configured with
LOG_LEVELfrom environment variables. It smartly enablespino-prettyonly whenNODE_ENVis 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.jsunder the/api/v1path prefix. This versions the API.
- Graceful Shutdown: Implements handlers for
SIGINTandSIGTERMsignals. 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
buildAppfunction. startServerFunction:- Calls
buildApp()to get the configured Fastify instance. - Calls
app.listen()to start the HTTP server, listening on thePORTandHOSTdefined in environment variables (or defaults). - Logs the listening address using
app.log.infoafter the server has successfully started listening. It correctly handles getting the address information. - Includes robust error handling for startup failures, attempting to use
app.logif available, otherwise falling back toconsole.error.
- Calls
- Imports: Imports the
Step 5: Adding Error Handling, Logging, and Retry Logic
<!-- GAP: Missing concrete examples of error scenarios and how to debug them (Type: Substantive) -->- Error Handling: We've already implemented significant error handling:
- Fastify's automatic request validation via schemas (
mms.js). try...catchblocks in the route handler (mms.js) and the service layer (sinchService.js).- Detailed error logging (using
request.login the route,console.errorin 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-prettyfor 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
grepor 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-retryintegrated with thesinchApiClientinsinchService.js.- Install:
npm install axios-retry - Example Integration (in
src/services/sinchService.js):javascriptimport 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
/batchesendpoint, 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).
MMS Technical Specifications and Sinch API Limits
<!-- DEPTH: Technical specs lack carrier-specific limitations and regional variations (Priority: High) -->MMS Media Requirements
Supported Formats:
- JPEG/JPG
- PNG
- GIF (static and animated)
File Size Limits:
- Recommended: 200–500 KB for optimal speed and deliverability
- Maximum: 2 MB (varies by carrier)
- Note: Larger files may experience delivery delays or failures on certain carrier networks
Image Dimensions:
- Default preview: 640×1138 pixels (9:16 aspect ratio)
- Square: 600×600 pixels (recommended for profile images)
- Landscape: 1280×720 pixels (recommended for wide content)
Sinch API Specifications
API Type: mt_media (required for MMS messages via batches endpoint)
Endpoint Structure:
https://{region}.sms.api.sinch.com/xms/v1/{service_plan_id}/batches
Regional Endpoints:
us- United Stateseu- Europeca- Canadaau- Australiabr- Braziljp- Japan
Rate Limits: Contact Sinch support for account-specific rate limits and throughput requirements.
<!-- GAP: Missing webhook configuration for delivery reports and status callbacks (Type: Critical) -->A2P 10DLC Compliance (US Only)
Mandatory Since: February 1, 2025
Registration Requirements:
- Business EIN (Employer Identification Number)
- Professional email address (non-free domains)
- Campaign description and use case
- Opt-in method documentation
- Privacy policy and terms of service URLs
Timeline: 1–3 weeks for standard campaigns (3–7 business days for campaign approval after brand registration)
Restricted Industries: Cannabis/hemp, firearms, payday loans, third-party collections are not eligible for 10DLC registration.
Consequence of Non-Registration: Messages blocked by US carriers, higher filtering rates, potential account suspension.
<!-- EXPAND: Could add FAQ section addressing common A2P 10DLC registration issues (Type: Enhancement) -->Related Resources
Looking to expand your messaging capabilities? Check out these related guides:
- E.164 Phone Number Format Guide - Essential formatting for international phone numbers
- A2P 10DLC Registration Process - Complete guide to US messaging compliance
- SMS vs MMS: When to Use Each - Compare messaging types and use cases
Source Citations
Node.js Version Information:
- Node.js Release Schedule: https://nodejs.org/en/about/previous-releases
- Node.js v22 LTS Announcement: https://nodesource.com/blog/Node.js-v22-Long-Term-Support-LTS
- Node.js End-of-Life Dates: https://endoflife.date/nodejs
Fastify Framework:
- Fastify npm Package: https://www.npmjs.com/package/fastify
- Fastify V5 Migration Guide: https://fastify.dev/docs/v5.0.x/Guides/Migration-Guide-V5/
- Fastify LTS Documentation: https://fastify.dev/docs/v5.4.x/Reference/LTS/
Sinch MMS API:
- Sinch SMS API Reference: https://developers.sinch.com/docs/sms/api-reference/
- Sinch MMS Support Documentation: https://developers.sinch.com/docs/sms/api-reference/mms-support/
- Sinch Batches Endpoint: https://developers.sinch.com/docs/sms/api-reference/sms/tag/Batches/
- Sinch SMS API Overview: https://developers.sinch.com/docs/sms/api-reference/sms/overview/
MMS Technical Standards:
- AWS MMS File Types and Size Limits: https://docs.aws.amazon.com/sms-voice/latest/userguide/mms-limitations-character.html
- Twilio MMS Supported File Types: https://support.twilio.com/hc/en-us/articles/360018832773
- MMS Image Size Guide: https://sakari.io/blog/mms-image-size-guide
A2P 10DLC Compliance:
- Twilio A2P 10DLC Documentation: https://www.twilio.com/docs/messaging/compliance/a2p-10dlc
- 10DLC 2025 Registration Guide: https://callhub.io/blog/compliance/10dlc-2025-registration-callhub/
- Infobip A2P 10DLC Guide: https://www.infobip.com/blog/10dlc-registration
- Nextiva A2P 10DLC Complete Guide: https://www.nextiva.com/blog/what-is-a2p-10dlc.html