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>answersGET,PUT,PATCHandDELETE. With aMethodVieweach method is a method of the class and the framework dispatches by name, instead of anif 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
decoratorsattribute 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.
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)
Source: Flask API: View.methods
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"]
Source: Flask API: View.as_view
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
Source: Flask API: MethodView
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
MethodViewcomputesmethodsper class. A single class for both would have to accept an optionalbook_idand branch on it. The official documentation’sregister_apihelper registers both, plus thedefaultstrick, 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 callsregister_api(app, books), and tests can pass a fake. -
init_every_request = Falseis safe here because the instance holds only a reference to a repository and no per-request data. Thepayload()helper andabort()use the request-bound proxies, which is correct. -
Status codes and headers follow HTTP.
201with aLocationheader for creation,204for a successful delete,404for a missing item,400for a malformed body,415fromget_json()when the content type is not JSON, and405from the router for a method the class does not define. -
Return dictionaries or
jsonify, neverjson.dumpsstrings. A view returningjson.dumps(...)sendstext/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"
Source: Flask: Using async and await
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.partialexpresses it as clearly; -
the team is new to Flask, because
as_viewand the lifetime rules are one more thing to learn; -
you want
@app.getand@app.postshortcuts (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 callModel.query.paginate(page, 10). Since Flask-SQLAlchemy 3.0 the arguments are keyword-only, so it raisesTypeError, andfrom flask_sqlalchemy import Paginationwas removed in 3.1. Usedb.paginate(db.select(Book).order_by(Book.title), page=page, per_page=10), as the Flask-SQLAlchemy page of this section shows. Thegetof the list view above paginates a plain list for the same shape of response. -
The legacy
Model.queryAPI in chapter 7’s views (Model.query.get(id),.filter_by(...).first()) is legacy in SQLAlchemy 2.0. Usedb.session.get(Book, book_id)ordb.get_or_404(Book, book_id)inside the methods. -
Flask-RESTful with
reqparse.reqparseis deprecated upstream and the extension is barely maintained. PresentMethodViewplusjsonifyfirst, 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 returnjson.dumps(...)strings, which are double encoded when the library serializes the return value again and are sent with the wrong content type. Return adict(Flask serializes it with the JSON provider) orjsonify(...). -
selfstate in views. The books assume a new instance per request, which is still the default. Theinit_every_requestattribute (Flask 2.2) is not mentioned in either edition, nor isdecoratorsor the__init_subclass__derivation ofmethods, 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 tologin_requiredfrom 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 withdecorators = [*Base.decorators, extra], not.append. -
init_every_request = Falseplusselfstate is a data leak. Put per-request data onflask.g. -
Endpoint name clashes raise
AssertionErrorat registration. Name endpoints after the resource. -
An extra verb inherited from a mixin is routed automatically. Check
methodsin a test. -
Missing authorization on one method. Apply authentication through
decoratorsor 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
MethodViewthat authenticates with the session cookie and acceptsPOST,PUTorDELETEneeds CSRF protection (the forms page covers Flask-WTF’sCSRFProtect), whereas a token in theAuthorizationheader 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 withMAX_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__. -
Viewneedsdispatch_request;MethodViewdispatches toget,post,put,patch,deleteand friends, withHEADfalling back toget, and derivesmethodsfrom the class body. -
init_every_request = Falseshares one instance; per-request data then belongs ong. -
decoratorsis 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, positionalpaginate,json.dumpsreturns,reqparse) out of new code.