This guide provides a step-by-step walkthrough for building a robust Node.js application suitable for deployment using the Fastify framework to send Multimedia Messaging Service (MMS) messages via the MessageBird API. Note that MessageBird's MMS sending capabilities are primarily focused on the US and Canada.
We will build a simple API endpoint that accepts recipient information, message content (text and/or media URLs), and uses MessageBird to deliver the MMS. We will also cover essential aspects like configuration, error handling, security, receiving status updates, testing, and deployment.
Project Overview and Goals
Goal: Create a reliable and scalable service to send MMS messages programmatically.
Problem Solved: Automates the process of sending rich media messages (images, videos, etc.) to users in the US and Canada, enabling richer communication for notifications, marketing, or user engagement compared to SMS alone.
Technologies:
- Node.js: A popular JavaScript runtime for building server-side applications.
- Fastify: A high-performance, low-overhead web framework for Node.js, known for its speed and excellent developer experience.
- MessageBird MMS API: A communication API platform enabling sending and receiving MMS messages.
- axios: A promise-based HTTP client for making requests to the MessageBird API.
- dotenv: A module to load environment variables from a
.env
file intoprocess.env
. - pino-pretty: A development tool to make Fastify's default JSON logs human-readable.
- Docker (Optional): For containerizing the application for consistent deployment.
System Architecture:
graph LR
Client[Client Application / curl] -->|POST /send-mms (JSON Payload)| FastifyApp[Fastify Node.js App];
FastifyApp -->|POST Request (API Key Auth)| MessageBird[MessageBird MMS API];
MessageBird -->|MMS Delivery| Recipient[Recipient Phone];
MessageBird -->|GET Status Update (Webhook)| FastifyApp;
FastifyApp -->|Log Status| Logs[Application Logs / Monitoring];
subgraph ""Our Application""
FastifyApp
Logs
end
subgraph ""External Services""
MessageBird
Recipient
end
Prerequisites:
- Node.js (v18 or later recommended) and npm/yarn.
- A MessageBird account with a Live API Access Key.
- An MMS-enabled virtual mobile number (VMN) purchased through MessageBird, specifically for the US or Canada. This will be your
originator
number. - Media files (images, etc.) hosted on publicly accessible URLs. MessageBird needs to fetch the media from these URLs.
- Basic familiarity with Node.js, APIs, and the command line.
Expected Outcome: A Fastify application with a secure endpoint (/send-mms
) capable of accepting MMS requests and relaying them to the MessageBird API for delivery. The application will also have an endpoint (/mms-status
) to receive delivery status updates from MessageBird.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
mkdir fastify-messagebird-mms cd fastify-messagebird-mms
-
Initialize Node.js Project: This creates a
package.json
file.npm init -y
(Use
yarn init -y
if you prefer Yarn) -
Install Dependencies: We need Fastify for the web server,
axios
to call the MessageBird API, anddotenv
for managing environment variables.npm install fastify axios dotenv
(Use
yarn add fastify axios dotenv
for Yarn) -
Install Development Dependencies:
pino-pretty
helps format logs nicely during development.npm install --save-dev pino-pretty
(Use
yarn add --dev pino-pretty
for Yarn) -
Create Project Structure: Create the basic files and directories.
touch server.js .env .env.example .gitignore
server.js
: Main application code..env
: Stores sensitive credentials (API keys, etc.). Do not commit this file..env.example
: A template showing required environment variables. Commit this file..gitignore
: Specifies files/directories Git should ignore.
-
Configure
.gitignore
: Addnode_modules
and.env
to your.gitignore
file to prevent committing them.# .gitignore node_modules .env npm-debug.log
-
Set up
package.json
Scripts: Add scripts topackage.json
for easily running the application.// package.json (partial) { "scripts": { "start": "node server.js", "dev": "node server.js | pino-pretty" } }
npm start
: Runs the server in production mode (standard logs).npm run dev
: Runs the server with human-readable logs viapino-pretty
.
2. Configuration
Securely managing credentials and settings is crucial. We'll use environment variables loaded via dotenv
.
-
Define Environment Variables (
.env.example
): List the required variables in.env.example
.# .env.example # MessageBird Configuration MESSAGEBIRD_API_KEY=YOUR_MESSAGEBIRD_LIVE_API_KEY MESSAGEBIRD_ORIGINATOR=YOUR_MESSAGEBIRD_MMS_ENABLED_NUMBER_E164 # Application Configuration PORT=3000 HOST=0.0.0.0 FASTIFY_API_KEY=YOUR_SECURE_API_KEY_FOR_THIS_APP LOG_LEVEL=info
-
Populate
.env
File: Create a.env
file (or copy.env.example
to.env
) and fill in the actual values:MESSAGEBIRD_API_KEY
: Your Live Access Key from the MessageBird Dashboard (Developers > API access).MESSAGEBIRD_ORIGINATOR
: Your MMS-enabled US or Canadian number purchased from MessageBird, in E.164 format (e.g.,+12015550123
). Crucially, this number must be explicitly enabled for MMS within your MessageBird settings.PORT
: The port your Fastify server will listen on (default: 3000).HOST
: The host address (default:0.0.0.0
to listen on all available network interfaces).FASTIFY_API_KEY
: A secret key you define. Clients calling your/send-mms
endpoint will need to provide this key for authentication. Generate a strong, random string for this.LOG_LEVEL
: Controls log verbosity (e.g.,info
,debug
,warn
,error
).
-
Load Environment Variables in
server.js
: At the very top of yourserver.js
, load the variables.// server.js require('dotenv').config();
3. Implementing the MMS Sending Route
Now, let's build the core logic in server.js
.
-
Basic Fastify Server Setup: Initialize Fastify and configure basic logging.
// server.js require('dotenv').config(); const Fastify = require('fastify'); const axios = require('axios'); const fastify = Fastify({ logger: { level: process.env.LOG_LEVEL || 'info', // Use pino-pretty only in development for readability ...(process.env.NODE_ENV !== 'production' && { transport: { target: 'pino-pretty', options: { translateTime: 'HH:MM:ss Z', ignore: 'pid,hostname', }, }, }), }, }); // Simple health check route fastify.get('/health', async (request, reply) => { return { status: 'ok' }; }); // --- MMS Sending Route will go here --- // --- Status Webhook Route will go here --- // Start the server const start = async () => { try { const port = parseInt(process.env.PORT || '3000', 10); const host = process.env.HOST || '0.0.0.0'; await fastify.listen({ port, host }); fastify.log.info(`Server listening on ${fastify.server.address().port}`); } catch (err) { fastify.log.error(err); process.exit(1); } }; start();
-
Add Authentication Hook: We'll protect our
/send-mms
route using a simple API key check via a Fastify hook.// server.js (Add this before defining routes that need protection) fastify.decorate('authenticate', async function (request, reply) { const apiKey = request.headers['x-api-key']; if (!apiKey || apiKey !== process.env.FASTIFY_API_KEY) { fastify.log.warn('Authentication failed: Invalid or missing API key'); reply.code(401).send({ error: 'Unauthorized' }); return Promise.reject(new Error('Unauthorized')); // Stop processing } });
-
Define the
/send-mms
Route: This route will handle POST requests to send MMS messages.// server.js (Add this after the authenticate hook) const sendMmsSchema = { body: { type: 'object', required: [], // Dynamically determined by oneOf/anyOf properties: { recipient: { type: 'string', description: 'Single recipient phone number in E.164 format.' }, recipients: { type: 'array', items: { type: 'string' }, description: 'Array of recipient phone numbers in E.164 format. Max 50 per request. Use either `recipient` or `recipients`, not both in the same call for clarity.' }, subject: { type: 'string', maxLength: 256, description: 'MMS subject line.' }, body: { type: 'string', maxLength: 2000, description: 'Text body of the MMS.' }, mediaUrls: { type: 'array', items: { type: 'string', format: 'url' }, maxItems: 10, description: 'Array of public URLs for media attachments (max 10).' }, reference: { type: 'string', description: 'Optional client reference string.' }, scheduledDatetime: { type: 'string', format: 'date-time', description: 'Optional schedule time (RFC3339 format: YYYY-MM-DDTHH:mm:ssZ).' } }, // Ensure at least body or mediaUrls is provided anyOf: [ { required: ['body'] }, { required: ['mediaUrls'] } ], // Ensure only one recipient definition method is used and at least one is present oneOf: [ { required: ['recipient'], properties: { recipient: { type: 'string'} }, not: { required: ['recipients']} }, { required: ['recipients'], properties: { recipients: { type: 'array'} }, not: { required: ['recipient']} } ] }, response: { 200: { description: 'MMS message accepted by MessageBird for delivery.', type: 'object', properties: { id: { type: 'string' }, href: { type: 'string' }, direction: { type: 'string' }, originator: { type: 'string' }, subject: { type: ['string', 'null'] }, // Subject can be null if not sent body: { type: ['string', 'null'] }, // Body can be null if not sent mediaUrls: { type: 'array', items: { type: 'string' } }, reference: { type: ['string', 'null'] }, scheduledDatetime: { type: ['string', 'null'], format: 'date-time' }, createdDatetime: { type: 'string', format: 'date-time' }, recipients: { type: 'object' } // Detailed recipient status omitted for brevity } }, 400: { description: 'Bad Request - Invalid input data or validation failure.', type: 'object', properties: { error: { type: 'string', example: 'Bad Request' }, message: { type: 'string' }, details: { type: 'object' } } }, 401: { description: 'Unauthorized - Missing or invalid API key for this service.', type: 'object', properties: { error: { type: 'string', example: 'Unauthorized' } } }, 500: { description: 'Internal Server Error - Failure during processing or communicating with MessageBird.', type: 'object', properties: { error: { type: 'string', example: 'Internal Server Error'}, message: { type: 'string'}, details: { type: 'object'} } }, 503: { description: 'Service Unavailable - Could not reach MessageBird API.', type: 'object', properties: { error: { type: 'string', example: 'Service Unavailable'}, message: { type: 'string'} } } } }; fastify.post('/send-mms', { schema: sendMmsSchema, preHandler: [fastify.authenticate] }, async (request, reply) => { const { recipient, recipients, subject, body, mediaUrls, reference, scheduledDatetime } = request.body; // Determine the recipient list const recipientList = recipients || (recipient ? [recipient] : []); // Handle both cases if (recipientList.length === 0) { reply.code(400).send({ error: 'Bad Request', message: 'At least one recipient is required.' }); return; } if (recipientList.length > 50) { reply.code(400).send({ error: 'Bad Request', message: 'Maximum of 50 recipients allowed per request.' }); return; } const messageBirdPayload = { originator: process.env.MESSAGEBIRD_ORIGINATOR, recipients: recipientList, subject: subject, body: body, mediaUrls: mediaUrls, reference: reference, scheduledDatetime: scheduledDatetime }; // Remove undefined/null fields cleanly before sending Object.keys(messageBirdPayload).forEach(key => (messageBirdPayload[key] === undefined || messageBirdPayload[key] === null) && delete messageBirdPayload[key]); try { fastify.log.info({ msg: 'Sending MMS request to MessageBird', payload: messageBirdPayload }); const response = await axios.post('https://rest.messagebird.com/mms', messageBirdPayload, { headers: { 'Authorization': `AccessKey ${process.env.MESSAGEBIRD_API_KEY}`, 'Content-Type': 'application/json' // Explicitly set content type } }); fastify.log.info({ msg: 'MessageBird API response received', messageId: response.data.id, status: response.status }); reply.code(200).send(response.data); // Forward MessageBird's response } catch (error) { let statusCode = 500; let responseBody = { error: 'Internal Server Error', message: 'Failed to send MMS via MessageBird.', details: {} }; if (error.response) { // Request made, server responded with non-2xx status fastify.log.error({ msg: 'MessageBird API Error', status: error.response.status, data: error.response.data }); statusCode = error.response.status >= 500 ? 500 : 400; // Treat 4xx as 400, 5xx as 500 responseBody.message = `MessageBird API error: ${error.response.data?.errors?.[0]?.description || 'Unknown error'}`; responseBody.details = error.response.data; responseBody.error = statusCode === 400 ? 'Bad Request' : 'Service Error'; } else if (error.request) { // Request made, no response received fastify.log.error({ msg: 'MessageBird API No Response', error: error.message }); responseBody.message = 'No response received from MessageBird API.'; responseBody.error = 'Service Unavailable'; statusCode = 503; } else { // Error setting up the request fastify.log.error({ msg: 'MMS Sending Error', error: error.message }); responseBody.message = error.message; } reply.code(statusCode).send(responseBody); } });
Explanation:
- Schema: We define a schema using Fastify's built-in validation. This ensures incoming requests have the correct structure (
recipient
/recipients
,body
/mediaUrls
), types, and constraints (lengths, max items) before our handler logic runs. It also specifies the expected success (200) and error (400, 401, 500, 503) response formats.anyOf
ensures eitherbody
ormediaUrls
is present.oneOf
ensures onlyrecipient
orrecipients
is used and that at least one is provided. preHandler: [fastify.authenticate]
: This applies our authentication hook to this specific route, handling potential 401 errors.- Payload Construction: We build the JSON payload required by the MessageBird
/mms
endpoint, using theoriginator
from environment variables and data from the incoming request. We clean up undefined or null optional fields. We add checks for recipient limits and presence (returning 400). axios.post
: We make the POST request to MessageBird's API endpoint.- The
Authorization: AccessKey ...
header is crucial for authenticating with MessageBird. - We explicitly set
Content-Type: application/json
.
- The
- Response Handling: If the request to MessageBird is successful (2xx), we log it and forward MessageBird's response back to our client with a 200 status.
- Error Handling: If
axios
throws an error (network issue, non-2xx response from MessageBird), we catch it, log detailed information, determine an appropriate HTTP status code (400 for MessageBird client errors, 503 for timeouts, 500 for server errors), and send a structured error response back to our client.
- Schema: We define a schema using Fastify's built-in validation. This ensures incoming requests have the correct structure (
4. Handling Media Attachments
MessageBird does not accept direct file uploads via the /mms
endpoint. You must provide publicly accessible URLs.
- Hosting: Upload your media (images, videos, etc.) to a service like AWS S3, Google Cloud Storage, Cloudinary, or even a simple public web server.
- Public URLs: Ensure the URLs generated are publicly accessible without authentication. MessageBird's servers need to fetch the content from these URLs.
- Latency: The media URLs should respond quickly (within 5 seconds according to MessageBird docs). Use a Content Delivery Network (CDN) for better performance if needed.
- Size Limit: Each individual media file must be 1MB (1024KB) or less.
- Count Limit: A maximum of 10
mediaUrls
can be included in a single MMS request. - Supported Types: Refer to the MessageBird MMS API Documentation for the full list of supported
Content-Type
values. Common types likeimage/jpeg
,image/png
,image/gif
,video/mp4
,audio/mpeg
are generally supported.
Example mediaUrls
Array in Request Body:
{
""recipient"": ""+14155550100"",
""subject"": ""Check this out!"",
""mediaUrls"": [
""https://your-cdn.com/images/logo.png"",
""https://your-public-bucket.s3.amazonaws.com/videos/promo.mp4""
]
}
5. Error Handling, Logging, and Retries
- Error Handling: Our
/send-mms
route includes robust error handling for API calls to MessageBird. It catches different error types (API errors, network errors) and returns informative JSON responses with appropriate HTTP status codes (400, 401, 500, 503). Fastify's default error handler catches other unexpected errors. - Logging: Fastify's built-in logger (
pino
) is configured inserver.js
.- We log informational messages for successful requests/responses.
- We log detailed error messages, including status codes and data from MessageBird API errors.
- The
LOG_LEVEL
environment variable controls verbosity. Set todebug
for more detailed logs during troubleshooting. - In development (
npm run dev
), logs are human-readable thanks topino-pretty
. In production (npm start
), JSON logs are standard, which is better for log aggregation tools (like Datadog, Splunk, ELK stack).
- Retries: Implementing retries can improve resilience against transient network issues when calling MessageBird.
- Simple Retry (Conceptual): You could wrap the
axios.post
call in a simple loop or use a library likeasync-retry
. Be cautious with retries for non-idempotent POST requests – ensure MessageBird handles duplicate requests gracefully if a retry occurs after the initial request succeeded but the response was lost. Using a uniquereference
field might help MessageBird deduplicate, but verify this behavior. - Recommendation: For production, start without automatic retries in the application layer unless specifically needed. Rely on monitoring to detect failures and potentially trigger manual or externally orchestrated retries if necessary, especially given the potential cost implications of sending duplicate messages. MessageBird itself might retry delivery on its end.
- Simple Retry (Conceptual): You could wrap the
6. Security Considerations
- Input Validation: Done via Fastify's schema validation in the
/send-mms
route definition. This prevents malformed requests and basic injection attempts. - Authentication:
- Your API: The
/send-mms
endpoint is protected by thex-api-key
header check (fastify.authenticate
hook). EnsureFASTIFY_API_KEY
is strong and kept secret. - MessageBird API: The
MESSAGEBIRD_API_KEY
is sent securely via HTTPS in theAuthorization
header to MessageBird.
- Your API: The
- API Key Security: Never hardcode API keys in source code. Use environment variables (
.env
locally, secure configuration management in deployment). Rotate keys periodically. - Rate Limiting: Protect your API from abuse by adding rate limiting.
npm install @fastify/rate-limit
This applies a global rate limit. You can configure it per-route if needed. Adjust// server.js (Add near the top, after Fastify initialization) fastify.register(require('@fastify/rate-limit'), { max: 100, // Max requests per windowMs timeWindow: '1 minute' });
max
andtimeWindow
based on expected usage. - HTTPS: Always run your application behind HTTPS in production (usually handled by a load balancer or reverse proxy like Nginx).
- Dependency Security: Regularly update dependencies (
npm audit fix
oryarn audit
) to patch known vulnerabilities.
7. Receiving Status Updates (Webhooks)
MessageBird can notify your application about the delivery status of sent MMS messages via webhooks.
-
Configure Webhook URL in MessageBird:
- Navigate to your MessageBird Dashboard.
- Go to the "Numbers" section and select your MMS-enabled number.
- Find the settings for incoming messages or webhooks. Look for a field like "Status reports URL" or similar specific to MMS/SMS if available. Note: The exact location might vary.
- Enter the public URL where your Fastify application will be listening for status updates (e.g.,
https://your-app-domain.com/mms-status
). - Ensure the method expected by MessageBird matches your implementation (GET for status reports as per docs).
-
Create the Status Webhook Route in Fastify: MessageBird sends status updates as GET requests to your configured URL.
// server.js (Add this alongside other routes) fastify.get('/mms-status', async (request, reply) => { const { id, reference, recipient, status, statusDatetime } = request.query; // Log the received status update // In a real application, you would likely: // 1. Validate the request source (e.g., check IP, use signed requests if MessageBird supports it) // 2. Look up the message ID or reference in your database // 3. Update the message status in your database // 4. Trigger any necessary downstream actions based on the status (e.g., notify admins on failure) fastify.log.info({ msg: 'Received MMS status update', messageId: id, reference: reference, recipient: recipient, status: status, statusTimestamp: statusDatetime }); // IMPORTANT: Respond with 200 OK quickly! // MessageBird expects a 200 OK to acknowledge receipt. // Failure to respond promptly may cause MessageBird to retry the webhook. reply.code(200).send('OK'); });
Explanation:
- The route listens on
/mms-status
for GET requests. - It extracts the status parameters from the query string (
request.query
). - It logs the received status. Crucially, add validation and database updates here in a real-world scenario.
- It immediately sends a
200 OK
response. This is vital for acknowledging receipt to MessageBird.
- The route listens on
-
Making Webhooks Accessible:
- Local Development: Use a tool like
ngrok
(ngrok http 3000
) to expose your local server (running on port 3000) to the internet with a public URL (e.g.,https://<unique-id>.ngrok.io
). Use this ngrok URL in the MessageBird dashboard for testing. - Production: Deploy your application to a hosting provider (see Deployment section) so it has a stable public domain name or IP address.
- Local Development: Use a tool like
8. Testing and Verification
Thorough testing ensures your integration works correctly.
-
Unit Tests (Example using
tap
- Fastify's default):npm install --save-dev tap nock # nock for mocking HTTP
Add test script to
package.json
:// package.json (scripts section) { ""scripts"": { ""start"": ""node server.js"", ""dev"": ""node server.js | pino-pretty"", ""test"": ""tap test/**/*.test.js"" } }
Create a test file
test/routes/mms.test.js
:// test/routes/mms.test.js const { test } = require('tap'); const Fastify = require('fastify'); const nock = require('nock'); // For mocking HTTP requests const dotenv = require('dotenv'); const axios = require('axios'); // Import axios to be used within the route handler // Load test environment variables if needed (e.g., from a .env.test file) // Ensure required env vars are set for tests process.env.FASTIFY_API_KEY = 'test-api-key'; process.env.MESSAGEBIRD_API_KEY = 'mock_mb_key'; process.env.MESSAGEBIRD_ORIGINATOR = '+10000000000'; process.env.LOG_LEVEL = 'silent'; // Keep test output clean // Mock the authenticate decorator for testing routes in isolation function build(t) { const app = Fastify({ logger: { level: process.env.LOG_LEVEL } }); app.decorate('authenticate', async function (request, reply) { // In tests, we can bypass actual auth check or mock it if (request.headers['x-api-key'] !== process.env.FASTIFY_API_KEY) { reply.code(401).send({ error: 'Unauthorized' }); return Promise.reject(new Error('Unauthorized - Mock')); } }); // Register your route (assuming server.js exports the app or setup function) // Simplified example: Directly defining the route logic here for clarity // Ideally, import and register the actual route handler from server.js const sendMmsSchema = { /* ... include schema if testing validation ... */ }; // Simplified for brevity app.post('/send-mms', { // schema: sendMmsSchema, // Add schema if needed preHandler: [app.authenticate] }, async (request, reply) => { // Replicate route logic for testing purposes const { recipient, recipients, subject, body, mediaUrls } = request.body; const recipientList = recipients || (recipient ? [recipient] : []); if (recipientList.length === 0) { return reply.code(400).send({ error: 'Bad Request', message: 'At least one recipient is required.' }); } if (!body && !mediaUrls) { return reply.code(400).send({ error: 'Bad Request', message: 'Either body or mediaUrls is required.'}); } const messageBirdPayload = { originator: process.env.MESSAGEBIRD_ORIGINATOR, recipients: recipientList, ...(subject && { subject: subject }), ...(body && { body: body }), ...(mediaUrls && { mediaUrls: mediaUrls }), }; try { // Actual axios call will be intercepted by nock in tests const response = await axios.post('https://rest.messagebird.com/mms', messageBirdPayload, { headers: { 'Authorization': `AccessKey ${process.env.MESSAGEBIRD_API_KEY}`, 'Content-Type': 'application/json' } }); reply.code(response.status).send(response.data); } catch (error) { if (error.response) { // Simulate the error handling logic from the actual route const statusCode = error.response.status >= 500 ? 500 : 400; const errorType = statusCode === 400 ? 'Bad Request' : 'Service Error'; reply.code(statusCode).send({ error: errorType, message: `MessageBird API error: ${error.response.data?.errors?.[0]?.description || 'Unknown error'}`, details: error.response.data }); } else if (error.request) { reply.code(503).send({ error: 'Service Unavailable', message: 'No response received from MessageBird API.' }); } else { reply.code(500).send({ error: 'Internal Server Error', message: error.message }); } } }); t.teardown(() => { app.close(); nock.cleanAll(); // Clean up nock interceptors after tests }); return app; } test('/send-mms route', async (t) => { t.test('should return 401 without API key', async (t) => { const app = build(t); const response = await app.inject({ method: 'POST', url: '/send-mms', payload: { recipient: '+11112223344', body: 'Test' } }); t.equal(response.statusCode, 401); t.match(response.json(), { error: 'Unauthorized' }); }); t.test('should return 401 with incorrect API key', async (t) => { const app = build(t); const response = await app.inject({ method: 'POST', url: '/send-mms', headers: { 'x-api-key': 'wrong-key' }, payload: { recipient: '+11112223344', body: 'Test' } }); t.equal(response.statusCode, 401); t.match(response.json(), { error: 'Unauthorized' }); }); t.test('should return 400 if recipient and recipients are missing', async (t) => { const app = build(t); const response = await app.inject({ method: 'POST', url: '/send-mms', headers: { 'x-api-key': 'test-api-key' }, payload: { body: 'Test' } // Missing recipient }); t.equal(response.statusCode, 400); t.match(response.json(), { message: 'At least one recipient is required.' }); }); t.test('should return 400 if body and mediaUrls are missing', async (t) => { const app = build(t); const response = await app.inject({ method: 'POST', url: '/send-mms', headers: { 'x-api-key': 'test-api-key' }, payload: { recipient: '+11112223344' } // Missing body/mediaUrls }); t.equal(response.statusCode, 400); t.match(response.json(), { message: 'Either body or mediaUrls is required.' }); }); t.test('should return 200 on successful mock MessageBird call', async (t) => { const app = build(t); const testPayload = { recipient: '+11112223344', body: 'Unit Test Message', mediaUrls: ['https://example.com/image.jpg'] }; const expectedMbPayload = { originator: process.env.MESSAGEBIRD_ORIGINATOR, recipients: [testPayload.recipient], body: testPayload.body, mediaUrls: testPayload.mediaUrls }; const mockMbResponse = { id: 'mb-fake-id', status: 'sent', recipients: { totalSentCount: 1 } }; // Use nock to intercept the outgoing HTTP request to MessageBird nock('https://rest.messagebird.com') .post('/mms', expectedMbPayload) // Match the payload axios would send .reply(200, mockMbResponse); const response = await app.inject({ method: 'POST', url: '/send-mms', headers: { 'x-api-key': 'test-api-key' }, // Use the key expected by mock authenticate payload: testPayload }); t.equal(response.statusCode, 200, 'Should return status code 200'); t.match(response.json(), mockMbResponse, 'Response body should match mock MessageBird response'); t.ok(nock.isDone(), 'MessageBird API mock endpoint should have been called'); // Ensure the mocked endpoint was called }); t.test('should return 400 on MessageBird API client error (e.g., invalid recipient)', async (t) => { const app = build(t); const testPayload = { recipient: '+12223334455', body: 'Bad request test' }; const mockMbErrorResponse = { errors: [{ code: 21, description: 'Recipient not valid', parameter: 'recipients' }] }; const expectedMbPayload = { originator: process.env.MESSAGEBIRD_ORIGINATOR, recipients: [testPayload.recipient], body: testPayload.body }; nock('https://rest.messagebird.com') .post('/mms', expectedMbPayload) .reply(422, mockMbErrorResponse); // MessageBird often uses 422 for validation const response = await app.inject({ method: 'POST', url: '/send-mms', headers: { 'x-api-key': 'test-api-key' }, payload: testPayload }); t.equal(response.statusCode, 400, 'Should return status code 400'); // Our app maps 4xx to 400 t.match(response.json(), { error: 'Bad Request', message: 'MessageBird API error: Recipient not valid', details: mockMbErrorResponse }, 'Response body should contain MessageBird error details'); t.ok(nock.isDone(), 'MessageBird API mock endpoint should have been called'); }); t.test('should return 503 when MessageBird API does not respond', async (t) => { const app = build(t); const testPayload = { recipient: '+13334445566', body: 'Timeout test' }; nock('https://rest.messagebird.com') .post('/mms') .delayConnection(100) // Simulate a delay .replyWithError({ code: 'ETIMEDOUT' }); // Simulate a timeout/network error const response = await app.inject({ method: 'POST', url: '/send-mms', headers: { 'x-api-key': 'test-api-key' }, payload: testPayload }); t.equal(response.statusCode, 503, 'Should return status code 503'); t.match(response.json(), { error: 'Service Unavailable' }); t.ok(nock.isDone(), 'MessageBird API mock endpoint should have been called'); }); // Add more tests for other scenarios: // - Using 'recipients' array instead of 'recipient' // - Missing 'body' but providing 'mediaUrls' // - Providing 'subject', 'reference', 'scheduledDatetime' // - Exceeding recipient limit (should return 400 before calling MessageBird) // - MessageBird 5xx errors (should return 500) });
nock
: Mocks HTTP requests to the MessageBird API, preventing actual calls during tests and allowing you to simulate success/error responses.app.inject
: Fastify's utility to simulate HTTP requests to your application without needing a running server.- Test Cases: Cover authentication failures, validation errors, successful calls (mocked), and various MessageBird API error scenarios (mocked).
-
Integration Testing:
- Run the application (
npm run dev
). - Use
curl
or a tool like Postman/Insomnia to send requests tohttp://localhost:3000/send-mms
(or your configured port). - Requires
.env
: Ensure your.env
file is populated with test credentials if possible, or be prepared to use live (but potentially costly) credentials carefully. - Test Payload:
curl -X POST http://localhost:3000/send-mms \ -H ""Content-Type: application/json"" \ -H ""x-api-key: YOUR_SECURE_API_KEY_FOR_THIS_APP"" \ # Use the key from your .env -d '{ ""recipient"": ""+1xxxxxxxxxx"", # Replace with a valid test recipient number ""body"": ""Hello from Fastify & MessageBird!"", ""mediaUrls"": [""https://www.messagebird.com/assets/images/og/messagebird.png""] # Example public URL }'
- Verify the response from your API.
- Check your application logs for request/response details and errors.
- Check the MessageBird dashboard logs to see if the request was received.
- Run the application (
-
Webhook Testing (using
ngrok
):- Start your server:
npm run dev
- Expose your local server:
ngrok http 3000
(replace 3000 if using a different port). Note the publichttps://*.ngrok.io
URL. - Configure the ngrok URL as the ""Status reports URL"" for your MessageBird number in their dashboard.
- Send an MMS using your integration test method above.
- Observe the logs in your running Fastify application. You should see log entries from the
/mms-status
route when MessageBird sends status updates. - Check the
ngrok
web interface (http://localhost:4040
by default) to inspect incoming webhook requests.
- Start your server:
9. Deployment
Deploying the application makes it accessible publicly.
-
Choose a Hosting Provider:
- PaaS (Platform-as-a-Service): Heroku, Render, Google App Engine, AWS Elastic Beanstalk. Often simpler to manage.
- IaaS (Infrastructure-as-a-Service): AWS EC2, Google Compute Engine, DigitalOcean Droplets. More control, more setup required.
- Serverless: AWS Lambda + API Gateway, Google Cloud Functions. Scales automatically, pay-per-use (consider cold starts).
- Containers: Docker + Kubernetes (EKS, GKE, AKS) or managed container services (AWS Fargate, Google Cloud Run).
-
Prepare for Production:
- Environment Variables: Configure environment variables securely on your hosting provider (do NOT commit
.env
). Use the provider's secrets management. - Build Step (if needed): If using TypeScript or a build process, ensure it runs before deployment.
NODE_ENV=production
: Ensure theNODE_ENV
environment variable is set toproduction
. This disables development features (likepino-pretty
) and enables optimizations in Fastify and other libraries.- Start Script: Ensure your
package.json
start
script (node server.js
) is correct. - HTTPS: Configure HTTPS (usually via a load balancer or reverse proxy provided by the host).
- Logging: Configure log aggregation/monitoring (e.g., Datadog, Splunk, ELK, CloudWatch Logs, Google Cloud Logging). Production JSON logs are ideal for this.
- Process Manager (if not using PaaS/Containers): Use
pm2
or similar to manage the Node.js process (restarts on crash, clustering).
- Environment Variables: Configure environment variables securely on your hosting provider (do NOT commit
-
Deployment Methods (Examples):
- Heroku:
- Install Heroku CLI.
heroku login
heroku create
- Set environment variables:
heroku config:set MESSAGEBIRD_API_KEY=... FASTIFY_API_KEY=...
etc. - Ensure
Procfile
exists (usually inferred for Node.js):web: npm start
git push heroku main
- Docker:
- Create a
Dockerfile
:# Dockerfile FROM node:18-alpine AS base WORKDIR /app # Install dependencies only when needed FROM base AS deps COPY package.json package-lock.json* ./ RUN npm ci --omit=dev # Rebuild the source code only when needed FROM base AS builder COPY /app/node_modules /app/node_modules COPY . . # Add build step here if necessary (e.g., RUN npm run build) # Production image, copy all the files and run next FROM base AS runner ENV NODE_ENV production # You can set other ENV variables here or via docker run/compose/k8s # ENV PORT=3000 # ENV HOST=0.0.0.0 # ENV MESSAGEBIRD_API_KEY=... (Better to pass these at runtime) COPY /app/node_modules /app/node_modules COPY /app /app EXPOSE ${PORT:-3000} CMD ["npm", "start"]
- Build:
docker build -t fastify-mms-app .
- Run:
docker run -p 3000:3000 -e MESSAGEBIRD_API_KEY=... -e FASTIFY_API_KEY=... fastify-mms-app
(Pass secrets via-e
or volume mounts). - Deploy the container image to your chosen container platform (Cloud Run, Fargate, Kubernetes, etc.).
- Create a
- Heroku:
Conclusion
You have successfully built a Node.js application using Fastify to send MMS messages via the MessageBird API. This service includes essential features like configuration management, authentication, robust error handling, logging, and webhook support for status updates. Remember to prioritize security, thoroughly test your implementation, and choose a deployment strategy that fits your needs. This foundation allows you to integrate rich media messaging into your applications effectively for users in the US and Canada.