How to receive and reply to SMS messages with TypeScript and Twilio

March 09, 2021
Written by
Twilion
Reviewed by
Twilion
Twilion

Hoqw to receive and reply to SMS messages with TypeScript and Twilio

We've looked at how to send an SMS using TypeScript, but with Twilio you can also receive and reply to incoming SMS messages.

When you send an SMS message to your Twilio phone number, Twilio will send a webhook, an HTTP request with all the details about the message, to a URL you associate with that number. You can reply to the message by responding to the webhook with TwiML (Twilio Markup Language).

In this post we will build a Node.js application with TypeScript, using Express and the Twilio Node package to reply to incoming SMS messages.

What you need

To follow this tutorial you will need:

Once you've prepared those bits, let's get started building the project.

A new TypeScript and Express project

Follow these instructions to get a new TypeScript and Express project off the ground. Start by creating a new directory and then initialise it with a package.json file:

mkdir receive-sms-with-typescript
cd receive-sms-with-typescript
npm init --yes

Next, install the dependencies we're going to need. In our application dependencies, install Express, which will be used to receive and parse the incoming webhooks, and the Twilio package. For development dependencies, install TypeScript and the Definitely Typed types for Express.

npm install express twilio
npm install typescript @types/express nodemon --save-dev

With those installed, use the TypeScript command tsc to initialise a tsconfig.json file for us.

npx tsc --init

Open tsconfig.json and uncomment "outDir", updating it like so:

"outDir": "./dist",

This will compile the TypeScript to the dist directory in our project.

Open up package.json, so that we can make some updates here. First, change the "main" property to point to dist/index.js. Then add the following to the "scripts" property, to compile and run the application:

  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "watch": "tsc --watch",
    "start": "node ."
  },

tsc is the TypeScript compiler, the "build" script will run compilation once and the "watch" script will keep recompiling as you make changes. The "start" script will run our application.

Create some files that we'll use in this application. We'll need index.ts, server.ts, and config.ts for now.

touch index.ts server.ts config.ts

Open server.ts and create a test Express application to make sure things are going as planned.

import express from "express";
const app = express();

app.get("/", (_req, res) => {
  res.send("Hello world!");
});

export default app;

This code requires Express, creates an application and sets up the root path to respond with "Hello world!". It then exports the app so we can use it elsewhere.

Open up config.ts. We don't need a lot of configuration in this application, but I like to keep it together in one file so that it can be loaded when it is needed. For this application, we are going to make the port the web server starts on part of the configuration. Add this to config.ts:

export const port = process.env.PORT || 3000;

Open index.ts, import the app and the port from the config and then use them to make the app listen on the supplied port:

import app from "./server";
import { port } from "./config";

app.listen(port, () => {
  console.log(`Server listening on http://localhost:${port}`);
});

Compile the application and run it.

npm run build
npm start

Open http://localhost:3000 in your browser. You should see the text "Hello world!". Our TypeScript Express server is up and running successfully.

From now, you might find it easier to run the watch command so that the project continues to compile as you make changes. Stop the server and run:

npm run watch

Responding to Twilio webhooks

Now our base application is working, let's add a route for the webhook. Create a new directory for routes and a file for the messages route:

mkdir routes
touch routes/messages.ts

Open routes/messages.ts.  In this file we will create a route that will respond to an incoming webhook request from Twilio. Webhook requests from Twilio are POST requests in the format application/x-www-form-urlencoded by default, so we will use Express's urlencoded middleware to parse the data in the body of the request. To have Twilio respond to the message we need to return TwiML, which is a subset of XML, with instructions to send a message back. We'll use the Twilio package to generate the TwiML.

Start by importing the various elements that we need:

import { twiml } from "twilio";
import { Router, urlencoded } from "express";

Next, create a router object and set up the urlencoded middleware to parse the body:

const router = Router();
router.use(urlencoded({ extended: false }));

Destructure the TwiML MessagingResponse from the twiml object.

const { MessagingResponse } = twiml;

Now we need to define the route. For this application we'll echo the message you send it back to you. We'll get the message from the request body, generate TwiML using the MessagingResponse class, set the response content type to "application/xml" and return the response. Finally, we export the router.

router.post("/", (req, res) => {
  const message = req.body.Body;

  const response = new MessagingResponse();
  response.message(`Hello from TypeScript! You said "${message}"`);

  res.set("Content-Type", "application/xml");
  res.send(response.toString());
});

export default router;

Open server.ts again and import the messaging router, then mount the router at the "/messages" path. You can also remove the initial "Hello world!" route.

import express from "express";
import messageRouter from "./routes/messages";

const app = express();

app.use("/messages", messageRouter);

export default app;

Before we hook this up to a Twilio phone number, we can test this out with an HTTP client like curl or Postman. Restart the server with npm start then make a request like this:

curl --data "Body=Hello%20world" http://localhost:3000/messages

You should get a response that looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Message>Hello from TypeScript! You said "Hello world"</Message>
</Response>

This is looking good, so let's get the application connected to your Twilio phone number.

Connecting a Twilio number to your application

To connect a Twilio number to the app we've built we need to be able to access the application from a public URL. That's where ngrok comes in, it creates a temporary URL that tunnels through to your local machine. Our application is running on localhost port 3000, so we can open a tunnel to it with the command:

ngrok http 3000

Once the tunnel has connected, ngrok displays the randomly generated URL that now points at your application. It should look like https://RANDOM_STRING.ngrok.io. Open up your Twilio console to your incoming numbers, choose the number you want to use for this app, or buy a new one. Edit the number and add your ngrok URL, plus the /messages path, as the webhook for when a message comes in.

If you have the Twilio CLI installed you can do this on the command line too, with the command:

twilio phone-numbers:update PHONE_NUMBER --sms-url https://RANDOM_STRING.ngrok.io/messages

Here's a bonus tip, if you have the Twilio CLI, you don't even need to start up ngrok yourself. Instead you can run:

twilio phone-numbers:update PHONE_NUMBER --sms-url http://localhost:3000/messages

The CLI will detect the localhost URL and set up the ngrok tunnel for you, keeping it alive until you quit the command.

Now you have connected the phone number, send it an SMS message from your own phone and you will receive a reply. You've received and replied to an SMS message with TypeScript!

Do we have enough types?

One thing I find myself asking when writing applications in TypeScript is whether I am taking advantage of TypeScript enough. In this case, I think there is more we can do to give us the confidence when we come to change the system later.

Open routes/messages.ts again. The request and response objects that are passed into the route handler are as generic as they can be to be able to handle any application's input and output.

For example, we know we want the handler to respond with a string, our TwiML. We can encode this in the types of the handler. Start by importing the Response object from Express:

import { Router, Response } from "express";

Now, in the route definition we can encode that the response will always be a string, like so:

router.post("/", (req, res: Response<string>) => {

Try passing anything that isn't a string to res.send() now. You will find that the compilation fails and, if your editor includes TypeScript checking, it will be highlighted.

When the Express Response type includes a string, you cannot return a number, like 404, with res.send.

Change that back to res.send(response.toString()); and everything will work again. That's one less mistake we can make.

But that's not all. We've addressed the response, but not the request. It bugs me that calling on req.body.Body is typed as any.

Hovering over req.body.Body shows that it is of type "any", which we don&#x27;t want.

Can we do better than this? Well, we know what the shape of the request will look like; it's a webhook with parameters defined in the documentation. We can use this to define a request type that will set the type of the body of the request.

Create a new directory called types and a file called request.ts.

mkdir types
touch types/request.ts

Open types/request.ts, here we will define our new request type. First we need to define the type for the body itself. This is made up of the webhook parameters.

type MessagingWebhookBody = {
  MessageSid: string;
  Body: string;
  From: string;
  To: string;
};

There are more parameters, I will leave adding the ones you need as an exercise.

The Express Request object is defined as:

interface Request<
  P = core.ParamsDictionary,
  ResBody = any,
  ReqBody = any,
  ReqQuery = core.Query,
  Locals extends Record<string, any> = Record<string, any>
> extends core.Request<P, ResBody, ReqBody, ReqQuery, Locals> {}

For the purposes of this post, we aren't interested in most of that, but we can see that the ReqBody is defined as any. We can create a new type alias of this Request and insert our MessagingWebhookBody type as the ReqBody. We need to import the Request interface from "express" and the other "core" types from "express-serve-static-core".

import { ParamsDictionary, Query } from "express-serve-static-core";
import { Request } from "express";

Then we can define, and export, our MessagingRequest like so:

export type MessagingRequest = Request<
  ParamsDictionary,
  any,
  MessagingWebhookBody,
  Query
>;

Now we need to import and use this type in our messages route. Open routes/messages.ts and import the request type:


import { twiml } from "twilio";
import { Router, Response, urlencoded } from "express";
import { MessagingRequest } from "../types/request";

Add the type to the route handler signature:

router.post("/", (req: MessagingRequest, res: Response<string>) => {

Now check out the types on req.body. It's a MessagingWebhookBody now and req.body.Body is a string.

Now hovering over the req.body shows that it is of type MessagingWebhookBody, which means req.body.Body is now a string.

We haven't changed the functionality of this route handler, but we have defined the types for the pieces that we are using and can be confident when using them in the future.

If you need to be sure everything still works, send your number another celebratory message!

A view of an iPhone&#x27;s messaging app, with a reply from our TypeScript application.

Onward with TypeScript

In this post you've seen how to use TypeScript to build an Express server that can receive and respond to incoming SMS messages. You can check out the full code to this application in this GitHub repo of Twilio and TypeScript examples.

Now you can build upon this work to create more with Twilio, like sending messages using the REST API or building an SMS weather bot. You don't even need to build a server with Express as you can go serverless and write Twilio Functions with TypeScript.

Are you building something cool using TypeScript and Twilio? I would love to hear about it. Drop me an email at philnash@twilio.com or send me a message onTwitter at @philnash.