Skip to content

2. Actions, Routing, Responses

This chapter examines the path a request takes, from the browser to the action that processes it, and the response that action returns. Views will not be introduced until Chapter 4: here, actions return text or JSON directly.

2.1. Example 01: The Structure of a Flask Application

The first example is the smallest possible [Flask] application: a single file.

01-structure/
└── app.py

2.1.1. The file app.py

# app.py: the smallest possible Flask application
from flask import Flask

# the application: an object of the Flask class. __name__ (here "__main__" or "app")
# tells Flask where the application is, so that it can look for its files there
app = Flask(__name__)


# an action (Flask says a "view"): the function called for the GET / request
@app.get("/")
def index():
    # the value returned by the function IS the response sent to the browser
    return "Hello from Flask!"


# executed only when the file itself is run: python app.py
if __name__ == "__main__":
    # the development server: http://localhost:5000; Ctrl-C stops it.
    # debug=True: automatic reload after a modification, and a debugging
    # page in case of error (NEVER in production)
    app.run(port=5000, debug=True)

Let’s break down this code:

  • line 2: imports the [Flask] class from the [flask] package, installed in the virtual environment by [pip]. In [Python], a file named [.py] is a module, and a module uses only the names it has imported or defined itself (aside from built-in language functions, such as [print] or [len]);
  • Line 6: the application, an object of the [Flask] class. It receives requests and selects the function that processes each one. The parameter is the name of the current module, which [Python] stores in the variable [__name__]: [Flask] uses this to locate the application folder, where it will later search for the views (folder [templates]) and the static files (folder [static]);
  • Line 10: [@app.get("/")] is a decorator. A decorator is written as [@expression], immediately before the definition of a function (or a class); [Python] calls the expression with the function as a parameter when it executes the definition. Here, [app.get("/")] registers the function [index] in the application: it will handle requests for [GET /]. This is routing. A function associated with a route in this way is an action (the [Flask] documentation refers to it as a view function; see the introduction);
  • lines 11–13: the action returns a string. The value returned by an action is the response sent to the browser: [Flask] places it in the body of the HTTP response. By convention, function names ([Python]) are written in lowercase, with words separated by underscores ([action01_post]);
  • line 17: when the file itself is executed ([python app.py]), [Python] assigns the value ["__main__"] to the variable [__name__]; when the module is imported by another module, it is set to the module’s name (["app"]). This test, which is very common in [Python], therefore reserves the rest of the code for when the file is launched directly;
  • line 21: [app.run] starts the [Werkzeug] development server, which listens for requests on port 5000 of the machine (5000 is the default value) until it is stopped by [Ctrl-C]. [debug=True] enables debug mode: the server monitors the application files and restarts the application as soon as one is modified; an unhandled exception displays a debug page (see Example 03). This mode is intended for development, and development only: the debug page allows you to execute [Python] code on the server.

2.1.2. Execution

In a terminal window open to the [01-structure] folder, with the examples’ virtual environment enabled, type [python app.py]. [Flask] displays its startup messages:

1
2
3
4
5
6
7
8
 * Serving Flask app 'app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 332-612-699

The messages show the application name, debug mode, listening address, and port. [127.0.0.1] is the address of the machine itself ([localhost]): the server accepts only requests from this machine, which is suitable for development ([app.run(host="0.0.0.0")] would open it up to other machines on the network). The line [Restarting with stat] indicates the automatic reload mechanism: the process launched by [python app.py] simply monitors the files; it launches the application in a separate process, which it restarts whenever a change is made. "PIN" protects the [Python] console from the debug page.

There is a second way to launch an application: the [flask] command, installed along with [Flask] in the virtual environment:

flask --app app run --debug

The option [--app] specifies the application module ([app], i.e., the file [app.py]), [run] starts the development server, and [--debug] enables debug mode; [--port 5001] would change the port. The command itself locates, within the module, the object [Flask] named [app] (or, as we’ll see in Example 02, a function named [create_app]). With this, the [if __name__ == "__main__"] test is no longer needed. The examples use [python app.py], which is easier to remember; the case study will do the same.

Let’s request URL and [http://localhost:5000/] using a browser:

Image

then with [curl]. The [-i] option also displays the HTTP headers from the response:

curl -i http://localhost:5000/
1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Server: Werkzeug/3.1.8 Python/3.13.7
Date: Sat, 26 Sep 2026 13:22:44 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 17
Connection: close

Hello from Flask!

The first line of the response contains the protocol version, the status code (200), and its label ([OK]). This is followed by the headers: the server name, the date, the content type, the content length in bytes, and [Connection: close] (the development server closes the connection after each response). A blank line separates the headers from the body of the response. In the rest of this document, we will no longer include the headers [Server], [Date], and [Connection], as they do not change.

The header [Content-Type: text/html; charset=utf-8] indicates that the response is in the format HTML, whose characters are encoded in UTF-8. This is the default encoding ([Flask]) for any string returned by an action: the browser therefore interprets the string as HTML (here, without a tag, it is displayed as-is). Example 03 will show how to specify a different encoding type.

For each request, the server console displays a log line: the client’s address, the date, the request, and the response code.

127.0.0.1 - - [26/Sep/2026 15:22:44] "GET / HTTP/1.1" 200 -

2.2. Example 02: Routing

Routing associates a URL and a method (HTTP, GET, POST...) with an action. Example 02 organizes these actions into two files:

1
2
3
4
5
6
02-routing/
├── controllers/
│   ├── __init__.py
│   ├── first.py
│   └── home.py
└── app.py

The [controllers] folder is a [Python] package (see introduction). Each of these two modules defines a blueprint: a group of routes, which the application then registers. This is the [Flask] equivalent of the controller classes in [Spring MVC] or [ASP.NET Core MVC].

2.2.1. The application: app.py

# app.py: the application, built by a "factory"
from flask import Flask

from controllers.home import home
from controllers.first import first


# the factory: a function that builds and configures the application
def create_app():
    app = Flask(__name__)
    # each blueprint (a group of routes) is registered with the application
    app.register_blueprint(home)
    app.register_blueprint(first)
    return app


if __name__ == "__main__":
    create_app().run(port=5000, debug=True)
  • Lines 4–5: Import the two blueprints, the objects [home] and [first], from the modules [controllers/home.py] and [controllers/first.py];
  • Lines 9–14: The application is no longer a module variable created upon import; it is constructed by a function called a *factory*. This is the approach recommended by the [Flask] documentation, which all subsequent examples will follow: the application’s configuration is consolidated in a single location, and multiple applications with different configurations can be created (for testing, for example). The [flask --app app run] command recognizes this: in the absence of a [app] variable, it calls the [create_app] function from the module;
  • lines 12–13: Each blueprint is registered in the application; its routes become those of the application;
  • line 18: the application is built, then the server is started.

2.2.2. The "home" blueprint: controllers/home.py

# controllers/home.py: the home page
from flask import Blueprint

# a blueprint: a set of routes, then registered in the application.
# "home": its name; __name__: the module where it is defined
home = Blueprint("home", __name__)


# GET /: an HTML page that contains links to the other actions.
# A string returned by an action is sent with the text/html type
@home.get("/")
def index():
    return """
        <h2>Example 02 - routing</h2>
        <ul>
          <li><a href="/first">/first</a></li>
          <li><a href="/first/action01">/first/action01</a></li>
          <li><a href="/first/products/17">/first/products/17</a></li>
          <li><a href="/first/products/17/reviews/3">/first/products/17/reviews/3</a></li>
          <li><a href="/first/files/images/logo.png">/first/files/images/logo.png</a></li>
          <li><a href="/first/products/abc">/first/products/abc</a></li>
          <li><a href="/unknown">/unknown</a></li>
        </ul>
        """
  • line 6: the blueprint. Its first parameter is its name, [home]: this will be used to identify its actions ([home.index], see Example 04). The second parameter, as with the application, is the module name;
  • line 11: A blueprint has the same decorators as the application: the action [index] processes [GET /];
  • lines 13–23: A string enclosed in triple quotes can span multiple lines and contain quotes. The action returns HTML, sent with the type [text/html] (see Example 01).

The action [index] thus returns a page HTML that contains links to the actions in the other blueprint:

Image

2.2.3. The first blueprint: controllers/first.py

# controllers/first.py: a blueprint whose routes all start with /first
from flask import Blueprint

# url_prefix: prefix shared by all the routes of the blueprint
first = Blueprint("first", __name__, url_prefix="/first")


# GET /first: the "" route designates the prefix itself
@first.get("")
def index():
    return "Action [index] of the [first] blueprint"


# GET /first/action01
@first.get("/action01")
def action01():
    return "Action [action01] of the [first] blueprint, GET request"


# POST /first/action01: same path, another HTTP method, another function
@first.post("/action01")
def action01_post():
    return "Action [action01_post] of the [first] blueprint, POST request"


# GET /first/products/17: <int:id> is a path variable;
# the [int] converter accepts digits only, and provides an integer
@first.get("/products/<int:id>")
def product(id):
    return f"Product no. {id}"


# GET /first/products/17/reviews/3: two path variables
@first.get("/products/<int:id>/reviews/<int:number>")
def review(id, number):
    return f"Review no. {number} of product no. {id}"


# GET /first/files/...: the [path] converter accepts any end of
# path, slashes included
@first.get("/files/<path:path>")
def file(path):
    return f"Requested file: {path}"

Let’s break down this code:

  • line 5: the parameter [url_prefix] sets the prefix common to all routes in the blueprint: its actions handle URL routes that begin with [/first];
  • Lines 9–10: The route [""] refers to the prefix itself: the action [index] handles [GET /first]. The URL of an action is the concatenation of the blueprint’s prefix and the path of its route. The action name ([index]) may be the same as in another blueprint: [Flask] distinguishes them by the blueprint name ([first.index], [home.index]);
  • lines 15–16: the action [action01] processes [GET /first/action01];
  • lines 21–22: the same path, but for the method POST: the decorator [post] replaces [get]. There are also [put], [delete], and [patch], as well as the general decorator [route], which accepts a list of methods: [@first.route("/action01", methods=["GET", "POST"])]. Within the same module, two functions cannot have the same name (the second would replace the first): hence the name [action01_post];
  • Lines 28–30: [<int:id>] is a path variable: this segment of URL can take on multiple values. [int] is a converter: the segment must consist of digits; otherwise, the route does not match; its value is converted to an integer and then passed to the action’s parameter of the same name, [id]. For [/first/products/17], [id] is equal to the number 17. This is data binding (*data binding*), which the next chapter will examine in detail. Without a converter ([<id>]), the variable accepts any text without a slash and remains a string. [Werkzeug] also provides the converters [float], [uuid], and [path], and Example 12 will show how to write your own;
  • Line 30: an f-string (the prefix [f]): each expression within curly braces is evaluated, and its value is inserted into the text;
  • lines 34–36: two path variables, passed as two parameters to the action;
  • lines 41–43: the [path] converter accepts any ending of URL, including [/].

2.2.4. Execution

Here are the responses to a few queries:

curl -i http://localhost:5000/first
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 39

Action [index] of the [first] blueprint
curl -i http://localhost:5000/first/products/17/reviews/3
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 30

Review no. 3 of product no. 17

The text is 30 characters long, and so is the response in bytes: in UTF-8, each of these (ASCII) characters takes up one byte.

curl -i http://localhost:5000/first/files/images/logo.png
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 31

Requested file: images/logo.png

To send a request for POST, use the [-X POST] option of [curl]:

curl -i -X POST http://localhost:5000/first/action01
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 61

Action [action01_post] of the [first] blueprint, POST request

A HTTP method that is not handled by any action for this path results in a 405 error ([Method Not Allowed]). The [Allow] header lists the accepted methods; [Flask] adds [HEAD] (the same request as GET, without the response body) and [OPTIONS] (the list of accepted methods), which it processes itself for any route GET:

curl -i -X PUT http://localhost:5000/first/action01
HTTP/1.1 405 METHOD NOT ALLOWED
Content-Type: text/html; charset=utf-8
Allow: HEAD, OPTIONS, POST, GET
Content-Length: 153

<!doctype html>
<html lang=en>
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
<p>The method is not allowed for the requested URL.</p>

Finally, a URL that no route handles results in a 404 error. This is also the case for an identifier that is not an integer: no route matches [/first/products/abc].

curl -i http://localhost:5000/first/products/abc
1
2
3
4
5
6
7
8
9
HTTP/1.1 404 NOT FOUND
Content-Type: text/html; charset=utf-8
Content-Length: 207

<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>

The error responses for [Flask] (actually, for [Werkzeug]) are short pages named HTML, in English. In a browser:

Image

Chapter 7 will show how to replace them with application-specific pages. Note also that [Flask] distinguishes [/first] from [/first/]: the latter, URL, also produces a 404 error, because no route ends with a forward slash. Conversely, a route declared with a trailing slash ([@first.get("/list/")]) also accepts URL without a slash, via a redirect.

The command [flask routes], run in the example folder, lists all the routes in the application:

flask --app app routes
Endpoint             Methods  Rule
-------------------  -------  ---------------------------------------------
first.action01       GET      /first/action01
first.action01_post  POST     /first/action01
first.file           GET      /first/files/<path:path>
first.index          GET      /first
first.product        GET      /first/products/<int:id>
first.review         GET      /first/products/<int:id>/reviews/<int:number>
home.index           GET      /
static               GET      /static/<path:filename>

The [Endpoint] column lists the name of each route: the blueprint name, a period, and the function name. The last route, [static], is created by [Flask] itself: it serves the files in the application’s [static] folder (see Example 15).

2.3. Example 03: Responses from an action

What an action returns determines the response HTTP. Example 03 reviews the main cases. The file [app.py]:

# app.py: the application of example 03
from flask import Flask

from controllers.responses import responses


def create_app():
    app = Flask(__name__)
    # the JSON produced keeps accented characters as they are ("é" and not
    # "\u00e9"), and the keys in their original order (not sorted)
    app.json.ensure_ascii = False
    app.json.sort_keys = False
    app.register_blueprint(responses)
    return app


if __name__ == "__main__":
    create_app().run(port=5000, debug=True)
  • Lines 11–12: two settings for the application’s JSON provider ([app.json]), the object that converts [Python] objects to JSON. By default, it writes non-ASCII characters as escape sequences ([é] becomes [\u00e9], which is valid JSON but not very readable), and it sorts dictionary keys alphabetically. We keep the characters as-is, and the keys in the order in which the action wrote them.

The file [controllers/responses.py]:

# controllers/responses.py: the different responses of an action
import asyncio
import time
from dataclasses import dataclass

from flask import Blueprint, Response, abort, jsonify

responses = Blueprint("responses", __name__, url_prefix="/responses")


# a string, announced as plain text: the browser displays it as is.
# Response: the complete response, built by the action itself
@responses.get("/text")
def text():
    return Response("<h1>Action [text]</h1>", mimetype="text/plain")


# a string alone: Flask sends it with the text/html type; the browser interprets it
@responses.get("/html")
def html():
    return "<h1>Action [html]</h1>"


# a dataclass: a data class (see example 06), declared here in the module
@dataclass
class Person:
    last_name: str
    first_name: str
    age: int


# an object: jsonify serializes it to JSON, with the [application/json] type
@responses.get("/json")
def json():
    return jsonify(Person("Martin", "Jean", 42))


# a dictionary returned as is: Flask serializes it to JSON itself
@responses.get("/dictionary")
def dictionary():
    return {"last_name": "Martin", "first_name": "Jean", "age": 42}


# a list: serialized to a JSON array
@responses.get("/list")
def list():
    return ["Mme", "Melle", "Mr"]


# a tuple (content, code): the 201 code (Created) instead of 200 (OK)
@responses.get("/created")
def created():
    return {"message": "resource created"}, 201


# no response: the 204 code (No Content) means "nothing to display"
@responses.get("/empty")
def empty():
    return "", 204


# an asynchronous action (async def): Flask waits for the end of the coroutine.
# It needs Flask's [async] extra: pip install "flask[async]"
@responses.get("/task")
async def task():
    start = time.perf_counter()
    await asyncio.sleep(0.5)  # a 500 ms wait, without blocking the event loop
    return {"wait": f"{round((time.perf_counter() - start) * 1000)} ms"}


# an "expected" error: the tuple (content, 404) produces a 404 response
@responses.get("/not-found")
def not_found():
    return {"message": "This resource does not exist"}, 404


# the same, with abort: an HTTP exception, which Flask turns into a 404 response
@responses.get("/abort")
def abort_action():
    abort(404, "This resource does not exist either")


# an "unexpected" error: an unhandled exception produces a 500 response
@responses.get("/exception")
def exception():
    raise RuntimeError("An unexpected error")

Let’s comment on this code:

  • Lines 13–15: The class [Response] represents a complete HTTP response. The action constructs it itself: the body and the type ([mimetype]), in this case [text/plain] (plain text). The browser will not interpret the tags;
  • lines 19–21: the same string, rendered on its own: [Flask] sends it with the type [text/html], and the browser interprets it;
  • lines 25–29: a dataclass—a class in which only the fields and their types are declared; the decorator [@dataclass] writes the constructor ([Person("Martin", "Jean", 42)]), the textual representation, and the object comparison itself. Example 06 will revisit this;
  • lines 33–35: the function [jsonify] converts its parameter to JSON and generates a response of type [application/json]. The provider JSON for [Flask] can convert dictionaries, lists, strings, numbers, Booleans, [None] (which becomes [null]), as well as dataclasses, dates, and numbers [Decimal] (see example 06);
  • lines 39–41: even simpler: an action that returns a dictionary (or a list, lines 45–47) produces a response JSON, without calling [jsonify]. A dataclass, on the other hand, must go through [jsonify]: if returned on its own, it would cause an error (“The view function did not return a valid response”);
  • lines 51–53: An action can return a tuple [(content, code)]: status code 201 ([Created]) replaces status code 200. A tuple can also contain headers: [(content, code, {"X-Example": "03"})];
  • lines 57–59: code 204 ([No Content]) means “nothing to display”: the response has no body;
  • lines 64–68: an asynchronous action, declared by [async def]. The keyword [await] suspends the coroutine while waiting ([asyncio.sleep]), without blocking the program. [Flask] executes the action in an event loop created for the request and waits for its result: this is useful for launching multiple waits in parallel (multiple calls to web services, for example), but it does not make [Flask] itself asynchronous. You must install the [[async]] extension of [Flask] (see [requirements.txt]);
  • lines 72–74: an “expected” error: the tuple [(content, 404)] returns a 404 response ([Not Found]), with a body (JSON) chosen by the action;
  • lines 78–80: the same error, generated by the function [abort]. [abort] returns nothing: it throws an exception ([NotFound], for the 404 code), which terminates the action; [Flask] converts it into an error response, with the message provided as the second parameter. This is the most common way to terminate an action due to an error, even deep within a function called by the action;
  • lines 84–86: an “unexpected” error—an exception that no one handles—produces a 500 response ([Internal Server Error]).

In a browser, the difference between [text/plain] and [text/html] is visible:

Image

Image

Here are the responses for the other actions:

curl -i http://localhost:5000/responses/json
1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 65

{
  "last_name": "Martin",
  "first_name": "Jean",
  "age": 42
}

The dataclass has been converted to the object JSON, with one field per attribute. In debug mode, JSON is displayed on multiple lines, indented; without debug mode, it fits on a single line ([{"last_name":"Martin","first_name":"Jean","age":42}]). The action [/responses/dictionary] produces exactly the same output.

curl -i http://localhost:5000/responses/list
1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 31

[
  "Mme",
  "Melle",
  "Mr"
]
curl -i http://localhost:5000/responses/created
1
2
3
4
5
6
7
HTTP/1.1 201 CREATED
Content-Type: application/json
Content-Length: 36

{
  "message": "resource created"
}
curl -i http://localhost:5000/responses/empty
HTTP/1.1 204 NO CONTENT
Content-Type: text/html; charset=utf-8
curl -i http://localhost:5000/responses/task
1
2
3
4
5
6
7
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 23

{
  "wait": "501 ms"
}
curl -i http://localhost:5000/responses/not-found
1
2
3
4
5
6
7
HTTP/1.1 404 NOT FOUND
Content-Type: application/json
Content-Length: 48

{
  "message": "This resource does not exist"
}
curl -i http://localhost:5000/responses/abort
1
2
3
4
5
6
7
8
9
HTTP/1.1 404 NOT FOUND
Content-Type: text/html; charset=utf-8
Content-Length: 122

<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>This resource does not exist either</p>

The response from [abort] is the standard error page for [Werkzeug], with our message (inserted into the page after escaping: text inserted into HTML must not be able to introduce tags there; we’ll come back to this).

That leaves the unhandled exception. In debug mode, the browser displays the [Werkzeug] debugger page: the exception type and message, followed by the call stack (the list of functions that were executing at the time of the error), from oldest to most recent, with the relevant line of code for each:

Image

The last function in the stack is our action, [exception], on line 86 of the file [responses.py]. By clicking on one of the lines in the stack, you can even open a [Python] console in the context of that function, after entering the PIN displayed at startup: this is why debug mode should never be enabled on a server accessible to others. The server console also displays the call stack (we’ve included only the end here):

1
2
3
4
5
6
127.0.0.1 - - [26/Sep/2026 15:23:10] "GET /responses/exception HTTP/1.1" 500 -
Traceback (most recent call last):
  ...
  File ".../examples/03-responses/controllers/responses.py", line 86, in exception
    raise RuntimeError("An unexpected error")
RuntimeError: An unexpected error

Without debug mode ([app.run(port=5000)]), the same request produces a brief 500 error page in English that reveals nothing about the code (“Internal Server Error. The server encountered an internal error and was unable to complete your request...”); the call stack is only written to the server console. Chapter 7 will show how to display an application-specific error page.

2.4. Example 04: Redirects

A redirect is a response that instructs the browser to fetch another URL: a 3xx code and the [Location] header, which contains the new address. The browser immediately sends a second request to that address. The [controllers/redirects.py] file:

# controllers/redirects.py: the redirects
from flask import Blueprint, redirect, url_for

redirects = Blueprint("redirects", __name__)


# permanent redirect (301): the page has permanently changed address
@redirects.get("/old")
def old_page():
    return redirect("/new", code=301)


@redirects.get("/new")
def new_page():
    return "You are on the [new] page"


# temporary redirect (302, the default code), to an address computed
# by gluing pieces of text together
@redirects.get("/go/<destination>")
def go(destination):
    return redirect("/page/" + destination)


# url_for builds the address of an ACTION, designated by its name
# ("blueprint.function"): the [name] parameter is correctly encoded for a URL
@redirects.get("/to-template/<destination>")
def to_template(destination):
    return redirect(url_for("redirects.page", name=destination))


@redirects.get("/page/<name>")
def page(name):
    return f"You are on the [{name}] page"


# redirect to another site
@redirects.get("/documentation")
def documentation():
    return redirect("https://flask.palletsprojects.com/")
  • lines 8–10: The [redirect] function generates a redirect response to the specified address. The 301 status code ([Moved Permanently]) indicates that the page has permanently changed its address: the browser (and search engines) can cache the new one;
  • lines 20–22: Without the [code] parameter, [redirect] produces a temporary redirect with a 302 status code ([Found]). The address is calculated by concatenating pieces of text, which poses a problem that we will examine;
  • Lines 27–29: The function [url_for] generates the URL for an action, identified by its name (["redirects.page"]: the blueprint, a point, the function), rather than by its path. Its named parameters provide the path variables (here, [name]), correctly encoded for a URL. If the path of the route [page] is ever changed, calls to [url_for] will automatically follow suit;
  • lines 38–40: a redirect to another site.

The responses, using [curl] (which, without any options, does not follow redirects):

curl -i http://localhost:5000/old
HTTP/1.1 301 MOVED PERMANENTLY
Content-Type: text/html; charset=utf-8
Content-Length: 195
Location: /new

<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/new">/new</a>. If not, click the link.

The response also includes a small body, HTML, for a client that would not follow redirects. The [-L] option of [curl] follows the redirect, just as a browser would:

curl -L http://localhost:5000/old
You are on the [new] page

Now let’s compare the two calculated redirects, with a destination that contains a question mark, encoded as [%3F] in URL (an uncoded [?] would start the parameters of the URL):

curl -i http://localhost:5000/go/a%3Fb
1
2
3
4
HTTP/1.1 302 FOUND
Content-Type: text/html; charset=utf-8
Content-Length: 205
Location: /page/a?b
curl -i http://localhost:5000/to-template/a%3Fb
1
2
3
4
HTTP/1.1 302 FOUND
Content-Type: text/html; charset=utf-8
Content-Length: 209
Location: /page/a%3Fb

[Flask] decoded the path variable before processing it: [destination] equals [a?b]. The first action pastes it as-is into the address: [/page/a?b], which the browser interprets as the path [/page/a] followed by the parameter [b]. The second one goes through [url_for], which encodes it again: [/page/a%3Fb]. Following the redirects:

curl -L http://localhost:5000/go/a%3Fb
curl -L http://localhost:5000/to-template/a%3Fb
You are on the [a] page
You are on the [a?b] page

Only the second one leads to the correct page. Hence the rule: an address containing a variable value is generated using [url_for], never by concatenating strings. In the browser, only the final page is visible; the address bar displays the new URL:

Image

Two other redirect codes exist: 303 ([See Other]) and 307 ([Temporary Redirect]), which are obtained via [redirect(url, code=303)]. After a POST request, codes 302 and 303 cause the browser to send a GET request to the new address, whereas 307 (and 308, the permanent version) cause it to repeat the POST request. Example 20 will use redirection following a POST.

2.5. Example 05: The Request and Response Objects

Until now, actions only received path variables and returned a value that [Flask] converted into a response. An action can also access the current request and construct the response itself. The file [controllers/http.py]:

# controllers/http.py: the request and response objects
import time

from flask import Blueprint, Response, make_response, request

http = Blueprint("http", __name__, url_prefix="/http")


# the request: method, path, parameters, headers...
# [request] is a global Flask object, which designates the CURRENT request
@http.get("/request")
def request_info():
    # a Python dictionary keeps the insertion order of its keys
    return {
        "method": request.method,
        "path": request.path,
        # query_string: bytes, which we decode into text
        "parameters": request.query_string.decode(),
        "browser": request.headers.get("User-Agent"),
        "ip": request.remote_addr,
    }


# the response: make_response builds the response object from what the
# action would return; we add a header to it before returning it
@http.get("/header")
def header():
    response = make_response("Response with an added header")
    response.headers["X-Example"] = "05-request-response"
    return response


# the response built "by hand": body, code, type
@http.get("/response")
def response():
    return Response("Response built by [Response(...)]", status=200,
                    mimetype="text/plain")


# a response sent IN CHUNKS: the code and the headers leave with the
# first chunk, and can no longer change afterwards
@http.get("/stream")
def stream():
    def chunks():
        yield "Start of the response... "
        time.sleep(1)
        # too late to announce an error: the 200 code has already left
        raise RuntimeError("error in the middle of the response")

    return Response(chunks(), mimetype="text/plain")
  • Line 4: Importing the [request] object. In [Flask], the current request is not a parameter of the action: it is a global object, [request], which is imported from the [flask] package. How can a global object refer to the current request, given that the server processes multiple requests simultaneously across multiple threads (parallel execution threads)? [request] acts as a proxy: each time it is used, it forwards the operation to the request in the current thread. [Flask] refers to this as the request context: while a request is being processed, [request] refers to that specific request. Outside of request processing, using it causes an error (“Working outside of request context”);
  • lines 14–20: some properties of the request: its method HTTP, its path, its raw parameters (the part of URL that follows [?], in byte form, which [decode] converts to text), its headers ([request.headers], a dictionary, whose method [get] returns [None] for a missing header), the client’s address IP. The parameters, one by one, will be covered in the next chapter;
  • lines 26–30: the function [make_response] constructs the response object (a [Response]) based on what the action would return (here, a string); the action can then modify it before returning it. A [X-Example] header is added to it (by convention, application-specific headers often begin with [X-]);
  • lines 34–37: the “hand-built” response: the body, the status code, and the type;
  • lines 42–50: a response sent in chunks (*streaming*). The function [chunks] is a generator: each [yield] produces a chunk of the body. Since the response is built using a generator, its length is not known in advance: the server sends the status code and headers, then each chunk as soon as it is produced. This is useful for a very long response, generated bit by bit. But once the first chunk has been sent, the status code can no longer change: here, the generator throws an exception after the first chunk. This is the mistake to avoid: a response that has already begun cannot become an error response.

Responses:

curl -i "http://localhost:5000/http/request?name=martin&age=42"
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 135

{
  "method": "GET",
  "path": "/http/request",
  "parameters": "name=martin&age=42",
  "browser": "curl/8.5.0",
  "ip": "127.0.0.1"
}

(The quotation marks around "URL" prevent the terminal from interpreting the character "[&]".) In a browser, the [User-Agent] header is the browser's header; [Chrome] displays the raw JSON, with a checkbox that indents it:

Image

curl -i http://localhost:5000/http/header
1
2
3
4
5
6
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 29
X-Example: 05-request-response

Response with an added header
curl -i http://localhost:5000/http/response
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 33

Response built by [Response(...)]
curl -i http://localhost:5000/http/stream
1
2
3
4
5
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked

Start of the response...

The header [Transfer-Encoding: chunked] replaces [Content-Length]: the body arrives in chunks, each preceded by its length. After one second, the response stops: [curl] received a 200 status code and a truncated body, with no way of knowing that an error occurred. Only the server console indicates this:

1
2
3
4
5
6
Error on request:
Traceback (most recent call last):
  ...
  File ".../examples/05-request-response/controllers/http.py", line 48, in chunks
    raise RuntimeError("error in the middle of the response")
RuntimeError: error in the middle of the response

2.6. Example 06: Shared Services and Dependency Injection

An action should not contain the application logic itself; it delegates it to services—objects that know how to perform a specific task (read products, calculate a shopping cart, etc.). Multiple actions often use the same service. In [Spring] or [ASP.NET Core], a dependency injection container creates the services and automatically provides them to those that need them. [Flask] does not have a container: the application factory creates the services, and the actions request them from the application. Example 06 organizes its code by domain (products, customers) rather than by type (controllers, models):

06-services/
├── clients/
│   ├── __init__.py
│   └── controller.py
├── products/
│   ├── __init__.py
│   ├── controller.py
│   ├── in_memory_service.py
│   ├── product.py
│   └── service.py
└── app.py

2.6.1. The Product

# products/product.py: a product
from dataclasses import dataclass
from decimal import Decimal


# a dataclass: Python writes the constructor, __repr__, __eq__... itself
# frozen=True: the object is immutable (its fields are set by the constructor)
@dataclass(frozen=True)
class Product:
    id: int
    name: str
    price: Decimal  # Decimal: an exact decimal number (no binary rounding)
  • Line 8: a dataclass (see Example 03). With [frozen=True], the object is immutable: its fields are set by the constructor, and any attempt to modify them ([p.price = 2]) throws an exception. An immutable object can be safely shared across multiple requests;
  • Lines 10–12: the fields, along with their types. Types are not checked by [Python] at runtime: they are annotations read by analysis tools (such as the one in [Visual Studio Code]) and certain libraries ([Pydantic], discussed in the next chapter). The price is a [Decimal], an exact decimal number: a [float] cannot accurately represent 0.1 or 3.2 (in binary, these numbers have an infinite number of digits), and a sum of prices in [float] may result in 5.500000000000001.

2.6.2. The service contract: service.py

# products/service.py: the CONTRACT of the service, what it can do, without saying how
from abc import ABC, abstractmethod

from flask import current_app

from products.product import Product


# an abstract class (ABC): no object can be created from it; a child
# class must write (override) all the methods marked @abstractmethod
class ProductsService(ABC):

    @abstractmethod
    def find_all(self) -> list[Product]: ...

    # Product | None: a product, or None if it does not exist
    @abstractmethod
    def find_by_id(self, id: int) -> Product | None: ...


# the service of the current application (see app.py). current_app designates
# the application that handles the current request
def products_service() -> ProductsService:
    return current_app.extensions["products_service"]
  • Lines 11–18: the service contract—what it can do, without specifying how. In [Python], a contract is defined by an abstract class: a class that inherits from [ABC] ([Abstract Base Class]), and whose methods marked [@abstractmethod] have no body (the three dots [...] serve as an empty body). You cannot create an object of this class; a subclass must override all of its abstract methods. This is equivalent to the interfaces [Java] and [C#];
  • line 14: the annotation for the type returned by the method (after [->]): a list of products. [self] is the object on which the method is called (the equivalent of [this] in [Java]), always listed as the first parameter;
  • line 18: [Product | None]: a product, or [None] if it does not exist;
  • Lines 23–24: The function that provides the service to the actions. [current_app] is, like [request], a proxy: it refers to the application that processes the current request. [app.extensions] is a dictionary that [Flask] makes available to extensions (the libraries that are added to [Flask]) so they can store their objects there; we use it for our own services. The function also specifies the service type ([-> ProductsService]): [Visual Studio Code] can then offer its methods.

2.6.3. The implementation: in_memory_service.py

# products/in_memory_service.py: an IMPLEMENTATION of the contract, with in-memory data
from decimal import Decimal

from products.product import Product
from products.service import ProductsService


class InMemoryProductsService(ProductsService):

    def __init__(self):
        # a tuple: a list that cannot be modified
        self._products = (
            Product(1, "pen", Decimal("1.5")),
            Product(2, "notebook", Decimal("3.2")),
            Product(3, "eraser", Decimal("0.8")),
        )

    def find_all(self) -> list[Product]:
        return list(self._products)

    def find_by_id(self, id: int) -> Product | None:
        # next(generator, None): the first matching product, or None
        return next((p for p in self._products if p.id == id), None)
  • line 8: the class inherits the contract and redefines its two methods;
  • lines 10–15: [__init__] is the class’s constructor. It stores the products in the object’s [_products] attribute; the leading underscore indicates, by convention, a private attribute that code outside the class should not use. The products are in a tuple (enclosed in parentheses): an immutable sequence. The price is constructed from a string ([Decimal("1.5")]): built from [float] 1.5, it would inherit its inaccuracies;
  • lines 18–19: The method returns a new list, a copy of the tuple: the caller can modify it without affecting the service’s data;
  • lines 21–23: [(p for p in self._products if p.id == id)] is a generator expression: it produces, one by one, the products whose ID matches the requested one. [next] returns the first product element, or the default value, [None], if there are none.

2.6.4. The product blueprint: controller.py

# products/controller.py: the actions of /products
from flask import Blueprint, jsonify

from products.service import products_service

products = Blueprint("products", __name__, url_prefix="/products")


@products.get("")
def list_all():
    # the service is obtained from the application, at each request
    return jsonify(products_service().find_all())


@products.get("/<int:id>")
def one_product(id):
    product = products_service().find_by_id(id)
    if product is None:
        return {"message": f"Product no. {id} not found"}, 404
    return jsonify(product)
  • Line 4: The blueprint knows only the service contract (the [service] module), not its implementation: it is unaware that the products are in memory;
  • Lines 10–12: The action requests the service from the application, then delegates the work to it. [jsonify] converts the list of dataclasses into an array JSON;
  • lines 16–20: an unknown product returns a 404 response, with a message in JSON.

2.6.5. The client blueprint: controller.py

# clients/controller.py: a second blueprint, which uses the SAME service
from dataclasses import dataclass
from decimal import Decimal

from flask import Blueprint, jsonify

from products.service import products_service

clients = Blueprint("clients", __name__, url_prefix="/clients")


# the content of the cart, returned as JSON
@dataclass
class Cart:
    client: str
    items: int
    total: Decimal


@clients.get("/cart")
def cart():
    products = products_service().find_all()
    # the sum of the prices; start=Decimal(0): we add Decimal values
    total = sum((p.price for p in products), start=Decimal(0))
    return jsonify(Cart("Jean Martin", len(products), total))
  • lines 14–17: the contents of the shopping cart, rendered as JSON;
  • line 22: this blueprint uses the same service as the products blueprint: a single, shared object;
  • line 24: [sum] sums the elements of an iterable; its parameter [start] is the initial value of the sum (0 by default). Starting with [Decimal(0)] rather than the integer 0 is not essential (an integer and a [Decimal] add together), but it guarantees a result of [Decimal] even for an empty shopping cart.

2.6.6. The service declaration: app.py

# app.py: the application of example 06
from flask import Flask

from clients.controller import clients
from products.controller import products
from products.in_memory_service import InMemoryProductsService


def create_app():
    app = Flask(__name__)
    # JSON: accents kept, keys in their original order (see example 03)
    app.json.ensure_ascii = False
    app.json.sort_keys = False
    # the SERVICE: a single object, created once, at startup, and shared by
    # all the requests. We store it in app.extensions, a dictionary that
    # Flask makes available to extensions... and to our services
    app.extensions["products_service"] = InMemoryProductsService()
    app.register_blueprint(products)
    app.register_blueprint(clients)
    return app


if __name__ == "__main__":
    create_app().run(port=5000, debug=True)
  • Line 17: This is where—and only here—the service implementation is chosen. The service is created once, at startup, and the same object handles all requests from all users: it is a singleton (the default scope for [Spring] beans). To switch to an implementation that reads from a database, you would simply need to change this line: the blueprints, which only know the contract, would remain unchanged. This is the whole point of dependency injection, even when implemented “by hand”: every part of the application depends on contracts, and a single location determines the implementations.

A service shared by all requests must be able to handle being used simultaneously by multiple requests: the development server processes each request on its own thread. This poses no problem: it simply reads data that never changes. Example 20 will show a service whose data changes, and the precautions this requires.

2.6.7. Execution

curl -i http://localhost:5000/products
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 191

[
  {
    "id": 1,
    "name": "pen",
    "price": "1.5"
  },
  {
    "id": 2,
    "name": "notebook",
    "price": "3.2"
  },
  {
    "id": 3,
    "name": "eraser",
    "price": "0.8"
  }
]

The prices have become strings (["1.5"]): JSON has only a numeric type, which most programs interpret as a floating-point number; so the provider JSON for [Flask] converts a [Decimal] to a string to preserve its precision.

curl -i http://localhost:5000/products/9
1
2
3
4
5
6
7
HTTP/1.1 404 NOT FOUND
Content-Type: application/json
Content-Length: 43

{
  "message": "Product no. 9 not found"
}
curl -i http://localhost:5000/clients/cart
1
2
3
4
5
6
7
8
9
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 62

{
  "client": "Jean Martin",
  "items": 3,
  "total": "5.5"
}