2025, Oct 16 09:00

Partial Page Updates in Flask: Use fetch() and JSON to write to SQLite and refresh the DOM

Learn how to add classes in SQLite and refresh part of a Flask page using fetch(), JSON, and Flask-Login. Async requests update the DOM without full reloads.

When a Flask view renders a template that depends on database state, the simplest way to reflect changes after a write is a full page reload. That works, but it’s jarring and inefficient when you only need to update one fragment, such as a list of classes. The goal is to persist new data in sqlite3 and refresh only part of the page without a complete reload.

Problem setup

Here is the database structure involved in the use case:

CREATE TABLE classes (
    model_id TEXT NOT NULL,
    class_number INTEGER,
    class_name TEXT,
    FOREIGN KEY(model_id) REFERENCES models(model_id)
);

The page that shows a single model is served from a route like this. The template receives multiple variables, including a list of classes and images, assembled inside the view:

@app.route("/model/<model_id>", methods=["POST", "GET"])
@login_required
def show_model(model_id):
    # ... application logic omitted ...
    with sqlite3.connect("database.db") as conn:
        # ... queries that fill the variables below ...
        return render_template(
            "train.html",
            categories=ALL_GROUPS,
            model_id=model_id,
            model=mdl,
            classes=class_rows,
            images=pics,
            istrained=trained_flag,
            test_image=probe_img,
        )

To create a new class, a separate route writes to the database and then redirects back to the model page. The redirect forces a full reload to pick up the new data.

@app.route("/add_class/<model_id>", methods=["POST", "GET"])
@login_required
def create_class(model_id):
    with sqlite3.connect("database.db") as conn:
        conn.row_factory = sqlite3.Row
        cur = conn.cursor()
        row = cur.execute(
            "SELECT MAX(class_number) FROM classes WHERE model_id = ?", (model_id,)
        ).fetchone()
        max_no = row[0] if row and row[0] is not None else 0
        next_no = int(max_no + 1)
        next_name = "Class " + str(max_no + 1)
        cur.execute(
            "INSERT INTO classes (model_id, class_name, class_number) VALUES (?, ?, ?)",
            (model_id, next_name, next_no),
        )
        conn.commit()
    return redirect(f"/model/{model_id}")

Returning a redirect reloads the entire page, which works but isn’t the desired UX. Returning an empty response or a different status code does not help, because the page’s data preparation happens in the first route.

What really causes the friction

Flask renders server-side templates and ships a complete HTML response. It doesn’t have a built-in mechanism to push partial updates to the browser by itself. Without a client-side request from the browser, the server can’t update only a fragment of the page. That’s why a redirect re-runs the template and reloads everything, while a non-redirect response doesn’t magically refresh the section with classes.

Flask doesn’t have special functions to send data from the browser to the server without reloading the page.

To change content without a full reload, the browser has to call back to the server asynchronously and then update the DOM in place. That requires JavaScript. A practical approach is to use the standard fetch() API to send a request to Flask and receive data (for example, JSON) that can be applied to the page. Flask, in turn, can inspect request.method and request.is_json, and respond with jsonify(...) so the browser can parse and use the result. If the view is protected with @login_required, the fetch request must include credentials to carry cookies.

Solution: async call with fetch() and a JSON response

The example below demonstrates how to send a request from the page to Flask without reloading, receive a JSON status, and display it inline. It also shows how to make this work with @login_required by adding credentials: 'include' to fetch().

from flask import Flask, request, jsonify, render_template_string, redirect
from flask_login import LoginManager, UserMixin, login_required, login_user, logout_user
class Account(UserMixin):
    def __init__(self, uid, mail, secret):
        self.id = uid
        self.email = mail
        self.password = secret
# key matches Account.id
account_store = {
    '007': Account('007', 'james_bond@mi6.gov.uk', 'license_to_kill')
}
srv = Flask(__name__)
srv.secret_key = "super secret string"  # replace in real app
auth = LoginManager()
auth.init_app(srv)
@auth.user_loader
def load_account(user_id):
    print('load_account:', user_id, account_store.get(user_id))
    return account_store.get(user_id)
@srv.route('/login')
def do_login():
    user = account_store.get('007')
    print('login:', user)
    login_user(user)
    print('redirect:', '/')
    return redirect('/')
@srv.route('/logout')
@login_required
def do_logout():
    logout_user()
    return redirect('/')
@srv.route('/')
def home():
    return render_template_string("""
<script type="text/javascript">
function pushData(modelId, httpMethod) {
    fetch("/add_model/" + modelId, {
        method: httpMethod,  // "POST" or "GET"
        // body: JSON.stringify({"username": "example"}), // if you need to send JSON in body
        headers: {
            // "Content-Type": "application/x-www-form-urlencoded", // for <form>
            "Content-Type": "application/json", // enables request.is_json in Flask
            // "X-Requested-With": "XMLHttpRequest" // common AJAX header
        },
        credentials: 'include', // cookies for @login_required
    }).then((resp) => {
        if (!resp.ok) {
            throw new Error(`HTTP error! Status: ${resp.status}`);
        }
        return resp.json(); // expecting JSON
        // return resp.text(); // if Flask returns text/HTML
    }).then((payload) => {
        document.getElementById("statusBox").innerHTML = `model #${modelId}: ${payload['status']}`;
    }).catch((err) => {
        console.log('Fetch problem: ' + err.message);
        document.getElementById("statusBox").innerHTML = 'Fetch problem: ' + err.message;
    });
}
</script>
{% if current_user.is_authenticated %}
<p>Logged in as: {{ current_user.id }} <a href="{{ url_for('do_logout') }}">Logout</a></p>
{% else %}
<p>You are not logged in. <a href="{{ url_for('do_login') }}">Login</a></p>
{% endif %}
{% for mid in range(1, 6) %}
<button onclick="pushData({{ mid }}, 'POST')">Add model #{{ mid }} (POST)</button>
<button onclick="pushData({{ mid }}, 'GET')">Add model #{{ mid }} (GET)</button>
<br/>
{% endfor %}
<div id="statusBox"></div>
""")
@srv.route('/add_model/<model_id>', methods=['GET', 'POST'])
@login_required
def add_model(model_id):
    print(f'add_model: {model_id} | method: {request.method} | is_json: {request.is_json}')
    if request.method == 'POST' and request.is_json:
        return jsonify({'status': 'OK'})
    else:
        return jsonify({'status': f'Wrong Method {request.method}'})
if __name__ == "__main__":
    srv.run(host='0.0.0.0')

This pattern is the core of updating content without a full reload. The browser uses fetch() to contact the Flask route. Flask returns JSON via jsonify(...). The page reads that JSON and updates only the relevant part of the DOM. If access requires authentication, credentials are included so the server can verify the session for @login_required.

Why this matters

When the page depends on data assembled in a single route, a redirect guarantees freshness but at the cost of re-rendering everything. Asynchronous calls keep the server-side logic intact while enabling a smoother UI. Notably, request.method and request.is_json let the server distinguish between request types, and jsonify ensures the response is correctly formatted for the browser to consume.

Practical wrap‑up

To change content without reloading the entire page, have the browser initiate an asynchronous request. Use fetch() to call a Flask route, return JSON (or HTML if that’s what you want to inject), and update the page in place. If routes are protected by @login_required, include credentials in fetch(). That’s the straightforward, reliable approach when you need partial updates in a Flask app that depends on sqlite3-backed state.

The article is based on a question from StackOverflow by Burak and an answer by furas.