How to Build a Complete CRUD app using Flask and Jinja2 in Python

Created on Jul 4, 2022

In a previous tutorial , we’ve seen how to build a backend of a CRUD app . In this tutorial, we will complete our full stack application by building the frontend of that app.

At the end of this tutorial, you will learn how to use Jinja2 template to render HTML templates and Bootstrap through Flask-Bootstrap extension by building a CRUD app which is a common task in web dev that allows you to create, read, update, and delete things. I’ve made it a useful RESTful API which is a bookshop app built by Flask .

I’ll also offer something for free right at the bottom. Stay tuned!

This tutorial is based on Flask and Flask-Bootstrap extension .

Flask is a lightweight web framework that allows you to build web apps in Python. You can use extensions to extend the functionality of your app like Bootstrap that we will going to use in this tutorial.

Bootstrap is a powerful frontend toolkit that you can use to create stunning apps.

In this tutorial, we will build this app:

Revisiting the backend

Let’s revisit the backend app introduced in the previous tutorial.

As we can see, our Flask API has 5 functions that depend on the Book table. You’ll see how these functions will invoke appropriate methods from SQLAlchemy. Let’s first see how we structure the API functions. Those functions will be invoked by the Flask API and will be decorated with the @app.route decorator. The mappings for each are shown below:

Creating the navigation bar

Let’s start with the navigation bar which we import from Bootstrap. To be able to use Bootstrap in our HTML template, you need to import it in the Jinja2 template. Let’s put the following in the index.html file:

{% extends "bootstrap/base.html" %}

You can set the title of our app with the following command:

{% block title %}Bookshop{% endblock %}

Add the following for the navbar block:

{% block navbar %}
<nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar"
                aria-expanded="false" aria-controls="navbar">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">Bookshop</a>
        </div>
        <div id="navbar" class="collapse navbar-collapse">
            <ul class="nav navbar-nav">
                <li class="active"><a href="/">Home</a></li>
            </ul>
        </div>
    </div>
</nav>
{% endblock %}

Let’s now set up the parent root to render the index template:

@app.route("/")
def index():
    return render_template("index.html")

Now, your navbar would look like the following:

Creating the table

We want to create a table like this:

To create such a table, use the bootstrap version of table and wrap it inside the content Jinja2 block like the following:

{% block content %}
<div class="container">
    <div class="row">
        <div class="col-md-6">
            <table class="table" border="1">
                <thead>
                    <tr>
                        <th>ISBN</th>
                        <th>Author</th>
                        <th>Title</th>
                        <th>Price</th>
                        <th colspan="2">Action</th>
                    </tr>
                </thead>
                <tbody>
                    {% for book in books %}
                    <tr>
                        <td>{{ book.isbn }}</td>
                        <td>{{ book.author }}</td>
                        <td>{{ book.title }}</td>
                        <td>{{ book.price }}</td>
                        <td><button type="button" class="btn btn-success" data-toggle="modal"
                                data-target="#update_book_{{book['isbn']}}">Update</button></td>
                        <td><button type="button" class="btn btn-danger" data-toggle="modal"
                                data-target="#delete_book_{{book['isbn']}}">Delete</button></td>
                    </tr>
                    {% endfor %}
                </tbody>
            </table>
        </div>
    </div>
</div>
{% endblock %}

Note that the div HTML tag with the container class provides a responsive width to the application to be convenient with different screen sizes.

Let’s make sure we understand the details of the previous HTML snippet. The table HTML tag wraps two HTML tags; thead and tbody . The thead includes the header cells; each one ( th ) is inside one row ( tr ). As you can see from the picture above, there are 5 columns with the headers: ISBN, Author, Title, Price, and Action. Each header spans one column except the Action column that spans two columns.

The tbody wraps multiple rows depending on how many books you have in your database. As you can see, you can run Python code inside a Jinja2 template. The expression of a for loop is wrapped in {% %} while the variable definition is wrapped inside {{ }} (the ones inside td tags).

Finally, the last two td tags have two buttons; one to update a book and another to delete a book. Each button is customized by bootstrap to indicate success or danger CSS. Each has a data toggle of modal to indicate that they will open a pop up modal and we will have a decision accordingly. The data-target attribute is a unique string that links to the id in the modal tag. We then end up with the for loop block in the Jinja2 template with endfor in a Jinja2 expression.

If you run this Flask app, you’ll see that the headers of the table are hidden by the nav bar and not aligned well on the screen. You’ll find also that the text inside the cells of the table are not centered. To edit that, we need to add some CSS styles here. But how do we import a CSS file in a Flask app?

Let’s first make the CSS file and then learn how a Flask app would pick this CSS file. Let’s name main.css and put it in app/static directory.

Open the file and add the following:

body {
    padding-top: 50px;
}

td, th {
    text-align: center
}

These CSS styles do three things.

First, remember that when the Jinja2 template is rendered, the page is structured by HTML tags. So here we style the body tag with a padding on the top. This will offset it from appearing under the nav bar. Adjust it to the amount you want. I’ve made it 50px.

The second style is customizing two HTML tags inside the table to have the text centered.

Now, it’s time to link this CSS file to be rendered in the index page.

Add the following to the index.html file:

{% block head %}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
{% endblock %}

The head block, in this Jinja2 template, is appending to the head HTML tag with the super() method. This method represents the parent class and what’s following it is what we need to list after the sibling of the head tag.

The sibling is a link tag pointing to the main.css file inside the static directory. This is done by wrapping the directory and the filename as arguments to the url_for() method as an expression value to the href attribute.

Now, if you run the app again, you’ll find that all td and th elements (texts and buttons) inside the table are centered and the whole table is padded appropriately.

Let’s add the title and structure our app’s templates accordingly.

Structuring the Jinja2 templates and setting the title

What we need to do is the following:

To set a title, go ahead to the index.html file and add the following to the content Jinja block the first thing.

<div class="starter-template">
    <h1>
        Welcome to our bookshop!
    </h1>
</div>

Note, that the div tag has a .starter-template class because we want to style that element. If you run the app now, you’ll find that the h1 tag is inclined to the left. To make it centered and to add some padding from all four sides, add the following to the main.css file:

.starter-template {
    padding: 40px 15px;
    text-align: center;
}

Now, The title has a top and bottom padding of 40px, and right and left padding of 15px. It’s also centered.

In web apps, it’s better to separate our presentation layers to do just one thing . You might observe that the nav bar is a necessary component in almost all pages of the app we’re building. Also, the title might be so. In this app, we demonstrate this separation to be able to extend HTML templates from one template to another and to avoid duplicate code especially if you want to make this app a bit more complex, not a single page.

So let’s break some components inside the index.html file and put them in a new file: base.html .

The base.html can now contain the head and navbar blocks.

Let’s also move the following from index.html :

{% extends "bootstrap/base.html" %}

{% block title %}Bookshop{% endblock %}

Now, we need to extend the index template extend from base template with the following:

{% extends "base.html" %}

To continue getting rid of DRY (Don’t Repeat Yourself), we can customize the content block. So instead of wrapping our HTML elements with a div tag with a container class, we can make a specific block inside the base template and use that block in any template we extend base from.

So append the following to what exists in the base.html :

{% block content %}
<div class="container">
    {% block page_content %} {% endblock %}
</div>
{% endblock %}

Now, we define a block called page_content . Use this block instead of the content block in the index template. Simply, open the index.html file, replace content by page_content and remove the div tag with the container class.

Let’s now create the popup window that will show when we add a new book.

Creating a popup modal for adding a book

The modal that we will build looks like the following:

Before we create it, let’s add the “Add a book” button. Add the following to the index.html right after the title:

<button type="button" data-toggle="modal" class="btn btn-lg btn-primary" data-target="#insert_book">Add a book</button>

This modal references an id called “insert_book” which is the id of the modal that we’re going to create. Add the following HTML snippet after that button:

<!-- Modal 1 for adding a book -->
<div class="modal fade" id="insert_book" tabindex="-1" role="dialog" aria-labelledby="basicModal" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                <h4 class="modal-title" id="myModalLabel">Add a book</h4>
            </div>
            <form action="{{url_for('add_book')}}" method="post">
                <div class="modal-body">
                    <div class="form-group row">
                        <label for="author" class="col-xs-2 control-label">Author</label>
                        <div class="col-xs-10">
                            <input type="text" id="author" class="form-control" name="author" placeholder="Author" />
                        </div>
                    </div>
                    <div class="form-group row">
                        <label for="author" class="col-xs-2 control-label">Title</label>
                        <div class="col-xs-10">
                            <input type="text" class="form-control" name="title" placeholder="Title" />
                        </div>
                    </div>
                    <div class="form-group row">
                        <label for="author" class="col-xs-2 control-label">Price</label>
                        <div class="col-xs-10">
                            <input type="number" class="form-control" name="price" placeholder="Price" />
                        </div>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                    <button type="submit" class="btn btn-success">Submit</button>
                </div>
            </form>
        </div>
    </div>
</div>
<!-- End Modal 1 -->

This is imported from Bootstrap. The div tag here has an id of “insert book” as we mentioned. This modal essentially consists of div tags with the modal-header , modal-body , and modal-footer classes. So how can we interact with that through the database? In other words, how can we input data and store it inside our MySQL engine (or whatever)? Simple.

In our case, we use a form. This form wraps the body and the footer of the modal. It has an action attribute pointing to the endpoint that we want to invoke, with a method of a post request. In this modal, we point to the add_book endpoint in the routes.py file.

Inside the body of the modal, we have three rows. Each has a label and an input tag inside a div. The input has a placeholder; the text that appears with gray on the text box. The input has a name attribute which is the name of the column in our database (It’s the variable name in the ORM, in our case).

To configure the endpoint of add_book , let’s add the following to the routes:

@app.route('/add_book/', methods=['POST'])
def add_book():
    if not request.form:
        abort(400)
    book = Book(
        title=request.form.get('title'),
        author=request.form.get('author'),
        price=request.form.get('price')
    )
    db.session.add(book)
    db.session.commit()
    return redirect(url_for("index"))

Note that we get the request from the form and add it to the SQLAlchemy session and then commit it to the database. We finally render the index template to trigger the change.

Try to add a new author name and a title priced at any number and see the new change when you hit Submit.

Let’s add the second modal to update a book.

Creating popup modal for updating a book

We need to create a popup modal like this:

This modal is similar to the previous one. Let’s add the following to the index.html file, after the Update button:

<!-- Modal 2 for updating a book -->
<div class="modal fade" id="update_book_{{book['isbn']}}" tabindex="-1" role="dialog"
    aria-labelledby="basicModal" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal"
                    aria-hidden="true">&times;</button>
                <h4 class="modal-title" id="myModalLabel">Update a book</h4>
            </div>
            <form action="{{url_for('update_book', isbn=book['isbn'])}}" method="post">
                <div class="modal-body">
                    <div class="form-group row">
                        <label for="author" class="col-xs-2 control-label">Author</label>
                        <div class="col-xs-10">
                            <input type="text" id="author" class="form-control" name="author"
                                value="{{book['author']}}" />
                        </div>
                    </div>
                    <div class="form-group row">
                        <label for="author" class="col-xs-2 control-label">Title</label>
                        <div class="col-xs-10">
                            <input type="text" class="form-control" name="title"
                                value="{{book['title']}}" />
                        </div>
                    </div>
                    <div class="form-group row">
                        <label for="author" class="col-xs-2 control-label">Price</label>
                        <div class="col-xs-10">
                            <input type="number" class="form-control" name="price"
                                value="{{book['price']}}" />
                        </div>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-default"
                        data-dismiss="modal">Close</button>
                    <button type="submit" class="btn btn-success">Submit</button>
                </div>
            </form>
        </div>
    </div>
</div>
<!-- End Modal 2 -->

Things to note here:

To make it work, add the following route:

@app.route('/update_book/<int:isbn>', methods=['POST'])
def update_book(isbn):
    if not request.form:
        abort(400)
    book = Book.query.get(isbn)
    if book is None:
        abort(404)
    book.title = request.form.get('title', book.title)
    book.author = request.form.get('author', book.author)
    book.price = request.form.get('price', book.price)
    db.session.commit()
    return redirect(url_for("index"))

Let’s finalize this tutorial with the delete modal.

Creating a popup modal for deleting a book

We want to end up with the following:

Let’s add the following to the index template, after the Delete button:

<!-- Modal 3 for deleting a book -->
<div class="modal fade" id="delete_book_{{book['isbn']}}" tabindex="-1" role="dialog"
    aria-labelledby="basicModal" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal"
                    aria-hidden="true">&times;</button>
                <h4 class="modal-title" id="myModalLabel">Delete a book</h4>
            </div>
            <form action="{{url_for('delete', isbn=book['isbn'])}}" method="post">
                <div class="modal-body">
                    <div class="form-group row">
                        <label class="col-sm-12 col-form-label">Do you want to delete the book <span
                                style='font-weight:bold;color:red'>{{book['title']}}</span>
                            ?</label>

                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-default"
                        data-dismiss="modal">Close</button>
                    <button type="submit" class="btn btn-danger">Delete</button>
                </div>
            </form>
        </div>
    </div>
</div>
<!-- End Modal 3-->

This modal has the same main components; a header, a body, and a footer. The body in this case is wrapped in a form with an action of delete and ISBN value passed to the delete endpoint.

Configure it with the following route:

@app.route("/delete/<int:isbn>", methods=["POST"])
def delete(isbn):
    book = Book.query.get(isbn)
    if book is None:
        abort(404)
    db.session.delete(book)
    db.session.commit()
    return redirect(url_for("index"))

Now once you hit Delete, that record will be deleted from the database.

Conclusion

This tutorial covered the basics of how to create a frontend for your Flask app. We used Jinja2 templates to render HTML pages, and Bootstrap for CSS to style our elements. We did so through creating a real project, a bookshop.

We’ve seen how to use modals and reference them in our app, how to use forms, how to map the forms to the database, how to create a route that gets the request from that form, and how to iterate in our app and build each component in a step-by-step approach.

To write clean code in Python like the above and beyond, download my free ebook; Cleaner Python

You can get the complete code for this tutorial from this link .

Originally published on Python Code