With all of the wonderful APIs available to developers today, building modern web applications can almost feel like using magic. In this article, we'll use three APIs in particular to build a service that allows users to sign up and receive text messages with the weather forecast every morning. The code we write will mostly be glue between the services, we'll rely on the third-party APIs for most of the heavy work.
If you'd like to see a live version in action, this is a project I launched a few months back called Rooster, check it out!
We'll be using the following APIs:
- Google's Geocoding API for turning user-entered information into precise latitude and longitude information.
- Forecast.io's Forecast API for aggregating weather data across multiple services and stations and returning human-readable forecasts.
- Twilio's SMS API for reliably delivering messages across dozens of different carriers around the world.
Before we proceed much further, take a minute to realize how complicated each of those tasks is on its own. Our application will be standing on the shoulder of giants, allowing us to perform some amazing lookups, data analysis and integrations by offloading that work to someone else. This allows us to build an app in a weekend -- instead of years -- and focus on usability, presentation and maybe a bit of marketing.
tl;dr: Grab the Python code on GitHub and sign up for a developer account with Forecast.io and Twilio. If you'd like to make it publicly accessible on the web, Heroku makes deployment for these kinds of simple projects very easy.
Defining our Models
With most projects, it helps to start by defining the data you'll be processing and storing. For this app, things stay pretty simple.
For each user, we'll need to know their phone number and location, as well as the time they'd like to receive their message every day, with proper timezone information. We'll also want to keep track of when they signed up and whether they're still active, which allows users to disable the messages temporarily without deleting themselves from the system completely.
We'll also probably want to keep track of each of the messages we sent out. This will allow us to make sure we don't accidentally send a single user a ton of messages in a row. Obviously we want to keep track of the text of the message, as well as who it was sent to and when. We'll tag messages with "categories" so that we can track things like forecast messages versus status messages differently.
Note that if we were sending something more sensitive than forecast information, it might be wise to store a hash of the message, rather than the plain text. This would still allow us to make sure we don't send the same message multiple times, but would protect the users' information in the event someone gains unauthorized access to our database.
Choosing the Right Tools
At this point, we know we'll be leveraging a lot of third party APIs for the heavy lifting and that our data models are pretty simple. We'll also try to make our app heavily SMS-based -- a user can update all of his or her information by texting it to us -- so there's no need for a complex website. For a situation like this, it's useful to choose a lightweight web framework, that comes without all the baggage and cruft of something more heavy-handed.
Since I wanted to keep things in Python for this project, I chose the Flask micro framework. Flask is built on top of Werkzeug for elegant request/response handling and Jinja2 for easy HTML templating. Our entire Flask application can live inside a single file.
For rapid front-end prototyping, we'll use Twitter's battle-tested Bootstrap framework. To make the landing page look a little less generic, I found an amazing creative commons photo on Flickr of a Rooster weathervane.
For the back-end services, we'll use Heroku for hosting the site and sending out messages 24/7. We'll also use Postgres as our database for storing information about our users and the messages we've sent them. For most of my projects, I usually use Sentry to notify me of any exceptions that happen in production -- they always do -- and Pingdom to notify me if the site ever goes offline.
We haven't even written a line of code yet and we already have an amazing array of power and functionality at our finger tips. Plugging those pieces together is our job.
The first thing we'll want to do is turn our models into code so that they can be persisted to a database. Our models will also contain all of the business logic of our application. For example, our User model contains a method that determines whether we should send that user a forecast right now, a method for sending a generic message to that user and a method for generating and sending a forecast message. The vast majority of our application's logic lives in those three methods calls. This makes it really easy and elegant to perform our application's main business logic, which we'll see when we talk about the worker, later.
Once we have our models defined, it's time to think about how a user will interact with those models. In our application, there are two main ways: through text messages and via the public website. We'll define two routes in our Flask app for responding to requests to both of those endpoints.
When a user responds to one of our messages, Twilio will pass that message back to our application -- to an endpoint we provide. Since we don't want our users' texts to go unanswered, we'll want to parse that incoming text and send them a response, either confirming that we did what they asked us to do, or asking for more information. Because we're processing instructions from a 160 character text response, the logic gets a bit hairy, but you can read through it here.
The other way users will interact with our applications is through the homepage route, which will render our landing page template on GET requests, and attempt to create a user on POST requests. Since there are a million tutorials on the web for building and responding to forms, I won't cover that here. I will mention one thing briefly though, and that's form validation.
Validating Phone Numbers
Form validation is important for two main reasons:
- It prevents bogus data from entering your system
- It allows you to notify users who might have made a typo or other mistake so they can fix it and try again.
Some types of inputs are very easy to validate -- phone numbers however, are extremely difficult. Every country has their own standard for punctuation, some use dashes and parenthesis and others use pluses and spaces. Trying to write a regular expression or do string parsing to determine whether something is a correct phone number gets chaotic quickly.
I ended up in the weeds pretty quickly, reading technical specifications and trying to build logic around what a phone number should look like. But then I had an epiphany. I simply needed to ensure the format of the phone number was something that Twilio could understand and send a message to. Why not use them for the validation -- if they could understand the phone number, then it's fine. If their API returns an error, then return it back to the user to try again.
So I decided to add a welcome text message to the sign up flow. When a user clicks "Register," we strip out all the punctuation from their phone number and try sending a welcome message through Twilio's API while the page is loading. If Twilio's API returns an error, then we immediately return and tell the user that the phone number looks wrong. If it goes through just fine, we show the user a success confirmation message, telling them to expect a text in a few seconds.
Adding a welcome message also ensures people don't make a typo and accidentally sign someone else up for the service. The welcome message has instructions for deactivating the service to that number, so a random person receiving it in error knows how to opt-out immediately. Also, the fact that the website tells a user to expect a message means that they might notice something's wrong if they don't get a text soon.
The welcome text allows us to validate the user's phone number as well as provide immediate feedback to get them used to the app.
Tying it All Together
At this point, we've written most of the functionality that our app really needs. The next step is integrating all of the different services into our app. There are some existing API clients available, but since our needs aren't very complicated, I decided to write my own API wrappers for Rooster. The Twilio SMS wrapper and Google Geocoding wrapper are pretty straightforward and readable. The Forecast.io wrapper contains a bit more logic for taking the forecast response and formatting it into something that fits into a 160 character text message -- pulling out the important pieces like current temperature, today's high temperature, and other information about today's weather like precipitation and severe weather alerts.
Now that all of our data and integrations have been built, the final step is to create the worker job that will run every few minutes and send out messages to users at their scheduled time. The code for that worker lives in worker.py, allowing us to run the worker with a simple
python worker.py command. Since we've done a good job putting logic in our models, this code is really straightforward. We look at each user and see if they need a message right now, and if so we send one.
If you take a look at the
needsmessagenow() method on the User, you'll notice that it does some fuzzy matching on the time, sending a message if the worker is run within about 8 minutes before or after the time the user wanted to receive the message. This means that our worker doesn't need to run exactly every 15 minutes, on the dot. This makes our system more resilient in case a worker is delayed. In fact, the Heroku scheduler that we use for running the worker process explicitly says that it isn't perfectly exact and your application should guard against that.
The other thing we check is to make sure we haven't already sent the user a forecast message for today. This way, if a user is scheduled to get a message at 8:15am and the worker runs at 8:08am and then again at 8:22am, we don't send them two messages in a row.
At this point, we've gotten all of the functionality we need, and our app is ready to go. The project includes both a Procfile and requirements.txt file so it can be easily deployed to Heroku. You want to make sure you configure the Postgres URL and the necessary API keys, and also setup the free Heroku scheduler add-on to run the
python worker.py command every 10 minutes.
Voila, you've got your own Rooster App clone!