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:
- A website serves static content and requires just client-side processing. A client is just the end user, like you or me accessing a website. Our computers would be the clients, and client-side processing would be any work that our computer needs to do, to show you a webpage. A website only requires client-side processing.
- A web application can generate content dynamically — for example, by showing posts and uploads from users. More importantly, a web application requires both client-side and server-side processing. Namely, the server generates content for the client to view.
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.
- Static blog: A static blog is simple. Each webpage is generated in advance, then uploaded to a server. When you access the blog, the server simply returns the pre-generated page. There is no server-side processing — just client-side. As a result, we call this a website.
- Dynamically-generated blog: A blog could also be generated dynamically. Specifically, webpages are generated every time someone accesses the blog. When someone accesses a page, the server generates the corresponding webpage and returns it to the client to render. In this example, both server-side and client-side processing is required, so we call this 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
.
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:
- Presentation tier: This is effectively the client, which handles rendering content on a webpage. For us, this is simply your browser.
- 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.
- 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:
- Model: This is the data structure that represents your data. For example, this may be a
User
class that represents all information about a user. - View: This is any representation of information that is returned to the user, as part of an interface.
- Controller: This is logic that handles user inputs and appropriately modifies the view, model, or both.
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:
- Create a table, which specifies the format for your stored todos.
- 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
.
-- 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
.
{% 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.
from flask import Flask, render_template
import sqlite3
Update your homepage to grab all todos and return the rendered template.
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
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.
<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.
from flask import Flask, render_template, request
Augment your webpage to insert a todo when the form is submitted.
@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
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.
- 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.
- 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.
- 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.
- 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.
- For simplicity, copy your files into your Glitch project:
init_db.sql
,server.py
, andtemplates/index.html
. - 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:
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".
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:
- For faster queries and increased amounts of data, consider a more scalable database, either a relational database such as MySQL or a NoSQL alternative such as mongoDB. Setup is more involved, but a separate database management system (DBMS) running on its own server is more scalable.
- For additional features, see How third-party login works to add Google login or see How a Python-only desktop app works to use similar technologies to build a native desktop application.
- If you're building an API, consider using a library such as Flask-RESTful, which translates object-oriented programs into REST endpoints. This includes input validation and output specification, and with additional extensions such as flask-restful-swagger, you can even automatically generate documentation for your API.
You can find the source code for this tutorial, as well as the finished code, on my "Guide to Hacking" repository. Happy building!
-
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. ↩
-
There are many options for production WSGI servers linked from the official Flask documentation. ↩
-
For more information, check out the official Flask documentation for working with gunicorn. ↩
Want more tips? Drop your email, and I'll keep you in the loop.