Minimal Web Application with Flask and AlpineJS

I recently needed to develop a small application for personal use. Since I had a specific need to use Python, I decided to build the backend with Flask. For the frontend, I only needed (literally) a single page to visualise some data, so I resisted the urge to go with something like Angular or React, and instead decided to try this minimal Alpine.js thing I had heard about a few days earlier.

In this article, I’m going to show you how you can really quickly create a minimal web application (REST API, static files and web UI) like I did. This can be handy for putting a simple tool together in very little time, but please consider it as a quick and dirty thing rather than any kind of best practice. I’m new to both Flask and Alpine.js, so I’m certainly not going to be giving advice on how best to use them.

Getting Started with Flask

After creating a new folder for your project, create a file called main.py (or whatever you want, really). We can kick off our API development by using the Flask Quickstart example:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

The same page also explains how to run the application so that it can listen for connections and serve requests:

“To run the application, use the flask command or python -m flask. You need to tell the Flask where your application is with the --app option.”

— Flask Quickstart

This is nice and all, but I like to be able to debug my code from Visual Studio Code, and for that it’s necessary to create a launch.json file with the right settings. This can be done as illustrated by the following images and their captions. If you need to do anything more advanced, see “Working with VS Code Launch Configurations“.

First, select the Debug tab on the left, and click “create a launch.json” file.
From the list that appears, select “Flask”.
Change the default suggested filename of app.py to the one you’re using, e.g. main.py.

Once that is done, it generates a launch.json file with the following contents:

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Flask",
            "type": "python",
            "request": "launch",
            "module": "flask",
            "env": {
                "FLASK_APP": "main.py",
                "FLASK_DEBUG": "1"
            },
            "args": [
                "run",
                "--no-debugger",
                "--no-reload"
            ],
            "jinja": true,
            "justMyCode": true
        }
    ]
}

You can now press F5 to start the application in debug mode. The integrated terminal informs you that the application is running at http://localhost:5000/, so you can open a browser window to see that it’s working. By running it in this way, you can also add breakpoints and debug your Python code as needed.

The Flask “Hello World” is definitely working.

A Simple REST API

To build a reasonable example, we need to return some data from our API. We can do this quite easily by returning a list of dictionary objects from a function annotated with the route “/products”:

@app.route("/products")
def get_products():
    return [
        {
            "name": "Rope",
            "description": "Used to climb heights or swing across chasms.",
            "price": 15
        },
        {
            "name": "Whip",
            "description": "An old and trusty friend.",
            "price": 20
        },
        {
            "name": "Notebook",
            "description": "Contains lots of valuable information!",
            "price": 80
        }
    ]

After restarting the application and visiting http://localhost:5000/products, we can see that the list gets translated quite nicely to JSON:

The response of the /products endpoint.

Serving a Static Webpage

It is now time to build a simple user interface. Before we do that though, we need to figure out how to serve static files so that we can return an HTML file and then use Alpine.js to dynamically populate it using the API.

It turns out that this is quite simple: Flask automatically serves any files you put under the static folder. So let’s create a static folder and add an index.html file with some minimal HTML in it:

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>My Products</title>
<body>

<h1>My Products</h1>

</body>
</html>

We can now see this page if we visit http://localhost:5000/static/index.html in a browser:

The HTML page is served from the static folder.

It works, but it’s not great. I’d like this to come up by default when I visit http://localhost:5000/, instead of having such a long URL. Fortunately, this is easy to achieve. All we need to do is replace our “Hello world” code in the default endpoint with the following:

from flask import Flask, send_from_directory

app = Flask(__name__)

@app.route('/')
def serve_index():
    return send_from_directory('static', 'index.html')

Displaying the Products

Now that we have an API and an easily accessible webpage, we can display the data after retrieving it from the API. We can now add Alpine.js to our website by adding a <script> tag, and then build out a table that retrieves data from the API:

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>My Products</title>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
    body {
        font-family: sans-serif;
    }

    table, th, td {
        border: solid 1px #555;
        border-collapse: collapse;
    }
</style>
<body>

<h1>My Products</h1>

<div
    x-data="{ products: [] }"
    x-init="products = await (await fetch('/products')).json()"
>
    <table>
        <tr>
            <th>Name</th>
            <th>Description</th>
            <th>Price</th>
        </tr>
        <template x-for="product in products">
            <tr>
                <td x-text="product.name"></td>
                <td x-text="product.description"></td>
                <td x-text="product.price"></td>
            </tr>
        </template>
    </table>
</div>

</body>
</html>

In the above HTML, the main thing to look at is the stuff inside the <div>. Outside of that, the new things are just the <script> tag that imports Alpine.js, and a little CSS to make the webpage look almost bearable.

The <div> tag’s attributes set up the data model that the table is based on. Using this x-data attribute, we’re saying that we have an array called “products”. The x-init attribute is where we use the Fetch API to retrieve data from the /products endpoint and assign the result to the “products” array.

Once this data is retrieved, all that’s left to do is display it. In the <table>, we use an x-for attribute in a <template> to loop over the “products” array. Then, we bind the content of each cell (<td> element) to the properties of each product using the x-text attribute, effectively displaying them.

The result is simple but effective:

Indiana Jones would be proud of this inventory.

Conclusion

As you can see, it’s pretty quick and easy to put something simple together and then build whatever you need on top of that. For instance, you might want to filter the data you display, apply conditional highlighting, or add other functionality.

Honestly, I’m not quite sure how far you can take Alpine.js. For instance, it doesn’t seem possible to create reusable components, which somewhat limits its usefulness. And I don’t yet know how to avoid having heaps of JavaScript within HTML attributes. I’ve only started using this a few days ago, after all.

But it’s great to be able to use something like this to create simple tools without having to incur the learning curves of things like Angular, React or Vue.

One thought on “Minimal Web Application with Flask and AlpineJS”

Leave a Reply

Your email address will not be published. Required fields are marked *