from Guide to Hacking on May 28, 2023

How a web application works

Coding a hello world web application is pretty straightforward, but how do we go from there to production?

In this post, we'll cover how a web application works, then show how to evolve a hello world application into a minimal production-ready one. Critically, we'll discuss how to setup a pipeline for deployment and share best practices.

What is a web application?

And most importantly, how is it different from a website? Some sources define a web application generically as "software that runs in your web browser". Many other sources have a similarly vague and incorrect definition. However, the difference between a website and a web application has nothing to do with complexity or capability. Instead, Wikipedia defines it most clearly:

Here's an example that illuminates the difference. Say we have two blogs, identical in nature as far as you can tell. However, one blog could be a website, and the other could be a web application.

This distinction isn't particularly important to remember, beyond this post. However, I make this distinction, because this post focuses on web applications in particular, where server-side processing is required.

Web server on your computer

Let's now build a hello world web application. We'll build this locally on your computer. Create and navigate to a directory for your project. Below, we'll use a directory on our desktop.

mkdir ~/Desktop/webapp
cd ~/Desktop/webapp

Next, create a virtual environment in this directory.

python -m venv env
source env/bin/activate

In this environment, install your Python dependencies. For now, our project has just one dependency: Flask, a minimal Python web framework.

pip install Flask==2.3.2

Create a minimal application, which comes from the official Flask tutorial. Save this in a file called server.py.

webapp/server.py

from flask import Flask

app = Flask(__name__)  # init web app

@app.route("/")  # define webpage with / URL
def hello_world():  # function run when user accesses /
    return "<p>Hello, World!</p>"  # return simple HTML webpage

if __name__ == '__main__':
    app.run(debug=True)  # start the web app

The debug=True keyword argument ensures that every change in our source code is immediately reflected in the locally-deployed server. It also enables more useful client-side errors when there's a bug.

Check your source code against our reference project for this "hello world" step. Run our new script to launch the server locally.

python server.py

At this point, you can access your locally-hosted web application in your browser, at http://127.0.0.1:5000. You've now launched your very first, hello world web application.

Anatomy of a web application

Above, we defined a web application as software that requires both client-side and server-side processing. In this sense, a web application has two "tiers" of processing. However, web applications are typically broken up in there are three tiers:

  1. Presentation tier: This is effectively the client, which handles rendering content on a webpage. For us, this is simply your browser.
  2. Middle or application tier: This is "middleware" which handles dynamically generating and collating data for the webpage. For us, this is Python running in Flask.
  3. Storage tier: This is a database that handles data, both storage and access. For us, this will be a SQLite database that we'll add shortly.

Due to how the web application is organized into these three tiers, code in the application tier is also organized into three groups, called the MVC software design pattern:

We will expand our web application to feature all three tiers, as well as identify different parts of our code that reflect the MVC software design pattern.

Web application on your computer

To complete our three tiers above, add a database to your web application. For simplicity, we'll use a minimal database called SQLite.

Start with a script that initializes your database. Our application will store and track todos:

  1. Create a table, which specifies the format for your stored todos.
  2. Insert rows of data into your table. In this case, every row of data is a todo.

Write this script to a file named init_db.sql.

webapp/init_db.sql

-- Delete old table, in case you run the script multiple times.
DROP TABLE IF EXISTS todos;

-- Define format for todos (id, text, and time it was created at).
-- Below, we define defaults for id and created_at
CREATE TABLE todos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    text TEXT NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Insert some todos. Only specify text, because id and created_at
-- are automatically populated.
INSERT INTO todos (text) VALUES ("laundry");
INSERT INTO todos (text) VALUES ("walk the dog");

In the script above, we use a language called SQL, which you can learn more about in SQL 101: Introduction to Databases.

Execute this SQL script to initialize your todo format and create a few todos.

sqlite3 todos.db < init_db.sql

With our database setup, we can now update our web application to read todos from this database. Since our webpage is now getting more complex, we'll write the returned webpage in a separate file called templates/index.html.

webapp/templates/index.html

{% for todo in todos %}  <!-- for every todo -->
<li>{{ todo[1] }}</li>  <!-- show the todo text in a list-item (li) -->
{% endfor %}

The above is a template, a mix of HTML — the <li> tags — as well as some presentation logic — the for loop1.

Then, in your server.py, import the SQLite library and a Flask utility for rendering webpages.

webapp/server.py

from flask import Flask, render_template
import sqlite3
app = Flask(__name__) @app.route("/") def hello_world():

Update your homepage to grab all todos and return the rendered template.

webapp/server.py

import sqlite3 app = Flask(__name__) @app.route("/")
def hello_world(): conn = sqlite3.connect('todos.db') # connect to our database stored in todos.db todos = conn.execute('SELECT * FROM todos').fetchall() # grab all rows from our todos table return render_template('index.html', todos=todos) # render our template with the todos we grabbed
if __name__ == '__main__': app.run(debug=True)

Check your source code against our reference implementation of this "sqlite" step. Launch your web application.

python server.py

Then, open http://127.0.0.1:5000 to see the todos being populated on your homepage.

For our final part of this step, we'll support the ability to add todos. Start by augmenting your template to include a submission form.

webapp/templates/index.html

<form method="post">  <!-- when form is submitted, send a POST request -->
    <input name="text">  <!-- input field with name 'text' -->
    <input type="submit" value="Add todo">  <!-- Submit button. The buttons says 'Add todo' -->
</form>
{% for todo in todos %}
<li>{{ todo[1] }}</li>
{% endfor %}

Then, import another Flask utility that will tell us what kind of incoming request your web application is receiving.

webapp/server.py

from flask import Flask, render_template, request
import sqlite3 app = Flask(__name__) @app.route("/", methods=['GET', 'POST']) # accepted both GET (normal) and POST (form submission) requests

Augment your webpage to insert a todo when the form is submitted.

webapp/server.py

from flask import Flask, render_template, request import sqlite3 app = Flask(__name__)
@app.route("/", methods=['GET', 'POST']) # accepted both GET (normal) and POST (form submission) requests def hello_world(): conn = sqlite3.connect('todos.db') if request.method == 'POST': # if form submission conn.execute('INSERT INTO todos (text) VALUES (?)', (request.form['text'],)) # define a command that creates a new todo, where the text comes from the input field named 'text' conn.commit() # run the command
todos = conn.execute('SELECT * FROM todos').fetchall() return render_template('index.html', todos=todos) if __name__ == '__main__': app.run(debug=True)

Check your source code against our reference implementation of this "create todo" step. Launch your web application.

python server.py

Navigate to http://127.0.0.1:5000 and try adding todos to your list. This now completes your very first three-tiered web application.

Why isn't this production ready?

There are three issues that make our setup non-production-ready.

  1. Can't handle traffic: Your web application is running in single-threaded mode, meaning it can only serve one user as a time. Every other user needs to wait while all the previous users have their webpages generated and returned.
  2. Not accessible: Namely, you're the only person that can access your web application at the moment, as by default, random ports like 5000 are not exposed to the internet. Only you can access port 5000 on your machine.
  3. Ephemeral "server": Your web application is being hosted on your own computer. The moment you turn off your computer, the web application dies and no one can access it.

In the next few steps, we'll address these issues, promoting your development-only web application into a fully-fledged production-grade setup that anyone can use.

Production-grade on your computer

The default, built-in development server for Flask "is not designed to be particularly efficient, stable, or secure". As a result, you should instead using a production WSGI server. In our demo, we will use gunicorn2. To start, install gunicorn.

pip install gunicorn

Now, instead of running server.py directly, use gunicorn.

gunicorn -w 4 'server:app'

This automatically launches a server at http://127.0.0.1:8000. Access this in your browser, and you should find the web application running normally 3.

At this point, only you can access your web application, so let's add a development-friendly but secure way to grant public access.

Download ngrok at ngrok.com/download, or install via Homebrew if you have it.

brew install ngrok/ngrok/ngrok

Then, setup an ngrok account and add the authtoken.

ngrok config add-authtoken <token>

Finally, from your project directory, start a tunnel pointing to port 8000.

ngrok http 8000

The ngrok command will then print outputs similar to the following

output

Session Status                online
Account                       Alvin (Plan: Free)
Update                        update available (version 3.3.1, Ctrl-U to update)
Version                       3.1.0
Region                        United States (us)
Latency                       68ms
Web Interface                 http://127.0.0.1:4040
Forwarding                    https://82ff-2600-1700-45f0-1740-b19a-4866-c5a6-b421.ngrok-free.app -> http://localhost:8000

Connections                   ttl     opn     rt1     rt5     p50     p90
                              4       0       0.00    0.00    0.01    0.02

You can then provide the URL under Forwarding to anyone. In my case, I could access https://82ff-2600-1700-45f0-1740-b19a-4866-c5a6-b421.ngrok-free.app from another computer or from my phone. However, in this setup, your computer is the "server", so you have to keep your computer on to keep your website up. Let's fix that now by hosting your web application on a server in the cloud.

Production-grade in the cloud

For this final section, we'll setup your web application on a remote server so that anyone can access your application, 24/7. There are a number of different free options, such as pythonanywhere.com and glitch.com. For this tutorial, we'll use the latter.

  1. To start, create a copy of my Python Flask Starter project by clicking on this link: https://glitch.com/edit/#!/remix/python-flask-starter. No account creation is required to get started.
  2. For simplicity, copy your files into your Glitch project: init_db.sql, server.py, and templates/index.html.
  3. At this point, your Glitch project should automatically launch your server. This is because we've pre-populated start.sh to initialize the database, install requirements, and launch your web application.

By default, your Glitch editor should show a preview of your web application on the right-hand side of your screen. Here's an example of what this screen should now look like:

icon

On the top left, you should see a project name. In the screenshot above, the project name is python-flask-todo. As a result, my project's public URL is python-flask-todo.glitch.me, which you can share with anyone to access your web application. And now, you've got a public-facing, minimally production-ready web application to share with anyone.

For one final step, we'll setup a git-centric deployment pipeline. At the end of this step, you'll simply git push to deploy your application to production. I'd highly recommend following these steps — it's much simpler than copy-and-pasting your files in every time you make a change.

To start, add a few files locally that we used to setup the boilerplate code on Glitch. Add a requirements.txt with libraries that your project depends on.

Flask==2.2.5
gunicorn==20.1.0

Then, create a start.sh file with your setup instructions. Glitch will call this file automatically to startup your web application.

glitch/start.sh

sqlite3 todos.db < init_db.sql  # initialize database
pip3 install -r requirements.txt  # install Python deps
gunicorn server:app -w 1  # launch production WSGI server

This is now all of the files we need to launch your web application on a server.

On your Glitch project, allow overrides to make deployment to production automatic. In your Glitch project, click on Terminal in the bottom left, and in the console that pops up, type in the following to prepare your repository for deployments.

git checkout main
git config receive.denyCurrentBranch updateInstead
refresh

Now, we'll setup a git repository for version control and easy deployment. Start by initializing a git repository in your current directory.

git init -b main
git add .
git commit -m "initial commit"

Next, in your Glitch window, click on "Tools" in the bottom left. From the popup, click "Import / Export".

icon

Then, from the popup window, copy Your Project's Git URL, which should look like https://[email protected]/git/your-project-name. Add this as a remote to your git repository.

git remote add prod https://[email protected]/git/your-project-name
git push prod --force  # override remote with your first commit

Now, to deploy to production in the future, simply push to the prod remote.

git push prod

And that's it! You now have a minimal production-ready setup, complete with a git-centric deployment pipeline.

Conclusion

In this tutorial, we went from zero to hero, covering critical concepts and terminology, as well as building and deploying a web application. This pipeline should get you started for any web application you may wish to build. Here are a few different key concepts and terms to consider exploring next:

You can find the source code for this tutorial, as well as the finished code, on my "Guide to Hacking" repository. Happy building!


back to Guide to Hacking



  1. The presentation logic is made possible by another Python library, automatically installed with flask, called Jinja2. The template above is then rendered to produce an all-HTML webpage. 

  2. There are many options for production WSGI servers linked from the official Flask documentation

  3. For more information, check out the official Flask documentation for working with gunicorn