Running Twilio Serverless Functions Using Database Events

June 06, 2024
Written by
Desmond Obisi
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Running Twilio Serverless Functions Using Database Events

In the world of modern application development, event-driven architecture has become a popular paradigm for building scalable and responsive systems. Event-driven messaging allows developers to design applications that respond dynamically to changes in the system, ensuring a more real-time response and efficient user experience.

Imagine a scenario where you need to automatically send SMS or push notifications as soon as a task enters a ”success” or ”done” state in your application. How can you achieve this without constantly polling your database for updates? In this article, we will explore how to leverage event-driven architecture to trigger Twilio Functions based on data change in a MongoDB database.

Prerequisites

Here are what you need to follow along in this tutorial

Developing Your Application

To set up your APIs, I attached a link to a codebase that contains the base application used for this tutorial. It contains all the code necessary to start a Node.js server and some already-made endpoints that work once you connect to a MongoDB database. This section will work you through how to run the project on your local machine, set up Twilio, and any other requirements you need to build your solution

Running the Node.js API

The API we will use in this guide is a bookstore API that allows authors to upload book inventory and readers to rent or borrow to return in due time. To get the project running on our local machine, you can follow these steps:

Navigate to your terminal and clone the project from the GitHub repository by running the following command:

git clone https://github.com/DesmondSanctity/db-events-messaging.git

Make sure you are in the twilio-starter branch and then run the installation script within the project directory to install the needed packages:

npm install

To get your MongoDB Atlas URL for the database connection, go to MongoDB’s website to log in if you already have an account or register if you are a new user.

Your dashboard should look like the one below. Click on Create to set up a new database.

MongoDB Atlas dashboard where you can create database clusters.

Fill in the options as shown below and proceed to create your cluster. This cluster will contain all the tables and their respective data for our application. Click on Create to continue.

MongoDB Atlas configuration screen where you select configurations for your cluster.

You will need to set access and authorization next. As shown below, MongoDB auto-generates a username and password for your cluster. Kindly update them to what you want and save them using the Create User button. Add 0.0.0.0/0to the IP Access List input and click Add Entry to save. This allows access to your MongoDB database from any IP. You can update this later from the Network Access tab in the dashboard if you want to give access to limited IPs.

MongoDB Atlas configuration where you choose authentication type and set the credentials.

Click on the Finish and Close button to finish your setup and navigate to the dashboard. From the Dashboard, we can get the link to connect to our database by clicking Connect

MongoDB Atlas dashboard showing a cluster deployed.

You will get to see different options to connect to your MongoDB database. In the tutorial, you will be using the Drivers which help you to access your data using MongoDB’s native drivers (e.g. Node.js, Go, etc.).

Connection options to connect to your Atlas cluster.

When you select the Drivers option, you will see the page with the credentials you need. It is a URL with a placeholder for your cluster password for authentication. Copy this URL for the next section. Be sure that Node.js is selected in the dropdown menu, and run the commands to install the drivers.

Connection options for using MongoDB Node.js driver.

Create a .env file in the root directory of your project and set the following variables in it.

APP_PORT=4000
JWT_SECRET=
JWT_EXPIRES=7d
DB_URL=
TWILIO_PHONE_NUMBER=

For the DB_URL, paste in the URL from the previous section and replace the placeholder with the password you added to the user when you configured the cluster.

The JWT_SECRET is a secret string used to tokenize the user’s access through jsonwebtoken. This can be any string but for security reasons make it longer, alphanumeric, and difficult to guess.

Setting up a Twilio account

To set up your Twilio account, sign up for an account and log into your Twilio Console using your account details. Access your Twilio Console dashboard and navigate to the WhatsApp sandbox in your account. This sandbox is designed to enable you to test your application in a development environment without requiring approval from WhatsApp. To access the sandbox, select Messaging from the left-hand menu, followed by Try It Out, and then Send A WhatsApp Message. From the sandbox tab, take note of the Twilio phone number and the join code. You will add the Twilio WhatsApp phone number to your .env file for later use.

A photo showing how to connect to the Whatsapp sandbox from Twilio console

Listening to Events and Triggers

In this section, you will learn about the different kinds of database events and how to listen to them. You will also deploy a Twilio serverless function that will run independently of your application. Running a serverless function that performs a particular task when an event occurs in your database is best handled serverless where you abstract scaling and optimization from the main app to the serverless infrastructure.

Create Twilio serverless functions

There are several ways to deploy a Twilio function: using the Twilio serveless toolkit, the API reference, from the console, or using the TwiML Bins. In this guide, we will be deploying from the console. To create your serverless function, log in to your Twilio console and select Functions and Assets from the sidebar menu. Select Overview and click on the Create Service button to create your service. Give your service a friendly name, and click Next to finalize creation. This service will contain all the functions and assets you want to use for a particular project.

Twilio Functions overview screen from the console.

When you have successfully created the service, you will see a code playground where you can create as many functions as you want as well as an option to deploy the function to be used in your application. From the sidebar of the code playground, click on the Add button to add an endpoint. Select Add Function and name the path /notify. Paste the code below into the editor, and save.

exports.handler = async function(context, event, callback) {
  // The pre-initialized Twilio Client is available from the `context` object
  const client = context.getTwilioClient();
  // Access data from incoming event
  const To = event.To;
  const From = event.From;
  const mediaUrl= event.mediaUrl
  const rentId = event.rentId;
  const notificationMessage = event.notificationMessage;
  
 try {
    await client.messages.create({
      body: notificationMessage,
      from: From,
      to: To,
      mediaUrl: [mediaUrl]
    });
    console.log('Twilio WhatsApp notification sent successfully!');
    callback(null, 'Notification sent for rent ID: ' + rentId); // Success response
  } catch (error) {
    console.error('Error sending Twilio WhatsApp notification:', error);
    callback(error); // Pass error to caller
  }
};

The code above is a handler function that has context, an event, and a callback function. Here is an explainer of the key parameters:

  • The context contains inbuilt Twilio credentials, Twilio instance, and other Twilio toolkits. As you can see, an instance of a Twilio client is initialized from the context.
  • The event is the body of the incoming request coming to the function. This is where variables and parameters that will be used within the function are passed and accessed. In the code above, the notification message, WhatsApp numbers of the sender and receiver, and the media for the WhatsApp message are obtained from the event.
  • The callback is where callback actions are passed to the function. If you want the function to do something else after it completes its cycle, then the function can be passed as a callback.

You can now deploy the function so it can be accessed publicly from your application. From the playground sidebar, click the Deploy All button to trigger the deployment. Your function can now be accessed via this URL: https://.twil.io/

Twilio Function editor cotaining the code for the /notify endpoint.

Add event utility functions

If you cloned the starter project, you will have a setup like the one below. You will be adding new utility functions for overdue book renting, a cron service, and a database event listener in the Rent model.

A screenshot showing adding event utility functions in the code

In the src/events/overdue.js file, you will add this code. This code is responsible for calling our Twilio serverless function which sends a WhatsApp message to overdue borrowers in our book renting application.

import { Users, Books } from '../models/index.js';
import { twilioPhoneNumber } from '../config/app.config.js';

export const overdueEvent = async (updatedRent) => {
 // Check if isOverdue was updated to true
 if (updatedRent.isOverdue === true) {
  //get the borrower phone number
  const user = await Users.findById(updatedRent.borrower);
  const book = await Books.findById(updatedRent.book);
  const notificationMessage = `Dear ${user.fullName}, your book rental with ID: ${updatedRent._id} is now overdue.\n\n Kindly return the book to the library as agreed. if you want to extend your time, use the app to extend and pay the extension fee.`;

  const twilioFunctionUrl = 'https://<service-name>.twil.io/<path>'; // Replace with your function URL
  try {
   const response = await fetch(twilioFunctionUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
     rentId: updatedRent._id,
     notificationMessage: notificationMessage,
     To: `whatsapp:${user.phoneNumber}`,
     From: `whatsapp:${twilioPhoneNumber}`,
     mediaUrl: book.coverImageUrl,
    }), // Send event data in request body
   });

   if (response.ok) {
    console.log(
     'Twilio notification request successful:',
     await response.text()
    );
    // return updatedRent;
   } else {
    console.error(
     'Error sending Twilio notification request:',
     await response.text()
    );
   }
  } catch (error) {
   console.error('Error calling Twilio serverless function:', error);
  }
 }
};
Remember to update the twilioFunctionUrl with your deployed Twilio function URL from the console.

In the src/models/rent.model.js file, update the existing code to the one below

import mongoose, { Schema } from 'mongoose';
import { overdueEvent } from '../events/overdue.js';
import { Rents } from './index.js';

const RentSchema = new mongoose.Schema({
 rentDate: {
  type: Date,
  required: [true, 'Please provide a rent date'],
 },
 returnDate: {
  type: Date,
  required: [true, 'Please provide a return date for the rentage'],
 },
 isOverdue: {
  type: Boolean,
  default: false,
  unique: false,
 },
 book: {
  type: Schema.Types.ObjectId,
  ref: 'Books',
  required: [true, 'rent record should have a book'],
 },
 author: {
  type: Schema.Types.ObjectId,
  ref: 'Users',
  required: [true, 'rent record should have an author'],
 },
 borrower: {
  type: Schema.Types.ObjectId,
  ref: 'Users',
  required: [true, 'rent record should have a borrower'],
 },
});

// Add post hook on update
RentSchema.post('save', async function () {
 // Get updated doc
 const updatedDoc = await Rents.findById(this._id);

 // Check if isOverdue changed to true
 if (updatedDoc.isOverdue === true) {
  // Call overdueEvent
  await overdueEvent(updatedDoc);
 }
});

export default mongoose.model('Rents', RentSchema);

The new update to the existing code is the addition of a post-hook function. Mongoose uses a hook to listen to events on the MongoDB database. Hooks can be called before(pre-hook) or after(post-hook) a database operation. In the hook function, the app is listening to a save operation in the Rent model, and when the isOverdue column in the saved data is true, it triggers the notification function.

Set triggers for events function

A trigger is needed to check which rented book is overdue return for the application. This guide uses node-cron, a cron job package that allows you to schedule tasks in Node.js. In the src/utils/cron.js file, add this code:

import cron from 'node-cron';
import { Rents } from '../models/index.js';

// Cron job to run every 5 minutes
cron.schedule('*/5 * * * *', async () => {
 console.log("/** cron started */")
 const overdueRents = await Rents.find({
  returnDate: { $lt: new Date() },
  isOverdue: false,
 });

 overdueRents.forEach(async (rent) => {
  rent.isOverdue = true;
  await rent.save();
 });
 console.log("/** cron ended */")
});

This code runs every 5 minutes to check for overdue rent by comparing the proposed returnDate and the current date. When it finds any, it updates the isOverdue column to true and saves it to the database . Since our app is listening for a save event in the Rent model, the overdue events function will run and send a message to the borrower on WhatsApp using our serverless function running on Twilio.

Testing and Demo

Your app is now ready for testing. You can start the local development server by running this command:

npm run dev

Once the app runs, you can access it at http://localhost:4000 . To make sure it works well in a live environment, you will use ngrok to generate a live server for your app by running this command.

ngrok http http://localhost:4000

A live server will be created by ngrok as a proxy to your localhost server as shown below:

Ngrok port forwading to live server from your localhost address

With the ngrok live server, you can create users, books, and rentals in the application using tools like Postman or other alternatives for API queries. This demo video shows how to use Postman to test the APIs and also how the cron task is timely triggered to check for overdue rentals and send notifications to the borrower via the Twilio serverless function we deployed earlier.

Conclusion

In this comprehensive guide, you have been able to understand how one can leverage events in the database to run tasks such as notifications and messaging using Twilio serverless functions.

Some key takeaways from the guide are:

  • Real-time Notification: By listening to MongoDB database events, you can achieve real-time notifications of changes happening in your database. This ensures that relevant actions are triggered immediately when data is modified, providing timely updates to users or systems.
  • Event-Driven Architecture: Implementing event-driven architecture allows for the decoupling of components in your system. This means that changes in one part of the system can trigger actions in another part without direct dependencies, leading to a more modular and scalable architecture.
  • Automation of Business Logic: By using MongoDB events to trigger actions like calling a Twilio serverless function, you can automate business logic based on changes in your data. For example, sending SMS notifications to borrowers when their rental overdue status changes.
  • Efficiency and Scalability: Handling events asynchronously can improve the efficiency and scalability of your system. Instead of continuously polling the database for changes, you can react only when relevant events occur, reducing unnecessary resource consumption and improving overall performance.

You can learn more about Twilio serverless functions by referring to Twilio’s documentation.

Desmond Obisi is a software engineer and a technical writer who loves doing developer experience engineering. He’s very invested in building products and providing the best experience to users through documentation, guides, building relations, and strategies around products. He can be reached on Twitter, LinkedIn, or through my email desmond.obisi.g20@gmail.com.