Kimberlee Johnson

🥡 Build a Twilio app to help people support local restaurants during COVID-19

This post was originally published on Dev.to.

If you have friends in the restaurant industry, have ever worked in food service, or have been reading your local news, you’re probably also worried about what gestures around wildly all of this means for some of our favorite places.

While nobody knows what the future of restaurants will look like, when I saw The San Francisco Chronicle’s list of open restaurants in the Bay Area, I thought of something I could do that might help a little bit during our present, well, situation. I made The Chronicle's list accessible offline, via a Twilio phone number, to make it easier to call restaurants directly instead of using delivery apps. I hope this post can help you do the same for lists of open restaurants near you.

How it works

A user texts a Twilio Phone Number a five-digit zip code. Our Twilio Phone Number sends an HTTP request including the zip code to a Node.js API deployed on Heroku. The API receives the zip code, looks up the relevant restaurants, and sends a formatted list of them back to the user’s phone number via a POST request.

To set it all up, you’ll want to have Twilio, Heroku, and Github accounts ready.

How to build it

Find your data

If you’re a solo developer like me, it would be a huge amount of work for you to track and manage a list of all the restaurants open for takeout and delivery near you. Thankfully, local news organizations are already doing this (and a lot of other) heavy lifting for us. I relied on The San Francisco Chronicle for my data. Check if your favorite outlet is keeping a list, or do a bit of Googling to find what you need.

Puppy picks up newspaper

If you’re lucky, your news outlet might’ve already released this data in a developer-friendly format. I recommend checking to see if they have a Github account and any related repositories.

If they don’t, you’ll need to extract what you need. To keep things simple, I decided to look for just restaurant names, phone numbers, and zip codes. I right-clicked View Page Source to check out the site’s source code.

View Page Source of Chronicle project

What to do next will vary on the site you’re using. It even looks a bit different for me today as I’m writing this than it did when I built the app. At first, I found the preload script that linked to all of the data I needed for all the restaurants. Since I was erring on the side of getting this deployed quickly, I just copy/pasted that into a restaurant_data.json file.

Record scratch, freeze frame. Yup, that’s me, just copy/pasting data into a file.

Cat scratching turntable

This was not the most sophisticated or scalable way to build what I needed. To really optimize for searching performance later, I could've reformatted the data into an Object with the zip codes as keys and restaurants as values. I could’ve scraped the data programmatically (Ben’s tutorial might’ve helped). Most of all, with hundreds of restaurants potentially being added to this list over time, it would be better to setup and work with a real database instead of a JSON file. As is, there’s no easy way for me to update the list other than repeating the copy/paste process, which is not ideal and a problem I’d love to solve in the future.

That said, my copy/paste gave me a scrappy start to get a basic API up and running.

Set up your API

An API is an application programming interface. Craig Dennis explains what they are better than I can, but the way I think of it is: I knew I needed a way to make this data appear somewhere other than my desktop json file (e.g. from a Twilio Phone Number), and an API could help make that happen.

I used Node.js and Express to get an API up and running locally quickly. This is what my app.js file looks like:

const express = require("express");
const { urlencoded } = require("body-parser");
const routes = require("./routes");
// Configuring the app
const app = express();
app.use(express.json());
app.use("/", routes);
app.use(urlencoded({ extended: false }));
// Throw an error when a resource is not found
app.use((req, res, next) => {
const err = new Error("Not found.");
err.status = 404;
next(err);
});
// Custom error handler
app.use((err, req, res, next) => {
res.status(err.status || 500);
res.json({
error: {
message: err.message
}
});
});
// Preparing port as an environment variable, to make deployment easier and more secure
const port = process.env.PORT || 5000;
// Telling the app where to listen
app.listen(port, () =>
console.log(
`SF Chronicle restaurant data app listening on http://localhost:${port}!`
)
);
view raw app.js hosted with ❤ by GitHub

Don’t worry too much about the routes-related lines or the body-parser. We’ll write our routes in a bit.

After that, within the directory in my terminal I ran npm i to install dependencies, and then npm start to make sure my app was running. You should see a message in your terminal confirming you’re up and running (mine is line 34 in the gist).

Once you see that message, we can start working with restaurant_data.json.

The data-model.js file parses through our .json. I wanted to comb through for three things: all the restaurants, all the zip codes in the dataset (this would be useful for comparisons later), and all the restaurants within a to-be-texted zip code.

// A library that interprets files, like our .json
const fs = require("fs");
// Returns all restaurants
function getRestaurants() {
return new Promise((resolve, reject) => {
fs.readFile("restaurant_data.json", "utf8", (err, data) => {
if (err) {
reject(err);
} else {
const json = JSON.parse(data);
resolve(json);
}
});
});
}
// Returns all zip codes (so we can compare a user's input)
async function getZips() {
// Load all the restaurants
const data = await getRestaurants();
return data.Restaurants.map((restaurant) => `${restaurant.Zip_Code}`);
}
// Return all restaurants within a given zip code
async function getZipRestaurants(zip) {
// Load all the restaurants
const data = await getRestaurants();
// Filter through all restaurants, finding ones in the zip
const zipRestaurants = data.Restaurants.filter(
(restaurant) => restaurant.Zip_Code == zip
);
// Return a formatted list
return zipRestaurants.map(
(restaurant) => `${restaurant.Name}: ${restaurant.Phone}\n\n`
);
}
module.exports = {
getRestaurants,
getZips,
getZipRestaurants
};
view raw data-model.js hosted with ❤ by GitHub

With those functions exported, I can call them in routes.js. The routes tell our API where to look for data, and what to do when data is found. Since we’ll be using Twilio, and I require it in line 4, I ran npm i twilio here.

I created two GET requests just to confirm data existed, one for all restaurants and one with a specific restaurant zip code. Then, I wrote a POST request to create a new text message based on an input. If the input isn’t in our list of zip codes, the POST request returns an error message.

const express = require("express");
const router = express.Router();
const model = require("./data-model");
const MessagingResponse = require("twilio").twiml.MessagingResponse;
const bodyParser = require("body-parser");
router.use(bodyParser.urlencoded({ extended: false }));
// Helper function: wraps another function in try/catch and passes errors to middleware
function asyncHandler(cb) {
return async (req, res, next) => {
try {
await cb(req, res, next);
} catch (err) {
next(err);
}
};
}
// Get all restaurant names
router.get(
"/restaurants",
asyncHandler(async (req, res) => {
const restaurants = await model.getRestaurants();
res.json(restaurants);
})
);
// Get all restaurants in a zip code
router.get(
"/restaurants/:zip",
asyncHandler(async (req, res) => {
const restaurants_in_zip = await model.getZipRestaurants(req.params.zip);
if (restaurants_in_zip) {
res.json(restaurants_in_zip);
} else {
res.status(404).json({
message: "No restaurants found in your zip code! Please enter another.",
});
}
})
);
// Receive a zip code, and return a list of restaurants
router.post(
"/sms",
asyncHandler(async (req, res) => {
const twiml = new MessagingResponse();
// Store the body of the user's text as a variable
const zip = req.body.Body;
// Load zip codes
const validZips = await model.getZips();
// If the zip is not in our valid zip codes list, return an error
if (!validZips.includes(zip)) {
twiml.message(`Hmmm, I'm not finding any restaurants open in ${req.body.Body}`);
twiml.message(`Could you please try another five-digit Bay Area zip code?`);
}
// But if it is, return the list of restaurants
else {
const restaurants_in_zip = await model.getZipRestaurants(zip);
twiml.message(
`Thanks for eating local❣️ Here are the restaurants open in ${req.body.Body}:`
);
// Formatting: remove the commas in the returned array with an empty space
twiml.message(restaurants_in_zip.join("").toString());
}
res.writeHead(200, { "Content-Type": "text/xml" });
res.end(twiml.toString());
})
);
module.exports = router;
view raw routes.js hosted with ❤ by GitHub

I tested the routes locally. When I confirmed I could see the right restaurants returned for my zip, I deployed to Heroku from Github. With a successful Heroku deploy, I turned to Twilio.

Set up a Twilio Phone Number

Texting in a retro phone

Developers use Twilio to programmatically send and receive calls and texts, but the limit really does not exist. Chloe Condon and I once used it to build a Mean Girls’ day bot, and Twilio Champions get up to all kinds of projects.

Sign up for an account if you don’t have one already. You’ll also need to pick a Twilio Phone Number, which you can set up from your Console. I recommend picking a number with an area code your users will be familiar with, so for me that was (415).

Now it’s time to configure your number. Head to Phone Numbers / Manage Numbers / Active Phone Numbers, and click on the number you set up. Scroll down to Messaging. Select Configure with Webhooks…, and when a message comes in, set the Webhook to be a HTTP POST request to your Heroku endpoint. Hit Save.

Alt Text

And with that, you should be ready to send a text!

What's next

There is so much I can do to make this better. As I mentioned, I would love help making staying on top of which restaurants are open and closed more automated. If you have ideas and want to help, please send me a DM, or file a Github issue.

Most of all, if you wind up replicating this in your city and are running into any challenges, please let me know! I would be more than happy to help debug, and am just a Zoom pair programming session away.

Virtual hug loading onscreen