Frequently Asked Questions
You can schedule SMS reminders using Node.js by building an application with Fastify, Prisma, and the Sinch SMS API. The application stores reminder details in a PostgreSQL database and uses node-cron to trigger messages via Sinch at the specified time. This setup ensures reliable delivery of time-sensitive communications.
The Sinch SMS API is used to send the actual SMS messages to recipients. The application integrates with Sinch via the @sinch/sdk-core package, allowing you to send messages globally. This simplifies the process of sending SMS messages from your Node.js application.
Fastify is a high-performance web framework known for its speed and extensible plugin architecture. Its efficiency makes it ideal for handling API requests and integrating with external services like the Sinch SMS API and PostgreSQL database.
A production-ready SMS scheduler is ideal when you need reliable, automated delivery of SMS messages at specific times. Use cases include appointment reminders, notifications, marketing campaigns, and other time-sensitive communications requiring accurate scheduling and error management.
The tutorial utilizes PostgreSQL, a powerful and reliable open-source relational database, to store the SMS reminder details. This is combined with Prisma, an ORM that simplifies database interactions and ensures type safety within your Node.js application.
The tutorial provides step-by-step instructions for setting up a Sinch SMS scheduler with Node.js. This involves installing necessary packages like Fastify, @sinch/sdk-core, and Prisma, configuring environment variables with your Sinch credentials, setting up the database schema, and creating the scheduler job.
Prisma is used as an Object-Relational Mapper (ORM) to simplify interactions with the PostgreSQL database. It streamlines database access, schema migrations, and provides type safety, making database operations cleaner and more efficient.
Node-cron is a task scheduler that enables the application to periodically check the database for due reminders. It's crucial for the automation aspect of the project, as it ensures reminders are sent at the correct scheduled times.
Error handling is implemented throughout the application using try...catch blocks and strategic logging. This includes handling potential errors during API requests, database interactions, and SMS sending via Sinch, ensuring the application remains stable and informative during issues.
Yes, you can modify the scheduler's frequency by changing the CRON_SCHEDULE environment variable. The default value is ' *', which means the scheduler runs every minute, but you can customize it using standard cron syntax.
While the provided example doesn't include automatic retries, you can implement this by adding a retryCount field to the database schema and modifying the scheduler to check and retry failed messages up to a maximum retry limit. For advanced retry logic, consider using a dedicated job queue library.
Node.js version 20.0.0 or later is required for this project. This is primarily because Fastify v5, a key dependency used as the web framework, necessitates Node.js 20 or higher to work properly.
Pino is the logger used by Fastify, offering efficient structured logging capabilities. Pino-pretty is a development dependency that formats Pino's JSON output into a human-readable format, making debugging easier. In production, pino is usually configured for JSON output compatible with logging systems.
Sinch credentials (Project ID, Key ID, and Key Secret) should be stored in a .env file in your project's root directory. Never commit this file to version control. The application loads these credentials from the .env file during startup. Obtain your credentials from your Sinch Customer Dashboard under Access Keys and Numbers > Your Numbers or SMS > Service Plans for the Sinch number.
This guide provides a step-by-step walkthrough for building a robust SMS scheduling and reminder application using Node.js, the Fastify web framework, the Sinch SMS API, and PostgreSQL with Prisma. You'll learn how to create an API endpoint to schedule reminders, store them in a database, and use a background job to send SMS messages via Sinch at the designated time.
We aim to create a reliable system capable of handling scheduled messages, including proper error handling, logging, and configuration management, suitable for production environments.
Project Overview and Goals
What We'll Build:
POST /reminders
) to accept SMS reminder requests (recipient number, message, scheduled time).node-cron
) to periodically check for due reminders.@sinch/sdk-core
to send the SMS messages.Problem Solved:
This application addresses the need to reliably send SMS messages at a future specified time, a common requirement for appointment reminders, notifications, marketing campaigns, and other time-sensitive communications.
Technologies Used:
@sinch/sdk-core
): Provides the functionality to send SMS messages globally. Chosen for its direct integration capabilities via the Node.js SDK.node-cron
: A simple cron-like job scheduler for Node.js. Chosen for its ease of use in running periodic tasks.dotenv
: A zero-dependency module that loads environment variables from a.env
file. Essential for managing configuration and secrets securely.pino-pretty
: A development dependency to make Pino logs human-readable.System Architecture:
Prerequisites:
curl
or a similar tool (like Postman) for testing the API.Final Outcome:
By the end of this guide, you will have a functional Node.js application that can:
1. Setting up the Project
Let's initialize the Node.js project, install dependencies, and configure the basic structure.
1. Create Project Directory:
Open your terminal and create a new directory for the project.
2. Initialize Node.js Project:
This creates a
package.json
file with default settings.3. Install Dependencies:
We need several packages for our application:
fastify
: The web framework.@sinch/sdk-core
: The official Sinch Node.js SDK.dotenv
: Loads environment variables from.env
.@prisma/client
: The Prisma database client.node-cron
: The job scheduler.pino
: Fastify's default logger.@fastify/rate-limit
: Plugin for API rate limiting.prisma
: The Prisma CLI (development dependency).pino-pretty
: Makes logs readable during development.nodemon
: Automatically restarts the server on file changes during development.4. Configure
package.json
Scripts:Open your
package.json
file and add/modify thescripts
section:start
: Runs the application in production.dev
: Runs the application usingnodemon
for auto-reloading and pipes logs throughpino-pretty
.prisma:migrate:dev
: Creates and applies database migrations during development.prisma:generate
: Generates the Prisma Client based on your schema.prisma:deploy
: Applies pending migrations in production environments."type": "module"
: Allows us to useimport
/export
syntax instead ofrequire
.5. Initialize Prisma:
Set up Prisma to manage our database connection and schema.
This command does two things:
prisma
directory with a basicschema.prisma
file..env
file (if it doesn't exist) and adds aDATABASE_URL
placeholder.6. Configure Environment Variables (
.env
):Open the
.env
file created by Prisma (or create one if it doesn't exist) and add the following variables. Never commit this file to version control.DATABASE_URL
: Replace the placeholder with your actual PostgreSQL connection string.SINCH_PROJECT_ID
,SINCH_KEY_ID
,SINCH_KEY_SECRET
: Obtain these from your Sinch Customer Dashboard under Access Keys. Navigate to your Sinch account > APIs > Access Keys. Create a new key if needed.SINCH_NUMBER
: This is the phone number or Service Plan ID you will send SMS from. You can find assigned numbers or create Service Plan IDs in your Sinch Customer Dashboard. For SMS, navigate to Numbers > Your Numbers or SMS > Service Plans.PORT
: The port your Fastify server will listen on.NODE_ENV
: Controls application behavior (e.g., logging).CRON_SCHEDULE
: Defines how often the scheduler job runs.* * * * *
means every minute.ENABLE_SCHEDULER
: Allows disabling the scheduler via environment variable.7. Create Project Structure:
Organize your code for better maintainability. Create the following directories:
src/
: Contains the main application code.src/routes/
: Holds API route definitions.src/services/
: Contains modules for interacting with external services (like Sinch) or database logic.src/jobs/
: Contains background job definitions (like the scheduler).Your project structure should now look similar to this:
Ensure
.env
andnode_modules/
are listed in your.gitignore
file.2. Creating a Database Schema and Data Layer
We'll use Prisma to define our database schema and interact with the database.
1. Define Prisma Schema:
Open
prisma/schema.prisma
and define the model for storing reminder information.id
: Unique identifier using CUID.phoneNumber
: Stores the recipient's number. Store in E.164 format (e.g.,+14155552671
) for consistency.message
: The text of the SMS.scheduledAt
: The core field for scheduling. Store this in UTC to avoid timezone issues. The application logic should handle conversion if necessary.status
: Tracks the reminder's state using a Prismaenum
. Defaults toPENDING
.sentAt
: Records when the message was processed.error
: Stores details if sending fails.createdAt
,updatedAt
: Standard timestamp fields.@@index([status, scheduledAt])
: Crucial for performance. The scheduler job will query based on these fields, so an index significantly speeds this up.2. Apply Database Migration:
Run the Prisma migrate command to create the initial migration files and apply the changes to your database.
prisma/migrations/
directory.DATABASE_URL
.prisma generate
automatically.3. Initialize Prisma Client:
Create a reusable Prisma Client instance. Create
src/services/prisma.js
:This centralizes the Prisma Client initialization.
3. Implementing Proper Error Handling, Logging, and Retry Mechanisms
Robust logging and error handling are essential for production applications.
1. Setup Centralized Logger:
Fastify uses Pino for logging. Let's configure it properly. Create
src/services/logger.js
:NODE_ENV
.pino-pretty
only in development for readability. In production, standard JSON logs are usually preferred for log aggregation systems.2. Integrate Logger with Fastify:
Modify the main server file (
server.js
, which we'll create fully in Section 7) to use this logger instance. Fastify v4+ uses thelogger
option passed as an instance.3. Error Handling Strategy:
try...catch
in handlers or Fastify'ssetErrorHandler
.try...catch
around its main logic (fetching reminders, sending SMS) to prevent the entire scheduler process from crashing due to a single failed reminder. Log errors and update the reminder status toFAILED
.4. Retry Mechanisms (Conceptual):
Directly implementing complex retry logic with exponential backoff can add significant complexity. For this guide, we'll keep it simple:
status
is set toFAILED
, and the error is logged. The scheduler will not automatically retry that specific message in the next run.retryCount
field to theReminder
model.FAILED
reminders withretryCount < MAX_RETRIES
.Example Error Logging (in
sendSms
):We will log errors with context in
src/services/sinch.js
(defined in the next section).4. Integrating with Sinch
Now_ let's create a service module to handle interactions with the Sinch SMS API.
1. Create Sinch Service Module:
Create
src/services/sinch.js
:SinchClient
instance using credentials from.env
.sendSms
Function:to
) and message (body
) as arguments.sms.batches.send
). Note: Always refer to the official@sinch/sdk-core
documentation for the specific version you are using.try...catch
block for error handling.5. Building the API Layer
Let's create the Fastify API endpoint to schedule new reminders.
1. Define API Route:
Create
src/routes/reminders.js
:body
and the successfulresponse
(status 201). Includes validation rules (required fields_ string formats_ date-time format_ phone number pattern). Corrected regex pattern.POST /reminders
route usingfastify.post
.createReminderSchema
to the route options. Fastify automatically validates the incoming request body and serializes the response according to the schema.scheduledAt
is in the future.prisma
client tocreate
a new reminder record.try...catch
block to handle potential database errors during creation.201 Created
status with the newly created reminder's details (filtered by the response schema). Returns appropriate error codes (400_ 500) on failure.6. Implementing Core Functionality (Scheduler Job)
Now_ let's create the background job that checks for due reminders and triggers the SMS sending.
1. Create Scheduler Job:
Create
src/jobs/scheduler.js
:node-cron
to schedule theprocessDueReminders
function based onCRON_SCHEDULE
from.env
. Runs in UTC.isJobRunning
flag. Note: This simple flag prevents overlap on a single instance but does not handle concurrency across multiple application instances in a distributed environment. More robust solutions like database advisory locks or dedicated job queues (e.g._ BullMQ) are needed for multi-instance deployments.PENDING
reminders wherescheduledAt
is less than or equal to the current time (now
). Usestake
for batching andorderBy
.sendSms
_ determinesupdateData
_ and updates the reminder status (SENT
orFAILED
) in the database.try...catch
blocks around DB query_ individual SMS sending_ and individual DB updates. Usesfinally
to ensure the lock is released.7. Server Setup (
server.js
)We need the main entry point to tie everything together. Create
server.js
in the root directory:.env
usingdotenv
.@fastify/rate-limit
plugin and thereminderRoutes
(prefixing them with/api
). Usesawait
for registration as it can be asynchronous.SIGINT
andSIGTERM
signals to close the server and disconnect Prisma properly before exiting.async
functionstartServer
to listen on the configured port and host (0.0.0.0
makes it accessible externally, adjust if needed).startScheduler()
only ifENABLE_SCHEDULER
is true.try...catch
aroundfastify.listen
to handle startup errors.