DEV Community

Paul Gowder
Paul Gowder

Posted on • Originally published at paultopia.github.io on

The Easiest Possible Way to Throw a Webapp Online (Flask + Heroku + Postgres)

Here's a situation that comes up a lot. I want to throw together a quick CRUD app, or, for my uses, more of a C app (i.e., "hey research assistants, dump all this stuff into a database"). I could use a google form or something, but that always leads to weird output, like bizarre formatting, out in google sheets.

So I'm getting into Flask on Heroku for this kind of thing, for several reasons:

  1. No wrangling around with database connections and such. You don't even need to give your code the environment variable to connect to postgres if you use the flask-heroku library.
  2. Making changes is seriously just pushing to a git remote.
  3. You can use interact with a real database shell via heroku pg:psql, or you can write canned queries and look at them online, or you can even sync them to google sheets (though that last one seems a bit shaky).
  4. Logs are just as simple as heroku logs and logging something from the application is just print().
  5. No mess with ngnix and other server config stuff.
  6. HTTPS is handled for you too, at least on applications served from herokuapp.com. Which is real nice, obviously.
  7. By the way, you get a falls-asleep-a-lot-but-good-enough-for-tiny-projects + 10k postgres rows for free.

So let's walk through the simplest possible setup.

Step 1: setup and install all the things.

(Installation details will depend on your platform, so I'll leave that for you to google.)

  1. Get a Heroku account and install the heroku cli.
  2. Make sure all your code is in a git repo, as we'll be using git to push to Heroku.
  3. Install postgres locally, so you can use Heroku psql, pull down from Heroku to a local database, etc.
  4. Set up a python virtual environment. This is usually a good idea anyway, but particularly important here because you'll be using pip freeze to get the requirements for Heroku, so you want to make sure you have a self-contained and reproducible environment.

I'm a big anaconda fan, so I just use conda to handle this for me, but other virtual environment managers should work fine too.

Incidentally, the following code assumes the latest version of Python 3. It'll probably work fine on Python 2 as well, but who really knows?

  1. Pip-install the following libraries.

Flask — of course, this is a Flask-based tutorial. It's about the easiest possible way to get basic web stuff happening in Python.

Flask-Heroku — this is just a very simple library that takes care of getting the heroku environment variables and passing them to your flask application in the right way. Not strictly necessary, but why make life harder for yourself?

SQLAlchemy — the standard library for connecting to a relational database in Python. It's really complicated, but it seems to work fine for me while only brushing the absolute surface, and with the help of the next library.

It's worth noting that SQLAlchemy, contrary to the Zen of Python, seems to have a million different ways to do everything, so different tutorials and documentation might have slightly different approaches to the table creation syntax and such.

Flask-SQLAlchemy — simplifies the SQLAlchemy API for Flask purposes.

Psycopg2 — Postgres database driver.

Gunicorn — just a communication layer between the server and your code, probably not strictly mandatory in quick-and-dirty hack-together apps with only a couple of users, but never hurts and can help manage lots of requests at once should they happen. For more, see this explanation of WSGI, and this real-life account.

Step 2: Write some code

Let's do a basic app, shall we? For the purposes of our minimal example, let's stick all of this in a file called app.py.

A. the basics:

from flask import Flask, render_template, url_for
from flask_sqlalchemy import SQLAlchemy
import sys
import json
from flask_heroku import Heroku
app = Flask( __name__ )
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
heroku = Heroku(app)
db = SQLAlchemy(app)
Enter fullscreen mode Exit fullscreen mode

Most of the work here is just creating objects in the global namespace that will hold your app and database.

The Heroku(app) line just puts your environment variables where they need to be.

The app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False line is to fix performance hits from a bad default config (and silence an annoying warning).

The rest should be pretty clear.

B. Set up a database

class Dataentry(db.Model):
    __tablename__ = "dataentry"
    id = db.Column(db.Integer, primary_key=True)
    mydata = db.Column(db.Text())

    def __init__ (self, mydata):
        self.mydata = mydata
Enter fullscreen mode Exit fullscreen mode

Pretty straightforward. You create a class that inherits from the model class that your db object brought in, and then you put the table schema into the fields of that class.

SQLAlchemy will happily auto-increment an integer primary key for you.

Whatever columns you want, you can declare and pass their type. SQLAlchemy has a bunch of types available, including all the basics, but also database-vendor-specific types, different flavors of datetimes, etc.

Then the constructor (ok, really the initializer) for an object just sets the properties based on data received from users.

Hey, maybe we should get some data?

C. Set up a route to receive data

I'm assuming here that we're just using a standard HTML form that generates a post request, so let's set up a route to receive that.

@app.route("/submit", methods=["POST"])
def post_to_db():
    indata = Dataentry(request.form['mydata'])
    data = copy(indata. __dict__ )
    del data["_sa_instance_state"]
    try:
        db.session.add(indata)
        db.session.commit()
    except Exception as e:
        print("\n FAILED entry: {}\n".format(json.dumps(data)))
        print(e)
        sys.stdout.flush()
    return 'Success! To enter more data, <a href="{}">click here!</a>'.format(url_for("enter_data"))
Enter fullscreen mode Exit fullscreen mode

Ok, this one is a little more complicated.

The first line is just a decorator that tells Flask that the function below services a route, gives it the route to service (see the Flask docs for all the interesting stuff you can do with routes), and tells it what methods to accept.

The function initializes that object we created above, and passes it the data we received on our form.

Note that this is as fields on a request object, which is actually a global. This is a Flask thing, and I think it's a real WTF design decision—Flask should make you pass it into the function as a parameter, but the Flask people made the choice to make it a global, so a global it is.

Then I have some real seat-of-the-pants error handling in here. Heroku doesn't let you save data to a filesystem—your choices are database or nothing. But it does have built-in logging (though it only keeps something like 1500 lines of logs unless you pay for a service from someone to hold onto more), and it makes it real easy to get it: anything your application saves to stdout goes into a log.

Since database writes can be finicky, I'm just catching all errors and logging them, along with all the data that it attempted to write. (And since users don't need to know that the database write failed, I'm telling them to keep going.)

Some of the slightly more obscure mechanics:

The return sends some html to display to the user. Here, I just use a raw string in order to give some feedback to confirm to the user that, yes, they actually managed to submit the form, and then prompt them to submit some more data if they want.

Note that the Flask url_for function can take a function corresponding to another route, and then intelligently insert the url there. So this will insert the url for the route corresponding to the enter_data function, which we should probably write...

C. Set up a route for users to enter data

@app.route("/")
def enter_data(): 
    return render_template("dataentry.html")
Enter fullscreen mode Exit fullscreen mode

That was pretty easy, wasn't it? This will render a Jinja2 template to the user. Which should, obviously, provide them with a form to add the data. Here's an example of a minimal template:

<html>
    <head>
        <title>data entry</title>
    </head>
    <body>
        <form method="POST" action="{{ url_for('post_to_db') }}">
            <label for="mydata">Gimme your data, fool!</label>
            <input type="text" id="mydata" name="mydata">
            <button type="submit">IT FEEDS IT THE DATA</button>
        </form>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Nothing should be surprising there, with one exception: note that Flask is kind enough to inject the url_for function into the template, so you can decouple your view from whatever you do with routes and whatever server you happen to be running on and so forth.

D. Finish it off

if __name__ == ' __main__':
    #app.debug = True
    app.run()
Enter fullscreen mode Exit fullscreen mode

When it's called from the commandline run the app.

I stuck the commented-out line in there to reveal one of the other really sweet things about Flask: it has an amazing debugger; if you run it in debug mode then when something blows up you'll be able to inspect the state right from the web page it generates. Obviously don't use this in production, unless you want to make life easy for malicious actors.

That's it, that's all the code we need for a minimal app!

Step 3: Setup for heroku

You'll need a requirements.txt file to tell Heroku what libraries you need.

pip freeze > requirements.txt

That was easy. You'll also need a Procfile to tell Heroku what to run:

echo "web: gunicorn app:app" > Procfile

It's nice to have a .gitignore, especially if you're also going to put it on github. Here's my minimal Mac users .gitignore:

*.bak
*.pyc
.DS_Store
Enter fullscreen mode Exit fullscreen mode

Step 4: Deploy!

Now you need to commit all this stuff, and then once it's all committed, just heroku create CHOOSEYOURNAME. That will create an application with whatever name you give it in that last argument. Then git push heroku master will get it on Heroku.

It really is that easy! The output of the heroku create command, incidentally, should give the url of your app, which will probably be something like chooseyourname.herokuapp.com.

There's one more step, however, and that's to setup the database. Two substeps:

  • Create the database in Heroku. The free flavor is the hobby-dev one (that's where you have a 10,000 row limit). Everything else is actually quite expensive. That's as simple as heroku addons:create heroku-postgresql:hobby-dev

(If you have more than one database, you'll have to do a little bit more work to connect it, but for a simple thing, you shouldn't have to bother.)

  • Create the tables in your new database. The easiest way to do this is to just fire up a Python REPL right on the heroku server and create the tables from within the app: heroku run python from your command line, and then, in the repl, from app import db and db.create_all().

That's it! You're done! If everything went well, the app should be live and functioning.

What Now?

  • If you want to update the (non-databasey) code in the application, it's as simple as pushing new changes to the Heroku remote.
  • To see the logs, go to the repo and do heroku logs.
  • To get a database shell, do heroku pg:psql.
  • To see the data online, probably the easiest approach is to use dataclips to see saved queries.
  • To actually get the data locally, you can use heroku pg:pull to pull down data locally. See the Heroku Postgres docs linked below for more details on what to give to that command.

Wait a minute, what about local dev, testing, etc.?

I'm not going to cover all that stuff, because this post is too long as is, but it's probably a good idea to set up local Postgres with an actual connection so you can test before deploying. For more information, see the references below.

Some people create separate git branches for the local version of the application and the Heroku version.

Useful References

Top comments (7)

Collapse
 
jvarness profile image
Jake Varness

Out of genuine curiosity, in your opinion, what separates Python and Flask from Rails or Amber (a Crystal web framework)?

I feel like all would be extremely simple ways to deploy heroku apps, so as someone who doesn't write much Python I'm just wondering what your opinions are.

Collapse
 
paultopia profile image
Paul Gowder

Across languages, I would think that Flask would be most comparable to something on Node, like maybe Express.js---just a super-lightweight "here are some tools to define routes and responses, go to town" kind of a framework.

Collapse
 
jvarness profile image
Jake Varness • Edited

That's useful info. Good comparisons! Thanks for the input! I might need to try this out myself

Collapse
 
paultopia profile image
Paul Gowder

I know nothing about Crystal---but with respect to Rails and such, the comparison I usually hear is that Django is the Python web framework most comparable to Rails. If that's right, then the difference is really going to be about batteries-included (and the price of that being a high learning curve) vs minimalism and a learning curve of basically zero.

Fundamentally, if you know Python, you can get a useful Flask app up within 15 minutes of glancing at the documentation for the first time. It won't be able to do a lot, but it'll at least be able to take a request, handle the data sent to it, and return an appropriate response. And if you want to do something else, like say talk to a database, you don't need to learn a special Flask way of doing it (there's nothing like Django's built-in ORM), it's just the database tools you already know.

Most of the Flask add-on libraries out there seem like just thin wrappers over existing libraries. Like Flask-SQLAlchemy is basically just a couple convenience functions.

Collapse
 
fibonaccisequence profile image
Tilman Nathaniel

I tried this but am running in an error that says ModuleNotFoundError: No module named 'flask_heroku'. When I tried installing it in my virtual environment it says requirement already satisfied. Any idea what's going on?

Collapse
 
1uc1f3r616 profile image
Kush Choudhary

I don't need to convery my Flask-sqlalchemy database to postgre?

Collapse
 
krishnasivakumar profile image
Krishna Sivakumar

Yeah, the queries stay the same. Although, you need to install psycopg2