Sending MMS with Node.js, Express, and Sinch
This guide provides a comprehensive walkthrough for building a production-ready Node.js application using the Express framework to send Multimedia Messaging Service (MMS) messages via the Sinch MMS JSON API. We'll cover everything from initial project setup to deployment and monitoring.
Important: Sinch offers multiple APIs (e.g., legacy APIs, unified Messages API). This guide focuses on a specific MMS JSON API structure. Always verify endpoints, payload structures, authentication methods, and specific behaviors against the current, official Sinch documentation relevant to your account and the specific API product version you are using. API details can change.
By the end of this tutorial, you will have a functional Express API endpoint capable of accepting MMS details (recipient number, media URLs, text) and dispatching the message through Sinch, including handling fallbacks to SMS.
Project Overview and Goals
Goal: To create a robust backend service that enables sending MMS messages programmatically using Sinch's dedicated MMS API.
Problem Solved: Provides a reliable way to integrate rich media messaging into applications, automating communication workflows that require images, videos, or other media alongside text.
Technologies Used:
- Node.js: A JavaScript runtime environment for building server-side applications.
- Express: A minimal and flexible Node.js web application framework used to build the API layer.
- Axios: A promise-based HTTP client for making requests to the Sinch API.
- dotenv: A module to load environment variables from a
.env
file for secure credential management. - Sinch MMS JSON API: The specific Sinch API endpoint and protocol for sending MMS messages (subject to verification against current documentation).
System Architecture:
+-------------+ +---------------------+ +-----------------+
| Your Client | ----> | Node.js/Express App | ----> | Sinch MMS API |
| (e.g., Web, | | (API Endpoint) | | |
| Mobile App) | +---------------------+ +-----------------+
+-------------+ |
| (Optional)
v
+-------------+
| Database |
| (Logging) |
+-------------+
(Note: This is a simplified text-based representation.)
Prerequisites:
- Node.js and npm (or Yarn) installed.
- A Sinch account with access to the MMS API product you intend to use.
- Your Sinch Service Plan ID (may also be called Campaign ID or similar - verify term in your dashboard/docs).
- Your Sinch API Token associated with the Service Plan ID.
- A Sinch provisioned phone number (Short Code, Toll-Free, or 10DLC) capable of sending MMS.
- Publicly accessible URLs for the media files (images, videos, audio, etc.) you intend to send. These servers must return a
Content-Length
header. - Familiarity with the official Sinch MMS API documentation for verification.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
Step 1: Create Project Directory
Open your terminal and create a new directory for the project, then navigate into it.
mkdir sinch-mms-sender
cd sinch-mms-sender
Step 2: Initialize Node.js Project
Initialize the project using npm. Accept the defaults or customize as needed.
npm init -y
This creates a package.json
file.
Step 3: Install Dependencies
We need Express for the server, Axios to make HTTP requests, and dotenv to handle environment variables. We'll also add nodemon
as a development dependency for easier development.
npm install express axios dotenv
npm install --save-dev nodemon
Step 4: Configure Environment Variables
Create a file named .env
in the root of your project. This file will store your sensitive credentials and configuration. Never commit this file to version control.
# .env
# --- Sinch Credentials ---
# Get these from your Sinch Customer Dashboard (e.g., under SMS -> APIs)
# Replace placeholders with your actual values.
# Your Service Plan ID (or Campaign ID - verify term)
SINCH_SERVICE_PLAN_ID=YOUR_SERVICE_PLAN_ID
# Your API Token for the Service Plan ID
SINCH_API_TOKEN=YOUR_API_TOKEN
# Your provisioned Sinch Number (use E.164 format, e.g., +1xxxxxxxxxx)
SINCH_NUMBER=YOUR_SINCH_PHONE_NUMBER
# --- Sinch API Configuration ---
# CRITICAL: Verify the correct API base URL and structure in the official Sinch documentation
# for your specific region (us, eu, au, etc.) and API product version.
# The structure might require the service plan ID in the path as shown, or it might be different.
# Example structure (VERIFY THIS):
SINCH_API_BASE_URL=https://us.mms.api.sinch.com/v1/services/YOUR_SERVICE_PLAN_ID
# --- Server Configuration ---
# Port your Express server will listen on
PORT=3000
# Optional: Logging level (e.g., 'info', 'debug', 'warn', 'error')
# LOG_LEVEL=info
SINCH_SERVICE_PLAN_ID
: Found on your Sinch Customer Dashboard. The exact name and location might vary. ReplaceYOUR_SERVICE_PLAN_ID
with your actual ID.SINCH_API_TOKEN
: Found on the Sinch Customer Dashboard, often near the Service Plan ID configuration. ReplaceYOUR_API_TOKEN
with your actual token.SINCH_NUMBER
: A virtual number assigned to your Sinch account, capable of sending MMS. Use the E.164 international format (e.g.,+12223334444
). ReplaceYOUR_SINCH_PHONE_NUMBER
with your actual Sinch number.SINCH_API_BASE_URL
: The base URL for the Sinch MMS API. Crucially, verify the correct URL and structure in the official Sinch documentation. It can vary based on region, API version, and whether the Service Plan ID is part of the path. The example provided is common but requires verification. ReplaceYOUR_SERVICE_PLAN_ID
within the URL if this structure is correct for your API.PORT
: The port your Express server will listen on.
Step 5: Set Up Project Structure
Create the following basic structure:
sinch-mms-sender/
├── node_modules/
├── public/ # (Optional: for static files if needed)
├── src/
│ ├── controllers/ # Request/response handling
│ │ └── mmsController.js
│ ├── services/ # Business logic (Sinch interaction)
│ │ └── sinchService.js
│ ├── routes/ # API route definitions
│ │ └── mmsRoutes.js
│ ├── app.js # Express app configuration
│ └── server.js # Server entry point
├── .env # Environment variables (DO NOT COMMIT)
├── .gitignore # Git ignore file
├── package.json
└── package-lock.json
Create the src
directory and the subdirectories/files within it.
Step 6: Create .gitignore
Create a .gitignore
file in the project root to prevent committing sensitive files and unnecessary directories.
# .gitignore
node_modules/
.env
npm-debug.log
*.log
Step 7: Add Run Scripts to package.json
Modify the scripts
section in your package.json
:
{
"scripts": {
"start": "node src/server.js",
"dev": "nodemon src/server.js",
"test": "echo \"Error: no test specified\" && exit 1"
}
}
(Note: The test
script is a placeholder. It will be updated in Section 13 when testing with Jest is introduced.)
npm start
: Runs the application using Node.npm run dev
: Runs the application usingnodemon
, which automatically restarts the server on file changes.
2. Implementing Core Functionality (Sinch Service)
This service will encapsulate the logic for interacting with the Sinch MMS API.
File: src/services/sinchService.js
// src/services/sinchService.js
const axios = require('axios');
// Load environment variables
require('dotenv').config();
const SINCH_SERVICE_PLAN_ID = process.env.SINCH_SERVICE_PLAN_ID;
const SINCH_API_TOKEN = process.env.SINCH_API_TOKEN;
const SINCH_API_BASE_URL = process.env.SINCH_API_BASE_URL; // Assumes base URL includes service ID path per .env example
if (!SINCH_SERVICE_PLAN_ID || !SINCH_API_TOKEN || !SINCH_API_BASE_URL) {
console.error(""Error: Missing Sinch credentials or API base URL in .env file. Please check SINCH_SERVICE_PLAN_ID, SINCH_API_TOKEN, and SINCH_API_BASE_URL."");
process.exit(1); // Stop the application if critical config is missing
}
// Construct the specific endpoint for the sendmms action
// **VERIFY THIS ENDPOINT STRUCTURE** against the official Sinch MMS JSON API documentation for your account/region.
// This assumes the base URL already contains '/v1/services/{service_plan_id}'
const SEND_MMS_ENDPOINT = `${SINCH_API_BASE_URL}/actions/sendmms`;
/**
* Sends an MMS message using the Sinch MMS JSON API.
*
* @param {string} to The recipient's phone number in E.164 international format (e.g., +1...).
* @param {string} from The Sinch phone number sending the message (E.164 format).
* @param {string} subject The subject line for the MMS (max 80 chars, 40 recommended).
* @param {Array<object>} slides An array of slide objects. Structure MUST match Sinch API docs.
* Each slide can contain text and/or one media type (image, audio, video, etc.).
* @param {string} [fallbackText] Optional text for the fallback SMS if MMS fails or is disabled. Required if fallback isn't explicitly disabled.
* @param {object} [options] Optional parameters like clientReference, disableFallbackSms, etc. (Refer to Sinch Docs).
* @returns {Promise<object>} The success response data from the Sinch API (contains tracking-id).
* @throws {Error} If the API request fails or returns an error status.
*/
const sendMms = async (to, from, subject, slides, fallbackText, options = {}) => {
console.log(`Attempting to send MMS to ${to} from ${from}`);
// Basic validation
if (!to || !from || !subject || !Array.isArray(slides) || slides.length === 0) {
throw new Error('Missing required parameters: to, from, subject, or non-empty slides array.');
}
if (!to.startsWith('+') || !from.startsWith('+')) {
console.warn(`Warning: Phone numbers should be in E.164 format (start with +). To: ${to}, From: ${from}`);
// Consider throwing an error here in production for strict validation
}
if (slides.length > 8) {
console.warn('Warning: Maximum of 8 slides recommended per Sinch guidelines.');
}
// Ensure fallbackText is provided if fallback SMS is enabled (default behavior)
const disableFallbackSms = options.disableFallbackSms === true;
if (!disableFallbackSms && !fallbackText) {
// Fallback is enabled by default, so text is required unless explicitly disabled.
throw new Error('fallbackText is required when disableFallbackSms is false (or not provided).');
}
// **VERIFY PAYLOAD STRUCTURE** against official Sinch MMS JSON API documentation.
const payload = {
action: 'sendmms',
'service-id': SINCH_SERVICE_PLAN_ID, // May or may not be required depending on API structure/endpoint used
to: to,
from: from,
'message-subject': subject,
slide: slides, // Array of slide objects - structure is critical
// --- Optional Parameters (Verify names and values in Sinch Docs) ---
'fallback-sms-text': disableFallbackSms ? undefined : fallbackText, // Only include if fallback is enabled
'disable-fallback-sms': options.disableFallbackSms || false, // Default: false (fallback enabled)
'disable-fallback-sms-link': options.disableFallbackSmsLink || false, // Default: false
// 'fallback-sms-link-expiration': options.fallbackLinkExpiration, // Example: ISO8601 Date String
// 'mms-expiry-timestamp': options.mmsExpiryTimestamp, // Example: ISO8601 Date String
'client-reference': options.clientReference, // Your internal reference ID (max 64 chars recommended)
'cache-content': options.cacheContent === undefined ? true : options.cacheContent, // Default: true (Sinch may cache media)
};
// Remove undefined optional keys to keep payload clean
Object.keys(payload).forEach(key => payload[key] === undefined && delete payload[key]);
console.log('Constructed Sinch Payload:', JSON.stringify(payload, null, 2)); // Log payload for debugging
try {
// **VERIFY AUTHENTICATION METHOD** (Bearer Token is common, but check Sinch Docs)
const response = await axios.post(SEND_MMS_ENDPOINT, payload, {
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Authorization': `Bearer ${SINCH_API_TOKEN}` // Standard Bearer token auth
},
timeout: 15000 // Set a reasonable timeout (e.g., 15 seconds) for the API call
});
console.log('Sinch API Response Status:', response.status);
console.log('Sinch API Response Data:', JSON.stringify(response.data, null, 2));
// Sinch often returns 200 OK even for queued messages, but check response body for success indicator if available
// The exact success condition might vary based on the API version. This checks for a common pattern.
if (response.data && response.data.status === 'success' && response.data['tracking-id']) {
console.log(`MMS successfully queued with Sinch. Tracking ID: ${response.data['tracking-id']}`);
return response.data; // Contains tracking-id etc.
} else {
// Handle cases where Sinch might return 200 but indicate failure/issue in the body, or success structure differs
console.error('Sinch API response body indicates potential issue:', response.data);
throw new Error(`Sinch API did not return expected success status or tracking-id in response body: ${JSON.stringify(response.data)}`);
}
} catch (error) {
console.error('Error sending MMS via Sinch:', error.message);
let errorMessage = 'Failed to send MMS via Sinch API.';
if (error.response) {
// The request was made and the server responded with a status code outside the 2xx range
console.error('Sinch API Error Status:', error.response.status);
console.error('Sinch API Error Headers:', error.response.headers);
console.error('Sinch API Error Data:', JSON.stringify(error.response.data, null, 2));
// Include details from Sinch error response if available
const errorDetails = error.response.data ? `: ${JSON.stringify(error.response.data)}` : '';
errorMessage = `Sinch API request failed with status ${error.response.status}${errorDetails}`;
} else if (error.request) {
// The request was made but no response was received
console.error('Sinch API No Response Received. Request details:', error.request);
errorMessage = 'No response received from Sinch API. Check network connectivity and endpoint URL.';
} else {
// Something happened in setting up the request that triggered an Error
console.error('Axios Request Setup Error:', error.message);
errorMessage = `Error setting up request to Sinch API: ${error.message}`;
}
// Re-throw a more informative error to be handled by the controller
throw new Error(errorMessage);
}
};
module.exports = {
sendMms,
};
Explanation:
- Dependencies & Config: Imports
axios
anddotenv
. Loads environment variables and checks for essential Sinch credentials and the base URL, exiting if missing. - Endpoint Construction: Defines the full URL for the
sendmms
action based on theSINCH_API_BASE_URL
. Includes a prominent warning to verify this endpoint against official documentation. sendMms
Function:- Takes required parameters (
to
,from
,subject
,slides
) and optionalfallbackText
andoptions
. - Performs basic input validation (required fields, E.164 format hint, slide count).
- Validates that
fallbackText
is present if fallback SMS is not explicitly disabled. - Constructs the JSON
payload
according to a common Sinch MMS JSON API specification. Includes a warning to verify the payload structure against official documentation. Handles common optional fields and cleans undefined values. - Uses
axios.post
to send the request. - Sets necessary headers:
Content-Type
andAuthorization
(Bearer token - verify this method). - Includes robust error handling for different failure scenarios (API errors with status codes, network errors, request setup errors), logging detailed information.
- Logs the constructed payload and the received response for debugging.
- Checks the response body for a success indicator (e.g.,
status: 'success'
andtracking-id
) as 200 OK might not guarantee acceptance. This check might need adjustment based on the specific API response structure. - Returns the success response data from Sinch upon successful queueing.
- Throws detailed errors for upstream handling.
- Takes required parameters (
3. Building the API Layer (Routes and Controller)
Now, let's create the Express route and controller to expose our MMS sending functionality.
File: src/routes/mmsRoutes.js
// src/routes/mmsRoutes.js
const express = require('express');
const mmsController = require('../controllers/mmsController');
// Optional: Import validation middleware if using express-validator (see Section 7)
// const { validateSendMms } = require('../middleware/validators'); // Example path
const router = express.Router();
// Define the route for sending MMS
// POST /api/mms/send
// Optional: Add validation middleware before the controller
// router.post('/send', validateSendMms, mmsController.handleSendMms);
router.post('/send', mmsController.handleSendMms);
module.exports = router;
File: src/controllers/mmsController.js
// src/controllers/mmsController.js
const sinchService = require('../services/sinchService');
// Load environment variables to get the sender number
require('dotenv').config();
const SINCH_NUMBER = process.env.SINCH_NUMBER;
if (!SINCH_NUMBER) {
console.error("FATAL: SINCH_NUMBER is not defined in .env file. Cannot determine sender number.");
process.exit(1);
}
/**
* Handles the incoming API request to send an MMS.
* Expects request body adhering to the defined contract, e.g.:
* {
* "to": "+1xxxxxxxxxx", // E.164 format
* "subject": "Your Media Update", // Max 80 chars
* "slides": [ // 1-8 slides, structure defined by Sinch API docs
* { "image": { "url": "https://your-cdn.com/image.jpg" }, "message-text": "Check out this picture!" },
* { "video": { "url": "https://your-cdn.com/video.mp4" }, "message-text": "And this video too." }
* ],
* "fallbackText": "View your media update here: [link inserted by Sinch if enabled]", // Required if fallback not disabled
* "clientReference": "internal-order-123", // Optional, max 64 chars
* "disableFallbackSms": false, // Optional, default false
* "disableFallbackSmsLink": false // Optional, default false
* }
*/
const handleSendMms = async (req, res) => {
// Extract data from request body
const { to, subject, slides, fallbackText, clientReference,
disableFallbackSms, disableFallbackSmsLink /* Add other options if needed */ } = req.body;
const from = SINCH_NUMBER; // Use the number configured in .env as the sender
// Basic Input Validation (Strongly recommend using a library like express-validator for production - see Section 7)
if (!to || !subject || !Array.isArray(slides) || slides.length === 0) {
console.warn(`Validation failed for incoming request: Missing required fields.`);
return res.status(400).json({
status: 'error',
message: 'Bad Request: Missing required fields: `to`, `subject`, `slides` (must be a non-empty array).',
});
}
// Add more specific validation here or use middleware (e.g., check 'to' format, subject length, slide content structure)
// Prepare options object for the service layer
const serviceOptions = {
clientReference,
disableFallbackSms,
disableFallbackSmsLink,
// Pass through other relevant options extracted from req.body
};
try {
console.log(`Processing request to send MMS via Sinch: To=${to}, From=${from}, Subject=${subject}`);
const result = await sinchService.sendMms(
to,
from,
subject,
slides,
fallbackText, // Pass fallbackText explicitly
serviceOptions // Pass the options bundle
);
// If sendMms resolves, it means the request was accepted by Sinch (or seemed to be)
console.log(`Successfully queued MMS to ${to} via Sinch. Tracking ID: ${result['tracking-id']}`);
// Respond with the success status and the data received from Sinch (including tracking ID)
res.status(200).json({
status: 'success',
message: 'MMS request accepted by Sinch and queued for delivery.',
data: result, // Send back the full Sinch success response
});
} catch (error) {
// Log the detailed error caught from the service layer
console.error(`Failed to process MMS request for ${to}:`, error.message); // error.message now contains detailed info
// Determine appropriate status code based on error type if possible
// If the error came from Sinch API (indicated by the service layer error message), use 400 or a more specific code if known.
// Otherwise, use 500 for internal server errors.
const statusCode = error.message.includes("Sinch API request failed") || error.message.includes("required parameters") || error.message.includes("fallbackText is required") ? 400 : 500;
res.status(statusCode).json({
status: 'error',
message: 'Failed to send MMS.',
// Provide the specific error message back (be cautious about leaking too much detail in production)
error: error.message,
});
}
};
module.exports = {
handleSendMms,
};
Explanation:
- Routes (
mmsRoutes.js
): Defines a single POST route/api/mms/send
that maps to thehandleSendMms
controller function. Includes a commented-out example showing where validation middleware (Section 7) would go. - Controller (
mmsController.js
):- Imports the
sinchService
. - Loads the
SINCH_NUMBER
from environment variables to use as thefrom
number, exiting if not found. - Extracts expected fields (
to
,subject
,slides
,fallbackText
, and optional fields likeclientReference
,disableFallbackSms
) from the request body (req.body
). - Performs basic validation on required fields. Strongly recommends using
express-validator
(Section 7) for robust validation in production. - Bundles optional parameters into a
serviceOptions
object. - Calls
sinchService.sendMms
with the extracted and validated data. - Handles success: Logs the success and responds with a 200 status and the Sinch API success response (which should include the
tracking-id
). - Handles errors: Catches errors thrown by the service layer, logs the detailed error message, determines an appropriate HTTP status code (400 for client-side/API input errors, 500 for internal server issues), and responds with a JSON error message including the error details from the service.
- Imports the
4. Integrating with Necessary Third-Party Services (Express Setup)
Let's tie everything together in our main Express application file.
File: src/app.js
// src/app.js
const express = require('express');
const mmsRoutes = require('./routes/mmsRoutes');
// Optional: Import security middleware like rate limiter (see Section 7)
// const { apiLimiter } = require('./middleware/rateLimiter');
// Optional: Import request logger like pino-http (see Section 5)
// const { httpLogger } = require('./utils/logger');
// Load environment variables early, especially for PORT and NODE_ENV
require('dotenv').config();
const app = express();
// --- Core Middlewares ---
// Request Logging (Replace with a structured logger like pino in production - See Section 5)
// app.use(httpLogger); // Example using pino-http
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] Incoming Request: ${req.method} ${req.originalUrl}`);
next(); // Pass control to the next middleware/route handler
});
// JSON Body Parser: Crucial for reading req.body from JSON payloads
app.use(express.json({ limit: '1mb' })); // Set a reasonable payload size limit
// URL-encoded Body Parser (optional, if you expect form data)
app.use(express.urlencoded({ extended: true }));
// --- Security Middlewares (Examples - See Section 7) ---
// app.use('/api/', apiLimiter); // Apply rate limiting to API routes
// Add other security middleware here (helmet, cors if needed)
// --- API Routes ---
app.use('/api/mms', mmsRoutes); // Mount the MMS routes under /api/mms
// --- Health Check Endpoint ---
app.get('/health', (req, res) => {
// Basic health check, can be expanded later
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
// --- Error Handling Middlewares (Must be defined LAST) ---
// Catch-all for 404 Not Found errors (requests falling through routes)
app.use((req, res, next) => {
res.status(404).json({ status: 'error', message: `Resource not found: ${req.originalUrl}` });
});
// Global Error Handler: Catches errors passed via next(error) or thrown in async route handlers
// Needs 4 arguments to be recognized by Express as an error handler
app.use((err, req, res, next) => {
// Log the error internally (use a proper logger in production)
console.error(""Unhandled Error:"", err.stack || err);
// Determine status code: Use error's status if set, otherwise default to 500
const statusCode = typeof err.status === 'number' ? err.status : 500;
// Send generic error response to the client
res.status(statusCode).json({
status: 'error',
message: err.message || 'Internal Server Error',
// Optionally include stack trace in development mode ONLY
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});
module.exports = app;
File: src/server.js
// src/server.js
const app = require('./app');
// Ensure environment variables are loaded (dotenv.config() might be called in app.js already)
require('dotenv').config();
const PORT = process.env.PORT || 3000; // Use PORT from .env or default to 3000
const NODE_ENV = process.env.NODE_ENV || 'development';
const server = app.listen(PORT, () => { // Store server instance for graceful shutdown
console.log(`Server running in ${NODE_ENV} mode on port ${PORT}`);
console.log(""--- Configuration Check ---"");
console.log(`Sinch Service Plan ID configured: ${process.env.SINCH_SERVICE_PLAN_ID ? 'Yes' : 'NO - Check .env!'}`);
console.log(`Sinch API Token configured: ${process.env.SINCH_API_TOKEN ? 'Yes' : 'NO - Check .env!'}`);
console.log(`Sinch Sender Number configured: ${process.env.SINCH_NUMBER || 'NO - Check .env!'}`);
console.log(`Sinch API Base URL configured: ${process.env.SINCH_API_BASE_URL || 'NOT SET - Check .env!'}`);
console.log(""---------------------------"");
if (!process.env.SINCH_SERVICE_PLAN_ID || !process.env.SINCH_API_TOKEN || !process.env.SINCH_NUMBER || !process.env.SINCH_API_BASE_URL) {
console.error(""FATAL: One or more required Sinch environment variables are missing. Application might not function correctly."");
}
});
// Optional: Graceful shutdown handling
process.on('SIGTERM', () => {
console.log('SIGTERM signal received: closing HTTP server');
// Add cleanup logic here (e.g., close database connections)
server.close(() => {
console.log('HTTP server closed');
process.exit(0);
});
});
Explanation:
app.js
:- Initializes the Express application (
app
). - Loads
dotenv
early. - Includes essential middleware: a basic request logger (recommend replacing with
pino-http
or similar for production),express.json()
for parsing JSON bodies, andexpress.urlencoded()
(optional). - Includes placeholders for security middleware (rate limiting, etc. - see Section 7).
- Mounts the
mmsRoutes
under the/api/mms
path prefix. - Adds a basic
/health
check endpoint for monitoring. - Includes a 404 handler for requests that don't match any route.
- Implements a global error handler middleware (must have 4 arguments:
err, req, res, next
) to catch unhandled errors from routes ornext(error)
calls. It logs the error and sends a standardized JSON error response to the client, avoiding leaking stack traces in production.
- Initializes the Express application (
server.js
:- Imports the configured
app
fromapp.js
. - Loads
dotenv
again (safe if already loaded) to ensure environment variables are available here too. - Gets the
PORT
andNODE_ENV
from environment variables (with defaults). - Starts the Express server using
app.listen
and stores the server instance. - Logs essential configuration status on startup for quick verification, including warnings if critical Sinch variables are missing.
- Includes optional basic graceful shutdown handling for
SIGTERM
, using the storedserver
instance.
- Imports the configured
5. Implementing Proper Error Handling, Logging, and Retry Mechanisms
We've built foundational error handling. Let's refine logging and consider retries.
Error Handling Strategy (Recap):
sinchService.js
: Catchesaxios
errors during the API call. Logs detailed Sinch API error responses. Throws a new, informativeError
object containing relevant details.mmsController.js
: Uses atry...catch
block to catch errors fromsinchService
. Logs a summary. Responds to the client with an appropriate HTTP status (400 for known client/API errors, 500 for unexpected internal errors) and a JSON error message derived from the caught error.app.js
(Global Handler): Catches any errors missed by route handlers or thrown unexpectedly. Logs the full stack trace (server-side) and sends a generic 500 response to the client.
Logging Enhancements (Production):
Standard console.log
/error
is okay for development, but insufficient for production. Use a dedicated, structured logging library like pino
.
- Benefits: Structured JSON output (machine-readable), log levels (info, debug, warn, error, fatal), faster performance, easy integration with log shippers (Fluentd, Logstash) and management systems (ELK, Datadog, Splunk).
- Implementation:
- Install:
npm install pino pino-http
- Configure: Create a logger instance (e.g., in
src/utils/logger.js
). - Integrate
pino-http
middleware inapp.js
for automatic request/response logging. - Replace
console.log
/error
calls throughout your services and controllers withlogger.info()
,logger.warn()
,logger.error()
, etc. Pass error objects directly tologger.error(error, "Optional message")
for stack trace logging.
- Install:
Retry Mechanisms:
Network issues or temporary Sinch API unavailability (e.g., 5xx errors) can occur. Retrying failed requests can improve resilience.
- When to Retry: Typically retry on transient errors: network timeouts, DNS issues, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout. Do not retry on permanent errors like 4xx (Bad Request, Unauthorized, Not Found) as the request is unlikely to succeed without changes.
- Strategy: Implement exponential backoff (wait longer between each retry) to avoid overwhelming the API.
- Library:
axios-retry
simplifies this.
Example using axios-retry
:
- Install:
npm install axios-retry
- Modify
src/services/sinchService.js
:
// src/services/sinchService.js
const axios = require('axios');
// Use default import for axios-retry
const axiosRetry = require('axios-retry').default;
// ... other imports and config (SINCH_SERVICE_PLAN_ID, TOKEN, BASE_URL, SEND_MMS_ENDPOINT) ...
// Create an Axios instance specifically for Sinch requests
const sinchApiClient = axios.create({
baseURL: SINCH_API_BASE_URL, // Set base URL here if consistent
timeout: 15000, // Default timeout for requests
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Authorization': `Bearer ${SINCH_API_TOKEN}` // Set common headers
}
});
// Configure axios-retry on the instance
axiosRetry(sinchApiClient, {
retries: 3, // Number of retries
retryDelay: (retryCount) => {
console.log(`Retry attempt: ${retryCount}`);
return retryCount * 1000; // Exponential backoff (1s, 2s, 3s)
},
retryCondition: (error) => {
// Retry on network errors or specific server errors (5xx)
return (
axiosRetry.isNetworkOrIdempotentRequestError(error) ||
(error.response && error.response.status >= 500 && error.response.status <= 599)
);
}_
onRetry: (retryCount_ error_ requestConfig) => {
console.warn(`Retrying request to ${requestConfig.url} due to error: ${error.message}. Attempt ${retryCount}`);
}
});
// ... inside sendMms function ...
try {
// Use the configured axios instance instead of axios.post directly
// Note: If SEND_MMS_ENDPOINT was constructed using SINCH_API_BASE_URL,
// you might need to adjust the URL passed here (e.g., just the path part).
// Assuming SEND_MMS_ENDPOINT is the full URL:
// const response = await sinchApiClient.post(SEND_MMS_ENDPOINT, payload);
// OR if SINCH_API_BASE_URL is set in the instance and SEND_MMS_ENDPOINT
// contains the full URL, extract the path:
const endpointPath = new URL(SEND_MMS_ENDPOINT).pathname; // Requires full URL in SEND_MMS_ENDPOINT
const response = await sinchApiClient.post(endpointPath, payload);
// ... rest of the success/error handling logic ...
} catch (error) {
// Error handling remains largely the same, but logs might show retry attempts
console.error('Error sending MMS via Sinch (after potential retries):', error.message);
// ... rest of error handling ...
}
// ... rest of the file ...
(Note: The axios-retry
example above is illustrative and requires careful integration with the existing sendMms
function, particularly regarding how the URL/endpoint is passed to the sinchApiClient.post
method based on whether baseURL
is set on the instance.)