JavaScript, fetch, and JSON

You may want to make your HTML page dynamic, by changing data without reloading the entire page. Instead of submitting an HTML <form> and performing a redirect to re-render the template, you can add JavaScript that calls fetch() and replaces content on the page.

fetch() is the modern, built-in JavaScript solution to making requests from a page. You may have heard of other “AJAX” methods and libraries, such as XMLHttpRequest() or jQuery. These are no longer needed in modern browsers, although you may choose to use them or another library depending on your application’s requirements. These docs will only focus on built-in JavaScript features.

Rendering Templates

It is important to understand the difference between templates and JavaScript. Templates are rendered on the server, before the response is sent to the user’s browser. JavaScript runs in the user’s browser, after the template is rendered and sent. Therefore, it is impossible to use JavaScript to affect how the Jinja template is rendered, but is is possible to render data into the JavaScript that will run.

To provide data to JavaScript when rendering the template, use the tojson() filter in a <script> block. This will convert the data to a valid JavaScript object, and ensure that any unsafe HTML characters are rendered safely. If you do not use the tojson filter, you will get a SyntaxError in the browser console.

data = generate_report()
return render_template("report.html", chart_data=data)
<script>
    const chart_data = {{ chart_data|tojson }}
    chartLib.makeChart(chart_data)
</script>

A less common pattern is to add the data to a data- attribute on an HTML tag. In this case, you must use single quotes around the value, not double quotes, otherwise you will produce invalid or unsafe HTML.

<div data-chart='{{ chart_data|tojson }}'></div>

Generating URLs

The other way to get data from the server to JavaScript is to make a request for it. First, you need to know the URL to request.

The simplest way to generate URLs is to continue to use url_for() when rendering the template. For example:

const user_url = {{ url_for("user", id=current_user.id)|tojson }}
fetch(user_url).then(...)

However, you might need to generate a URL based on information you only know in JavaScript. As discussed above, JavaScript runs in the user’s browser, not as part of the template rendering, so you can’t use url_for at that point.

In this case, you need to know the “root URL” under which your application is served. In simple setups, this is /, but it might also be something else, like https://example.com/myapp/.

A simple way to tell your JavaScript code about this root is to set it as a global variable when rendering the template. Then you can use it when generating URLs from JavaScript.

const SCRIPT_ROOT = {{ request.script_root|tojson }}
let user_id = ...  // do something to get a user id from the page
let user_url = `${SCRIPT_ROOT}/user/${user_id}`
fetch(user_url).then(...)

Making a Request with fetch

fetch() takes two arguments, a URL and an object with other options, and returns a Promise. We won’t cover all the available options, and will only use then() on the promise, not other callbacks or await syntax. Read the linked MDN docs for more information about those features.

By default, the GET method is used. If the response contains JSON, it can be used with a then() callback chain.

const room_url = {{ url_for("room_detail", id=room.id)|tojson }}
fetch(room_url)
    .then(response => response.json())
    .then(data => {
        // data is a parsed JSON object
    })

To send data, use a data method such as POST, and pass the body option. The most common types for data are form data or JSON data.

To send form data, pass a populated FormData object. This uses the same format as an HTML form, and would be accessed with request.form in a Flask view.

let data = new FormData()
data.append("name": "Flask Room")
data.append("description": "Talk about Flask here.")
fetch(room_url, {
    "method": "POST",
    "body": data,
}).then(...)

In general, prefer sending request data as form data, as would be used when submitting an HTML form. JSON can represent more complex data, but unless you need that it’s better to stick with the simpler format. When sending JSON data, the Content-Type: application/json header must be sent as well, otherwise Flask will return a 400 error.

let data = {
    "name": "Flask Room",
    "description": "Talk about Flask here.",
}
fetch(room_url, {
    "method": "POST",
    "headers": {"Content-Type": "application/json"},
    "body": JSON.stringify(data),
}).then(...)

Following Redirects

A response might be a redirect, for example if you logged in with JavaScript instead of a traditional HTML form, and your view returned a redirect instead of JSON. JavaScript requests do follow redirects, but they don’t change the page. If you want to make the page change you can inspect the response and apply the redirect manually.

fetch("/login", {"body": ...}).then(
    response => {
        if (response.redirected) {
            window.location = response.url
        } else {
            showLoginError()
        }
    }
)

Replacing Content

A response might be new HTML, either a new section of the page to add or replace, or an entirely new page. In general, if you’re returning the entire page, it would be better to handle that with a redirect as shown in the previous section. The following example shows how to replace a <div> with the HTML returned by a request.

<div id="geology-fact">
    {{ include "geology_fact.html" }}
</div>
<script>
    const geology_url = {{ url_for("geology_fact")|tojson }}
    const geology_div = getElementById("geology-fact")
    fetch(geology_url)
        .then(response => response.text)
        .then(text => geology_div.innerHtml = text)
</script>

Return JSON from Views

To return a JSON object from your API view, you can directly return a dict from the view. It will be serialized to JSON automatically.

@app.route("/user/<int:id>")
def user_detail(id):
    user = User.query.get_or_404(id)
    return {
        "username": User.username,
        "email": User.email,
        "picture": url_for("static", filename=f"users/{id}/profile.png"),
    }

If you want to return another JSON type, use the jsonify() function, which creates a response object with the given data serialized to JSON.

from flask import jsonify

@app.route("/users")
def user_list():
    users = User.query.order_by(User.name).all()
    return jsonify([u.to_json() for u in users])

It is usually not a good idea to return file data in a JSON response. JSON cannot represent binary data directly, so it must be base64 encoded, which can be slow, takes more bandwidth to send, and is not as easy to cache. Instead, serve files using one view, and generate a URL to the desired file to include in the JSON. Then the client can make a separate request to get the linked resource after getting the JSON.

Receiving JSON in Views

Use the json property of the request object to decode the request’s body as JSON. If the body is not valid JSON, or the Content-Type header is not set to application/json, a 400 Bad Request error will be raised.

from flask import request

@app.post("/user/<int:id>")
def user_update(id):
    user = User.query.get_or_404(id)
    user.update_from_json(request.json)
    db.session.commit()
    return user.to_json()