Class-based Views

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Flask 3.1.3 (released 2026-02-18) and Werkzeug 3.1.9, checked on 2026-10-07. Flask 3.2 was not released on that date, so everything described as "3.2" is upcoming. The code uses Python 3.10+ syntax and was executed on Python 3.13 with app.test_client(). A class-based view (the official documentation also calls it a pluggable view) is a class whose instances answer requests, as opposed to the plain function that @app.route decorates. Flask ships exactly two base classes for it, flask.views.View and flask.views.MethodView, both in a single small module. This page explains how they turn a class into the view function Werkzeug’s router expects, every attribute that controls that conversion, and how to use them for the Bookshelf catalogue’s CRUD API. Routing itself (add_url_rule, converters, url_for) is covered by the routing page of this section, and the full REST design discussion by the building REST APIs page.

Why Class-based Views

A Flask "view" is any callable that receives the URL variables as keyword arguments and returns something Flask can turn into a response. Routing never sees a class: the URL map (a Werkzeug Map of Rule objects) stores an endpoint name and app.view_functions maps that name to a callable. A class-based view is therefore only a way of building that callable. as_view() creates it, attaches the class to it as view_class, and copies the class’s methods and provide_automatic_options attributes onto it so that add_url_rule can read them like it would read them from a function.

Three situations make a class worth the extra ceremony:

  • One URL, several methods, one place. A resource such as /api/books/<int:book_id> answers GET, PUT, PATCH and DELETE. With a MethodView each method is a method of the class and the framework dispatches by name, instead of an if request.method == ... ladder inside one function.

  • Reuse and variation. A generic "list something" view can be subclassed or instantiated with different arguments for books, authors and shelves, which is what the official documentation shows with its ListView.

  • Shared behaviour. The decorators attribute applies a list of decorators (authentication, caching, rate limiting) to every view built from the class, in one declaration.

The libraries underneath are small. Werkzeug’s Rule and MapAdapter do the matching and decide 405 Method Not Allowed and the automatic OPTIONS and HEAD answers from the methods set. Flask’s flask.views module (about a hundred lines) builds the callable. The ordinary request machinery, including current_app.ensure_sync for async support, is Flask’s own.

flowchart TD A["Request arrives, Werkzeug matches a Rule"] --> B["Endpoint name selects app.view_functions entry"] B --> C["View function built by as_view runs"] C --> D{"init_every_request?"} D -->|"True (default)"| E["New instance created for this request"] D -->|"False"| F["Shared instance created once at registration"] E --> G["ensure_sync wraps dispatch_request"] F --> G G --> H{"Base class"} H -->|"View"| I["Your dispatch_request runs"] H -->|"MethodView"| J["Method named like the HTTP method runs (HEAD falls back to get)"] I --> K["Return value becomes the response"] J --> K

The decorators listed in decorators wrap the whole function drawn above, so they run before the instance is even created.

View and dispatch_request

Subclass View, override dispatch_request, and register the callable as_view() returns. Any variables from the URL rule arrive as keyword arguments, exactly as they do for a function view. The name passed to as_view becomes both the function’s __name__ and the endpoint, so it is also what url_for takes.

from flask import Flask, url_for
from flask.views import View

app = Flask(__name__)
BOOKS = {1: "Dune", 2: "Neuromancer"}


class BookTitle(View):
    def dispatch_request(self, book_id: int):
        return BOOKS.get(book_id, "unknown")


app.add_url_rule("/books/<int:book_id>/title", view_func=BookTitle.as_view("book_title"))

client = app.test_client()
assert client.get("/books/1/title").text == "Dune"
with app.test_request_context():
    assert url_for("book_title", book_id=2) == "/books/2/title"
print(client.get("/books/9/title").text)

The default methods of a plain View follow the same rule as route and add_url_rule: GET, plus the implicit HEAD and OPTIONS. To accept other methods, set the class attribute methods to a collection of method names. Because dispatch_request of a plain View is one function for all of them, it branches on request.method itself, which is the case where a MethodView is the better choice.

from flask import request

app = Flask(__name__)
client = app.test_client()


class BookNote(View):
    methods = ["GET", "POST"]

    def dispatch_request(self, book_id: int):
        if request.method == "POST":
            return f"saved note for {book_id}", 201
        return f"note for {book_id}"


app.add_url_rule("/books/<int:book_id>/note", view_func=BookNote.as_view("book_note"))

assert client.post("/books/1/note").status_code == 201
assert client.put("/books/1/note").status_code == 405
print(client.get("/books/1/note").text)

as_view, Constructor Arguments and Endpoint Names

as_view(name, *class_args, **class_kwargs) forwards everything after name to the class’s __init__. That makes the class a small factory: the same view with different collaborators, with no globals. For the Bookshelf, one list view serves several resources, each registered with its own endpoint name, template and data source.

from flask import render_template_string

PAGE = "{% for item in items %}{{ item }};{% endfor %}"
app = Flask(__name__)
client = app.test_client()


class ListView(View):
    def __init__(self, repository, template: str = PAGE):
        self.repository = repository
        self.template = template

    def dispatch_request(self):
        return render_template_string(self.template, items=self.repository())


app.add_url_rule("/books", view_func=ListView.as_view("book_list", lambda: sorted(BOOKS.values())))
app.add_url_rule("/authors", view_func=ListView.as_view("author_list", lambda: ["Herbert", "Gibson"]))

assert client.get("/books").text == "Dune;Neuromancer;"
print(client.get("/authors").text)

Rules can only be added before the application handles its first request (Flask raises an AssertionError otherwise), which is why each example below builds its own Flask object. The endpoint name must be unique per application. Registering two different view functions under the same endpoint raises an AssertionError ("View function mapping is overwriting an existing endpoint function") when the second rule is added, which is the usual symptom of copying an as_view("index") line and forgetting to rename it. The generated function also remembers its class, which tests and tools can use to find out what is behind an endpoint.

app = Flask(__name__)
app.add_url_rule("/books", view_func=ListView.as_view("book_list", lambda: []))
app.add_url_rule("/note", view_func=BookNote.as_view("book_note"))

try:
    app.add_url_rule("/clash", view_func=BookNote.as_view("book_list"))
except AssertionError as exc:
    print("rejected:", exc)

assert app.view_functions["book_list"].view_class is ListView
assert app.view_functions["book_note"].methods == ["GET", "POST"]

Registering a class-based view always goes through add_url_rule (or Blueprint.add_url_rule) with view_func=. The @app.route decorator is for functions, and stacking it on a class does not work because the class itself is not the view function. An application with many class-based views therefore usually keeps the registrations together in one function, which also gives one readable place to see the URL space.

View Lifetime and self

By default as_view builds a function that creates a new instance of the class for every request, so self is request-local state and you may freely store things on it in __init__ or in methods. That is convenient and cheap for most views. When construction is expensive (parsing a large file, building a client), set the class attribute init_every_request = False (added in Flask 2.2). Then one instance is created when as_view runs, at registration time, and every request is served by that same object, possibly from several threads at once.

app = Flask(__name__)
client = app.test_client()


class Visits(View):
    init_every_request = False

    def __init__(self):
        self.count = 0              # shared by every request, not thread-safe

    def dispatch_request(self):
        self.count += 1
        return str(self.count)


class Fresh(View):
    def __init__(self):
        self.count = 0              # new for every request

    def dispatch_request(self):
        self.count += 1
        return str(self.count)


app.add_url_rule("/visits", view_func=Visits.as_view("visits"))
app.add_url_rule("/fresh", view_func=Fresh.as_view("fresh"))

assert [client.get("/visits").text for _ in range(3)] == ["1", "2", "3"]
assert [client.get("/fresh").text for _ in range(3)] == ["1", "1", "1"]

With init_every_request = False nothing request-specific may be stored on self: another request can overwrite it between two lines of your method, which produces data that leaks from one user to the next. Use flask.g for request-global data, as the official documentation recommends, and keep self for read-only configuration set in the constructor. The shared counter above is exactly the thing to avoid in real code, and it is shown only because it makes the lifetime visible. The g object belongs to the current context, not to the view instance, so each concurrent request sees its own copy.

Decorating Class-based Views

Decorating the class does not decorate the views built from it, because as_view creates a new function that merely calls into the class. Put the decorators in the decorators attribute instead. Flask applies them, in list order, to the generated function. The list is applied like successive wrappers (the first entry is the innermost), so it reads in the opposite order to stacked @ lines: the first decorator in the list is the bottom decorator, and the last one is the first to run.

import functools
import secrets

from flask import abort, request

app = Flask(__name__)
client = app.test_client()
API_TOKEN = secrets.token_hex(16)   # in a real app: injected through configuration or the environment
trace: list[str] = []


def require_token(view):
    @functools.wraps(view)
    def wrapper(*args, **kwargs):
        sent = request.headers.get("Authorization", "").removeprefix("Bearer ")
        if not secrets.compare_digest(sent, API_TOKEN):   # constant-time comparison
            abort(401)
        trace.append("auth")
        return view(*args, **kwargs)
    return wrapper


def log_calls(view):
    @functools.wraps(view)
    def wrapper(*args, **kwargs):
        trace.append("log")
        return view(*args, **kwargs)
    return wrapper


class PrivateShelf(View):
    decorators = [require_token, log_calls]     # log_calls ends up outermost

    def dispatch_request(self):
        return "private shelf"


app.add_url_rule("/private", view_func=PrivateShelf.as_view("private"))

assert client.get("/private").status_code == 401
assert trace == ["log"]        # the outer decorator ran, the inner one aborted
trace.clear()
ok = client.get("/private", headers={"Authorization": f"Bearer {API_TOKEN}"})
assert ok.text == "private shelf"
assert trace == ["log", "auth"]

Two details save debugging time. First, a decorator in the list receives the function that as_view builds, so it should use functools.wraps and accept *args, **kwargs (the URL variables arrive as keyword arguments). Second, a decorator listed there guards every method of a MethodView identically. If only some methods need protection, for example a public GET and an authenticated POST, either split the class in two, decorate the individual methods in the class body, or check inside the method. Method-level decorators work because they decorate ordinary functions, but note that they run after the instance exists and after self is bound, so they receive self as the first argument.

Authentication that must never be skipped belongs in decorators (or a before_request hook, covered by the hooks page of this section), not in a convention that each method must call a helper. A method added later cannot forget a decorator that is applied by the class.

MethodView

MethodView extends View with a ready-made dispatch_request. It lower-cases request.method, looks the name up on the instance, and calls it with the URL variables as keyword arguments. If the request is HEAD and the class defines no head method, it falls back to get.

The mechanics are in __init_subclass__. When a MethodView subclass is created without an explicit methods, Flask builds the set from every method name the class defines (get, post, put, patch, delete, head, options and trace), upper-cases it, and unions it with the methods of the base classes. That set becomes the rule’s allowed methods, so Werkzeug answers every other method with 405 Method Not Allowed and an Allow header before your code runs.

from flask.views import MethodView

app = Flask(__name__)
client = app.test_client()


class Shelf(MethodView):
    def get(self):
        return "shelf contents"

    def delete(self):
        return "", 204


assert sorted(Shelf.methods) == ["DELETE", "GET"]

app.add_url_rule("/shelf", view_func=Shelf.as_view("shelf"))

assert client.get("/shelf").text == "shelf contents"
head = client.head("/shelf")
assert head.status_code == 200 and head.data == b""
assert client.delete("/shelf").status_code == 204

denied = client.post("/shelf")
assert denied.status_code == 405
print(sorted(m.strip() for m in denied.headers["Allow"].split(",")))
print(sorted(m.strip() for m in client.options("/shelf").headers["Allow"].split(",")))

The HEAD and OPTIONS rows come for free. Werkzeug adds HEAD to any rule that allows GET, and with provide_automatic_options unset Flask answers OPTIONS itself from the allowed set, which is what a browser’s CORS preflight needs the application to handle (the CORS page of this site covers when to configure it). If your class must answer OPTIONS itself, define an options method and set the class attribute provide_automatic_options = False. The attribute is the class-level spelling of the provide_automatic_options argument of add_url_rule, and None (the default) means "use the same default as route`". The unreleased 3.2 changelog also mentions passing `provide_automatic_options=True per view.

Controlling the Allowed Methods

Because methods is computed, the usual way to restrict a MethodView is simply not to define the method. Setting methods explicitly in the class body skips the computation, and then the set you wrote is the whole truth. That is useful in two cases: when a base class defines helper methods that happen to be named like HTTP methods, and when a subclass needs to remove an inherited verb, which the union rule otherwise makes impossible.

app = Flask(__name__)
client = app.test_client()


class ReadOnlyShelf(Shelf):
    methods = ["GET"]           # explicit: the inherited delete is no longer routed


assert sorted(Shelf.methods) == ["DELETE", "GET"]
assert ReadOnlyShelf.methods == ["GET"]

app.add_url_rule("/readonly", view_func=ReadOnlyShelf.as_view("readonly"))
assert client.delete("/readonly").status_code == 405
assert client.get("/readonly").status_code == 200

The reverse also bites. A subclass of a MethodView that defines one extra method inherits every verb of its parent, so a "read-only" parent with a mutating subclass exposes GET and POST on the subclass’s URL, as intended, but a mixin that defines delete makes every class that includes it deletable. Review the final methods set of each registered view in a test, as shown later in this page.

A CRUD API for the Bookshelf

The standard layout for a REST resource is two views: one for the collection (/api/books: list and create) and one for the item (/api/books/<int:book_id>: read, replace, update and delete). This is the same idea as the Cookbook’s chapters 4 and 7, rewritten for current Flask, with the repository kept behind a small class so the views stay thin. The data lives in memory, which is enough to run the code and is replaced by Flask-SQLAlchemy on the Flask-SQLAlchemy page of this section.

from flask import Flask, abort, jsonify, request, url_for
from flask.views import MethodView


class BookRepository:
    """In-memory stand-in for the database used in later pages."""

    def __init__(self) -> None:
        self._books: dict[int, dict] = {}
        self._next_id = 1

    def all(self) -> list[dict]:
        return list(self._books.values())

    def get(self, book_id: int) -> dict | None:
        return self._books.get(book_id)

    def add(self, title: str, author: str) -> dict:
        book = {"id": self._next_id, "title": title, "author": author}
        self._books[self._next_id] = book
        self._next_id += 1
        return book

    def replace(self, book_id: int, title: str, author: str) -> dict:
        book = {"id": book_id, "title": title, "author": author}
        self._books[book_id] = book
        return book

    def delete(self, book_id: int) -> None:
        self._books.pop(book_id, None)


def payload() -> dict:
    """Validate the JSON body shared by POST and PUT."""
    data = request.get_json()           # 415 for a non JSON Content-Type, 400 for bad JSON
    if not isinstance(data, dict):
        abort(400, description="JSON object expected")
    title, author = data.get("title"), data.get("author")
    if not isinstance(title, str) or not title.strip() or not isinstance(author, str) or not author.strip():
        abort(400, description="title and author are required strings")
    return {"title": title.strip(), "author": author.strip()}


class BookListAPI(MethodView):
    init_every_request = False           # the repository is injected and read-only here

    def __init__(self, books: BookRepository) -> None:
        self.books = books

    def get(self):
        page = request.args.get("page", 1, type=int)
        per_page = min(request.args.get("per_page", 10, type=int), 100)
        items = self.books.all()
        start = (max(page, 1) - 1) * per_page
        return jsonify(items=items[start:start + per_page], page=page, per_page=per_page, total=len(items))

    def post(self):
        book = self.books.add(**payload())
        return jsonify(book), 201, {"Location": url_for("book_item", book_id=book["id"])}


class BookItemAPI(MethodView):
    init_every_request = False

    def __init__(self, books: BookRepository) -> None:
        self.books = books

    def _find(self, book_id: int) -> dict:
        book = self.books.get(book_id)
        if book is None:
            abort(404, description=f"book {book_id} not found")
        return book

    def get(self, book_id: int):
        return jsonify(self._find(book_id))

    def put(self, book_id: int):
        self._find(book_id)
        return jsonify(self.books.replace(book_id, **payload()))

    def patch(self, book_id: int):
        book = self._find(book_id)
        data = request.get_json()
        if not isinstance(data, dict):
            abort(400, description="JSON object expected")
        for key in ("title", "author"):
            if key in data:
                if not isinstance(data[key], str) or not data[key].strip():
                    abort(400, description=f"{key} must be a non-empty string")
                book[key] = data[key].strip()
        return jsonify(book)

    def delete(self, book_id: int):
        self._find(book_id)
        self.books.delete(book_id)
        return "", 204


def register_api(app: Flask, books: BookRepository) -> None:
    app.add_url_rule("/api/books", view_func=BookListAPI.as_view("book_list", books))
    app.add_url_rule("/api/books/<int:book_id>", view_func=BookItemAPI.as_view("book_item", books))


api = Flask("bookshelf")
api.config["TESTING"] = True
register_api(api, BookRepository())
http = api.test_client()

created = http.post("/api/books", json={"title": "Dune", "author": "Frank Herbert"})
assert created.status_code == 201
assert created.headers["Location"] == "/api/books/1"
assert created.get_json()["id"] == 1

assert http.get("/api/books/1").get_json()["title"] == "Dune"
assert http.get("/api/books").get_json()["total"] == 1
assert http.put("/api/books/1", json={"title": "Dune", "author": "F. Herbert"}).get_json()["author"] == "F. Herbert"
assert http.patch("/api/books/1", json={"title": "Dune Messiah"}).get_json()["title"] == "Dune Messiah"
assert http.post("/api/books", json={"title": ""}).status_code == 400
assert http.post("/api/books", data="not json", content_type="text/plain").status_code == 415
assert http.get("/api/books/99").status_code == 404
assert http.delete("/api/books/1").status_code == 204
assert http.get("/api/books/1").status_code == 404
assert http.post("/api/books/1").status_code == 405
print(http.get("/api/books?page=2&per_page=5").get_json())

Notes on the design, since each choice answers a question the books leave open:

  • Collection and item are two classes. They share a URL prefix but differ in which methods and URL variables they take, and MethodView computes methods per class. A single class for both would have to accept an optional book_id and branch on it. The official documentation’s register_api helper registers both, plus the defaults trick, from one function; the helper above is the same idea without it.

  • Dependencies arrive through as_view. The repository is created once and passed to both views. In an application factory the factory calls register_api(app, books), and tests can pass a fake.

  • init_every_request = False is safe here because the instance holds only a reference to a repository and no per-request data. The payload() helper and abort() use the request-bound proxies, which is correct.

  • Status codes and headers follow HTTP. 201 with a Location header for creation, 204 for a successful delete, 404 for a missing item, 400 for a malformed body, 415 from get_json() when the content type is not JSON, and 405 from the router for a method the class does not define.

  • Return dictionaries or jsonify, never json.dumps strings. A view returning json.dumps(...) sends text/html.

  • Parse query values with type=. request.args.get("page", 1, type=int) returns the default on invalid input instead of raising, and the page size is capped so a client cannot ask for everything.

A real application would validate the body with a schema library (the REST page compares marshmallow and Pydantic), return errors as Problem Details, and read from a database. The skeleton of the two classes stays the same.

URL Variables and Optional Parameters

URL variables are passed to the method by keyword, so a variable the method does not name is a TypeError, and a variable with a default in the rule should have a default in the signature. The official documentation shows registering one view under two rules, the collection rule with defaults={"book_id": None}, which lets a single class answer both. It works but mixes two resources in one class, and every method must cope with None. Two classes are usually clearer.

app = Flask(__name__)
client = app.test_client()


class BookByIsbn(MethodView):
    def get(self, isbn: str, fmt: str = "json"):
        return jsonify(isbn=isbn, format=fmt)


app.add_url_rule("/isbn/<isbn>", defaults={"fmt": "json"}, view_func=BookByIsbn.as_view("isbn"))
app.add_url_rule("/isbn/<isbn>.<fmt>", view_func=BookByIsbn.as_view("isbn_fmt"))

assert client.get("/isbn/9780441172719").get_json() == {"format": "json", "isbn": "9780441172719"}
assert client.get("/isbn/9780441172719.xml").get_json()["format"] == "xml"

The same class is registered twice here, each time under a different endpoint name. That is allowed because as_view is called twice and returns two distinct functions.

Async Methods

Both dispatch_request implementations call the method through current_app.ensure_sync, so a view method may be an async def when Flask is installed with its async extra (pip install "flask[async]", which brings asgiref). Flask then runs the coroutine to completion on an event loop while the request is handled, which keeps WSGI semantics: the request still occupies one worker, so an async view is not faster than a synchronous one under WSGI. It is useful for awaiting several I/O calls concurrently inside one request. The async and ASGI pages of this section discuss the trade-offs, and Quart is the framework to consider when most of your views are asynchronous.

import asyncio

app = Flask(__name__)
client = app.test_client()


class SlowShelf(MethodView):
    async def get(self):
        await asyncio.sleep(0.01)
        return "awake"


app.add_url_rule("/slow", view_func=SlowShelf.as_view("slow"))
assert client.get("/slow").text == "awake"

An async def get mixed with a synchronous post in one class is fine, because ensure_sync inspects each callable separately. A decorator in decorators is a different matter. It wraps the generated function, which is synchronous, so a decorator must be a synchronous one unless it also handles the coroutine case. Flask decorators from extensions are written with that in mind, but your own wrapper functions should not await the view result.

Testing Class-based Views

The test client is enough. Two extra checks are specific to classes and cost almost nothing: verifying the final methods of each registered view, to catch an accidental extra verb inherited from a mixin, and calling the view_class attribute of the endpoint when a test must know what stands behind a URL.

def routes_by_endpoint(app: Flask) -> dict[str, set[str]]:
    exposed = {}
    for rule in app.url_map.iter_rules():
        if rule.endpoint == "static":
            continue
        view = app.view_functions[rule.endpoint]
        exposed[rule.endpoint] = set(rule.methods or ()) - {"HEAD", "OPTIONS"}
        assert getattr(view, "view_class", None) is not None or callable(view)
    return exposed


exposed = routes_by_endpoint(api)
assert exposed["book_list"] == {"GET", "POST"}
assert exposed["book_item"] == {"GET", "PUT", "PATCH", "DELETE"}
print(exposed)

When to Prefer Function Views

Class-based views are a tool, not an upgrade. Flask’s own tutorial and most of its documentation use functions, and a function with a decorator is easier to read at a glance. Prefer a plain function when:

  • the view has one method and no reuse, which is most pages and most simple JSON endpoints;

  • behaviour varies only by a few arguments and a closure or functools.partial expresses it as clearly;

  • the team is new to Flask, because as_view and the lifetime rules are one more thing to learn;

  • you want @app.get and @app.post shortcuts (added in Flask 2.0), which give per-method functions and per-method decorators with the same URL and no class at all.

Prefer a MethodView when one URL carries several methods that share a lookup (the item resource above), when several URLs share the same skeleton with different collaborators (the ListView), or when you want one decorator list to cover a whole resource. Past about a handful of resources, moving to a schema-driven extension (flask-smorest, APIFlask) is usually a better step than growing hand-written MethodView`s. Those extensions build on `MethodView, so everything above still applies to their resource classes.

Where the Books Are Outdated

The Cookbook’s chapter 4 (Working with Views) uses MethodView for a small API and its chapter 7 (RESTful API Building) builds the full CRUD API, first with MethodView and then with Flask-RESTful. The 2nd edition (Flask 1.0.2) is the source of most of the idioms you will meet in older code. The MethodView part is still correct. The surrounding code is not, and each item is covered where it belongs:

  • request.is_xhr. The 2nd edition’s chapter 4 uses it to decide between a JSON and an HTML answer. The property was removed in Werkzeug 1.0. Detect an explicit header at most, and better serve an HTML view and a JSON API view on separate routes, which class-based views make cheap (two classes, two URLs).

  • Positional paginate. Both editions call Model.query.paginate(page, 10). Since Flask-SQLAlchemy 3.0 the arguments are keyword-only, so it raises TypeError, and from flask_sqlalchemy import Pagination was removed in 3.1. Use db.paginate(db.select(Book).order_by(Book.title), page=page, per_page=10), as the Flask-SQLAlchemy page of this section shows. The get of the list view above paginates a plain list for the same shape of response.

  • The legacy Model.query API in chapter 7’s views (Model.query.get(id), .filter_by(...).first()) is legacy in SQLAlchemy 2.0. Use db.session.get(Book, book_id) or db.get_or_404(Book, book_id) inside the methods.

  • Flask-RESTful with reqparse. reqparse is deprecated upstream and the extension is barely maintained. Present MethodView plus jsonify first, as this page does, then flask-smorest, APIFlask or Flask-RESTX, compared in the REST API page of this section. Treat Flask-RESTful as code you will meet.

  • json.dumps(...) in resources. The cookbook’s Flask-RESTful resources return json.dumps(...) strings, which are double encoded when the library serializes the return value again and are sent with the wrong content type. Return a dict (Flask serializes it with the JSON provider) or jsonify(...).

  • self state in views. The books assume a new instance per request, which is still the default. The init_every_request attribute (Flask 2.2) is not mentioned in either edition, nor is decorators or the __init_subclass__ derivation of methods, which is why those parts are written here from the official documentation.

Pitfalls and Security

  • Decorating the class does nothing for the view. Use decorators. The same applies to login_required from extensions.

  • Mutable class attributes are shared. decorators = [] or any mutable default set on a base class is shared by every subclass that does not override it; append with decorators = [*Base.decorators, extra], not .append.

  • init_every_request = False plus self state is a data leak. Put per-request data on flask.g.

  • Endpoint name clashes raise AssertionError at registration. Name endpoints after the resource.

  • An extra verb inherited from a mixin is routed automatically. Check methods in a test.

  • Missing authorization on one method. Apply authentication through decorators or a hook, and authorize the object inside the method (a user may be logged in and still not own book 7).

  • State-changing methods and cookies. A MethodView that authenticates with the session cookie and accepts POST, PUT or DELETE needs CSRF protection (the forms page covers Flask-WTF’s CSRFProtect), whereas a token in the Authorization header is not sent automatically by browsers.

  • Trusting the body. Validate and bound everything from request.get_json(), cap page sizes, and limit the body size with MAX_CONTENT_LENGTH.

  • No debugger or development server in production. The examples run through the test client only.

Summary

  • A class-based view is a way to build the view function; as_view(name, *args, **kwargs) creates it and passes the arguments to __init__.

  • View needs dispatch_request; MethodView dispatches to get, post, put, patch, delete and friends, with HEAD falling back to get, and derives methods from the class body.

  • init_every_request = False shares one instance; per-request data then belongs on g.

  • decorators is the only way to decorate the generated view function; the first entry is innermost.

  • Use two `MethodView`s for a resource (collection and item) and keep storage behind an injected repository.

  • Prefer function views for simple endpoints, and keep the Cookbook idioms (is_xhr, positional paginate, json.dumps returns, reqparse) out of new code.