Skip to content

Class-based views

Class-based views group related logic into a class instead of a flat function. Veloce offers two styles: View, where one class handles a URL through a single dispatch_request, and MethodView, where one async method maps to each HTTP verb.

from veloce import Request, Veloce, View

app = Veloce()


class IndexView(View):
    async def dispatch_request(self, request: Request):
        return {"page": "index"}


app.add_url_rule("/", view_func=IndexView.as_view("index"))

as_view("index") builds an async view function from the class and gives it the name "index" (used by url_for and route introspection). add_url_rule registers it like any other handler.

How as_view works

View.as_view(name, *class_args, **class_kwargs) returns a coroutine function that the router calls per request. Any extra positional or keyword arguments are passed to the class constructor each time an instance is built, which lets one class back several routes with different configuration:

from veloce import Request, Veloce, View

app = Veloce()


class GreetView(View):
    def __init__(self, greeting: str):
        self.greeting = greeting

    async def dispatch_request(self, request: Request):
        return {"message": self.greeting}


app.add_url_rule("/hi", view_func=GreetView.as_view("hi", "Hello"))
app.add_url_rule("/bye", view_func=GreetView.as_view("bye", "Goodbye"))

The generated view function carries view_class, methods, and __name__ for the router. The first argument dispatch_request receives is the Request; path parameters follow as keyword arguments.

Method-based dispatch with MethodView

MethodView dispatches to a method named after the HTTP verb. Define get, post, put, patch, delete, head, or options as async def; the allowed methods are inferred from which verbs you implement.

from veloce import MethodView, Request, Veloce

app = Veloce()


class UserView(MethodView):
    async def get(self, request: Request, id: int):
        return {"action": "read", "id": id}

    async def post(self, request: Request, id: int):
        return {"action": "create", "id": id}


app.add_url_rule("/users/{id:int}", view_func=UserView.as_view("user"))

A GET /users/1 calls get, a POST /users/1 calls post. A request for a verb the class does not implement raises MethodNotAllowed, which Veloce turns into a 405 response with an Allow header listing the verbs the view does support.

Note

Veloce handlers are async def. A MethodView whose verb method is a plain def raises TypeError at class-definition time, not at request time — so the mistake surfaces immediately on import.

Path and query parameters

The verb methods receive path parameters as keyword arguments, the same way function handlers do. Declarative parameter markers and dependency injection work too — annotate the method parameters as you would on a function handler.

from veloce import MethodView, Query, Request, Veloce

app = Veloce()


class SearchView(MethodView):
    async def get(self, request: Request, q: str = Query(default="")):
        return {"query": q}


app.add_url_rule("/search", view_func=SearchView.as_view("search"))

See Parameters and Dependency injection for the full set of options available on these method parameters.

Instance lifetime

By default a fresh instance is constructed for every request (init_every_request = True), so per-request attributes set in dispatch_request never leak between requests. Set the class attribute to False to reuse a single shared instance across all requests — only safe when the view holds no per-request mutable state.

from veloce import Request, Veloce, View

app = Veloce()


class CachedView(View):
    init_every_request = False

    def __init__(self):
        self.calls = 0

    async def dispatch_request(self, request: Request):
        self.calls += 1
        return {"calls": self.calls}


app.add_url_rule("/counter", view_func=CachedView.as_view("counter"))

Warning

With init_every_request = False the single instance is shared across concurrent requests. Do not store request-scoped data on self in that mode — use g or request.state instead.

Declaring methods on a plain View

A base View answers GET by default. To advertise other verbs (for routing and OpenAPI introspection) set the methods class attribute and read request.method inside dispatch_request yourself:

from veloce import Request, Veloce, View

app = Veloce()


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

    async def dispatch_request(self, request: Request):
        if request.method == "POST":
            return {"submitted": True}
        return {"form": "empty"}


app.add_url_rule("/form", view_func=FormView.as_view("form"))

Applying decorators

The decorators class attribute holds a list applied to the generated view function. Entries are applied innermost-first, so the last entry wraps outermost. This is the place for cross-cutting wrappers that should apply to the whole view regardless of which verb runs.

import functools

from veloce import MethodView, Request, Veloce

app = Veloce()


def log_calls(view_func):
    @functools.wraps(view_func)
    async def wrapper(request: Request, **kwargs):
        print(f"{request.method} {request.path}")
        return await view_func(request, **kwargs)

    return wrapper


class ReportView(MethodView):
    decorators = [log_calls]

    async def get(self, request: Request):
        return {"report": "ok"}


app.add_url_rule("/report", view_func=ReportView.as_view("report"))

Testing class-based views

Class-based views are ordinary registered routes, so the in-memory TestClient drives them like any handler:

from veloce import MethodView, Request, TestClient, Veloce

app = Veloce()


class PingView(MethodView):
    async def get(self, request: Request):
        return {"pong": True}


app.add_url_rule("/ping", view_func=PingView.as_view("ping"))


def test_ping():
    client = TestClient(app)
    assert client.get("/ping").json() == {"pong": True}
    assert client.post("/ping").status_code == 405

Next steps