Building a Serverless Appointment Scheduler Using Twilio, Express, and AWS: Deploying The Services

The SaaS Enthusiast
8 min readMay 16, 2024

--

An image showcasing an IVR system in action

Introduction

In this article, we’ll explore how to collect user input from a phone conversation using the twilio package and migrate our local Express application to a serverless architecture using AWS Lambda, API Gateway, and the Serverless Framework.

Migrating from Local to Serverless

In our previous article, we used ngrok to expose our local server publicly and configure a Twilio phone number. Now, we’ll migrate our local code to AWS, using the Serverless Framework to create scalable endpoints.

First, set up a new project with necessary dependencies:

npm init --y
npm install --save body-parser express serverless-http twilio

serverless.yml Configuration

Define your service in the serverless.yml file to specify AWS as the provider and set up the routing for incoming HTTP requests:

service: twilio-app

provider:
name: aws
runtime: nodejs18.x
region: us-east-1
profile: rocketeast

functions:
app:
handler: handler.app
events:
- http:
path: /
method: any
- http:
path: /{proxy+}
method: any

Handling Incoming Calls

In the index.js, set up a route to handle incoming calls and collect DTMF inputs:

app.all('/', (request, response) => {
response.type('xml');
const twiml = new VoiceResponse();
twiml.say("Welcome to Miguel's calendar");
const gather = twiml.gather({
input: 'dtmf',
action: `${baseUrl}/results`,
speechTimeout: 'auto',
});
gather.say('Please press 1 to schedule a meeting with Miguel. <break time=".25s"/> Press 2 to cancel a meeting. <break time=".25s"/> Press 3 to re-schedule a meeting.');
response.send(twiml.toString());
});

This code greets the user and asks for input to schedule, cancel, or reschedule a meeting. It uses <gather> to collect DTMF tones and direct the call based on the user’s input.

Handling User Responses

Define how your application should react based on the collected input:

app.all('/results', (request, response) => {
const twiml = new VoiceResponse();
const digits = request.body.Digits;

switch (digits) {
case '1':
twiml.redirect(`${baseUrl}/meetings/create`);
break;
case '2':
twiml.redirect(`${baseUrl}/meetings/cancel`);
break;
case '3':
twiml.redirect(`${baseUrl}/meetings/update`);
break;
default:
twiml.say('Invalid option. Please try again.');
twiml.redirect(`${baseUrl}/`);
break;
}

response.send(twiml.toString());
});

This section of the code handles different operations based on the user’s input, redirecting to specific endpoints to process each request.

Overview

The following code is a comprehensive setup of an Express application integrated with Twilio for handling voice interactions, tailored to function within a serverless environment using AWS Lambda. Here’s a breakdown of its key components and functionalities:

  • Dependencies: The code utilizes express for routing, serverless-http to adapt Express for serverless deployment, body-parser to parse incoming request bodies, and twilio for handling telecommunications features.
  • Express App Initialization: An Express application is initialized and configured to parse URL-encoded bodies.
  • Base URL Configuration: A variable baseUrl is defined to set the base URL for the application, which is crucial for forming callback URLs for Twilio's asynchronous operations.

Endpoints

  • /voice: Handles POST requests to play a simple “Hello world!” message when called. This is likely a test or initial endpoint to ensure basic Twilio integration.
  • /foo: A simple GET endpoint that returns a plain text response. This serves as a basic example of an HTTP route.
  • Root (/): A versatile endpoint configured to handle all HTTP methods. It greets callers, offers options via DTMF inputs, and uses Twilio’s <gather> to collect these inputs. The action URL redirects to /results for processing the input.
  • /results: Processes user inputs from the root endpoint. Depending on the input (‘1’, ‘2’, ‘3’), it redirects to different sub-routes for meeting management (/meetings/create, /meetings/cancel, /meetings/update) or repeats the menu for invalid inputs.
  • /meetings/create: Initiates the meeting scheduling process. It prompts the caller to enter or say the date and time of the meeting.
  • /meetings/create/date-time: Receives and processes the date and time input from the user, then asks for the timezone.
  • /meetings/create/timezone: Finalizes the scheduling by receiving the timezone and confirming the scheduled meeting.
  • /meetings/cancel and /meetings/update: Currently placeholders that inform the user these functionalities are not implemented yet.

Serverless Deployment

  • Exporting the App: The application is wrapped with serverless-http to make it compatible with AWS Lambda, enabling it to handle requests from AWS API Gateway.

Functional Flow

The application flow is designed to handle voice interactions for scheduling, updating, or canceling meetings. It intelligently handles different stages of the meeting setup, including dynamic inputs through voice or keypad, and provides feedback to the user at each step. The use of environmental variables and modular route handling makes this setup scalable and adaptable for different deployment environments.

  1. Caller Interaction: The caller interacts with the system through voice or DTMF inputs.
  2. Dynamic Routing: Based on the input, the system routes the call to different endpoints to handle specific tasks (scheduling, canceling, or updating meetings).
  3. Input Collection: The system uses Twilio’s <gather> to collect user inputs at various stages.
  4. Serverless Deployment: The app is designed to run in a serverless environment, ensuring scalability and reliability.

This code provides a robust foundation for an interactive voice response (IVR) system that can manage scheduling tasks efficiently, leveraging modern serverless technologies for deployment.

Final Code

const serverless = require('serverless-http');
const express = require('express');
const VoiceResponse = require('twilio').twiml.VoiceResponse;
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.urlencoded({ extended: true }));

const baseUrl = process.env.BASE_URL || 'https://XXXXXXXX.execute-api.us-east-1.amazonaws.com/dev';

app.all('/', (request, response) => {
response.type('xml');
const twiml = new VoiceResponse();
twiml.say("Welcome to Miguel's calendar");
const gather = twiml.gather({
input: 'dtmf',
action: `${baseUrl}/results`,
speechTimeout: 'auto',
});
gather.say('Please press 1 to schedule a meeting with Miguel. <break time=".25s"/> Press 2 to cancel a meeting. <break time=".25s"/> Press 3 to re-schedule a meeting.');
console.log(twiml.toString());
response.send(twiml.toString());
});

app.all('/results', (request, response) => {
console.log('results');
response.type('xml');
const twiml = new VoiceResponse();
console.log(request.body);
const digits = request.body.Digits;

switch (digits) {
case '1':
twiml.redirect(`${baseUrl}/meetings/create`);
break;
case '2':
twiml.redirect(`${baseUrl}/meetings/cancel`);
break;
case '3':
twiml.redirect(`${baseUrl}/meetings/update`);
break;
default:
twiml.say('Invalid option. Please try again.');
twiml.redirect(`${baseUrl}/`);
break;
}

response.send(twiml.toString());
});

app.post('/meetings/create', (request, response) => {
console.log('create');
const twiml = new VoiceResponse();
twiml.say('You have chosen to schedule a meeting with Miguel.');
const gather = twiml.gather({
input: 'speech dtmf',
finishOnKey: '#',
action: `${baseUrl}/meetings/create/date-time`,
});
gather.say('Please say or enter the date and time of the meeting followed by the pound key.');
response.type('text/xml');
response.send(twiml.toString());
});

app.post('/meetings/create/date-time', (request, response) => {
console.log('Received request at /meetings/create/date-time');
const twiml = new VoiceResponse();
const dateTime = request.body.SpeechResult || request.body.Digits;

console.log('Date and time received:', dateTime);

if (request.body.Digits) {
twiml.say(`You entered ${request.body.Digits}. Please say timezone followed by the pound key.`);
} else {
twiml.say(`You said ${request.body.SpeechResult}. Please say your timezone followed by the pound key.`);
}

const gather = twiml.gather({
input: 'speech dtmf',
finishOnKey: '#',
action: `${baseUrl}/meetings/create/timezone`,
});
response.type('text/xml');
response.send(twiml.toString());
});

app.post('/meetings/create/timezone', (request, response) => {
console.log('Received request at /meetings/create/timezone');
const twiml = new VoiceResponse();
const timezone = request.body.SpeechResult || request.body.Digits;

console.log('Timezone received:', timezone);

if (timezone) {
twiml.say(`You entered ${timezone}.`);
} else {
twiml.say('Invalid input. Please try again.');
twiml.redirect(`${baseUrl}/meetings/create`);
}

twiml.say('Thank you for providing the details. <break time=".25s"/> Your meeting has been scheduled.<break time=".25s"/> Have a great day. <break time=".25s"/>);');
response.type('text/xml');
response.send(twiml.toString());
});

app.post('/meetings/cancel', (request, response) => {
console.log('Received request at /meetings/cancel');
const twiml = new VoiceResponse();
twiml.say('You have chosen to cancel an already scheduled meeting. This functionality is not yet implemented. Please try again later.');
response.type('text/xml');
response.send(twiml.toString());
});

app.post('/meetings/update', (request, response) => {
console.log('Received request at /meetings/update');
const twiml = new VoiceResponse();
twiml.say('You have chosen to change a meeting. This functionality is not yet implemented. Please try again later.');
response.type('text/xml');
response.send(twiml.toString());
});

module.exports.app = serverless(app);

On this series:

  • Initial Configuration: This article covers setting up a phone number for clients to schedule meetings is straightforward with the right tools. This guide uses Twilio for phone number management and AWS for hosting in a Serverless Architecture. For local development and testing, we’ll use Express and ngrok.
  • Designing DynamoDB Appointments Table: we went through the implementation of a persistent mechanism using AWS DynamoDB as a NoSQL database. We’ll save information about callers, ensuring time zone considerations are taken into account. Once appointments are saved, we’ll check if the time slot is available for new callers who want to schedule an appointment.

What’s next?

In the next article, we will design the persistent layer for our scheduling system. We’ll integrate interactions with DynamoDB to manage data effectively. This will include:

  1. Designing the Database Schema: We’ll set up DynamoDB tables to store appointment details and availability.
  2. Fetching Available Slots: Creating endpoints to retrieve the list of available spots for a given date.
  3. Creating Appointments: Adding functionality to create and save new appointments in DynamoDB.
  4. Local Testing: Using serverless-offline to test our changes locally, ensuring that everything works smoothly before deploying to AWS.

By the end of the next article, you’ll have a fully functional scheduling system with a persistent layer, capable of managing real-time data and scaling with ease.

Conclusion

In this article series, we started with a basic local setup using ngrok and Twilio to quickly get a functional prototype. We then transitioned to a more robust and scalable architecture by deploying our application to AWS using the Serverless Framework.

Using the right tools at the right time is crucial for efficient development. We began with a quick and dirty approach to validate our idea and iteratively improved it by leveraging Amazon’s managed services. This approach not only saved time but also provided a clear path to scale our application infinitely without worrying about infrastructure management.

As we’ve seen, starting with a simple local setup and gradually moving to a scalable serverless architecture allows us to focus on building features while relying on AWS to handle the scalability and reliability of our application. This journey underscores the importance of choosing appropriate tools and methodologies to develop robust and scalable solutions efficiently.

Empower Your Tech Journey:

Explore a wealth of knowledge designed to elevate your tech projects and understanding. From safeguarding your applications to mastering serverless architecture, discover articles that resonate with your ambition.

New Projects or Consultancy

For new project collaborations or bespoke consultancy services, reach out directly and let’s transform your ideas into reality. Ready to take your project to the next level?

Protecting Routes With AWS

Mastering Serverless Series

Advanced Serverless Techniques

--

--