Views & CRUD#
Views group related FastAPI endpoints in Python classes. A view declares shared route configuration and dependencies once. Subclasses inherit or override its endpoint methods.
Choose a view#
You are building |
Use |
|---|---|
One simple standalone endpoint |
A plain FastAPI route, with no Restly view |
A group of related non-CRUD endpoints: login/auth flows, webhook receivers, RPC-style actions, composite-key resources |
|
A database-backed CRUD resource |
|
CRUD plus custom actions such as publish, vote, or bulk operations |
|
Use AsyncRestView for new
applications unless the application already uses synchronous SQLAlchemy. Using
RestView defines its CRUD routes, schemas, and configuration.
Customizing RestView shows how to override those routes or add
custom actions. All three classes use the same registration, dependency
injection, and inheritance mechanics.
How class-based views work#
A class-based view (CBV) is a class that groups related endpoints together with the configuration they share. In plain FastAPI, an endpoint is a function:
@app.get("/users")
async def list_users(session: AsyncSession = Depends(get_session)):
...
@app.post("/users")
async def create_user(payload: UserCreate, session: AsyncSession = Depends(get_session)):
...
A class-based view declares the same endpoints as methods on a class instead:
import fastapi_restly as fr
from fastapi import Depends
from sqlalchemy import select
@fr.include_view(app)
class UserView(fr.View):
prefix = "/users"
dependencies = [Depends(require_logged_in)]
session: fr.AsyncSessionDep
@fr.get("")
async def list_users(self) -> list[UserSchema]:
users = await self.session.scalars(select(User))
return [UserSchema.model_validate(user) for user in users]
@fr.post("")
async def create_user(self, payload: UserCreate) -> UserSchema:
user = User(**payload.model_dump())
self.session.add(user)
await self.session.flush()
return UserSchema.model_validate(user)
Dependencies, prefix, tags, and metadata are declared once on the class. The
session attribute is a FastAPI dependency too, injected per request and
available as self.session. Methods are ordinary Python methods, so helpers,
class config, and self all work normally.
Why CBVs at all?#
Function endpoints are fine for a few routes, but they become repetitive in larger codebases:
Repetition: the same
Depends(get_session), the same auth dependency, and the same response config are duplicated across every related endpoint.Scattering: endpoints that conceptually belong together (everything about users, everything about invoices) live as separate top-level functions, so renames, splits, and shared edits become tedious.
No natural place for shared state: a request scope often has a few values that every endpoint in a group needs (the current user, a tenant context, a serialised filter). With functions, you pass them through parameters or recompute them; with a CBV, they are attributes on
self.
A CBV solves all three with one tool: the class itself.
Declaration and registration#
The base class itself is small: FastAPI router metadata plus one pre-registration hook.
class View:
prefix: ClassVar[str]
tags: ClassVar[Iterable[str | Enum] | None] = None
dependencies: ClassVar[Any] = None
responses: ClassVar[dict[int | str, dict[str, Any]]] = {}
route_options: ClassVar[Mapping[str | ViewRoute, Mapping[str, Any]]] = {}
@classmethod
def before_include_view(cls): ...
Methods on a View subclass use @fr.get(...), @fr.post(...), or
@fr.route(...). Those decorators only store route metadata; registration
happens when you call:
fr.include_view(app, UserView)
For larger apps, define classes in view modules and include them from the app/router composition layer. Small apps can use the decorator shortcut:
@fr.include_view(app)
class UserView(fr.View): ...
include_view walks the class’s MRO, collects every method tagged with route
metadata, wires each method’s self up as a dependency on the view class (so
FastAPI instantiates a fresh view per request), and registers each route on
the parent router or app.
Routes are bound at include-time against the class you pass in; they are not bound at decoration time. This is what makes subclassing work.
One base view for the whole app#
An application-wide base view declares your app’s request context once on a
bare View. Subclass it everywhere. No CRUD
is required:
from typing import Annotated
from fastapi import Depends
class AppView(fr.View):
"""Project base; every endpoint group in the app subclasses this."""
session: fr.AsyncSessionDep
current_user: Annotated[User, Depends(get_current_user)]
@fr.include_view(app)
class ProfileView(AppView):
prefix = "/profile"
@fr.get("/")
async def whoami(self) -> dict:
return {"user": self.current_user.name}
@fr.include_view(app)
class BillingView(AppView):
prefix = "/billing"
@fr.post("/checkout")
async def checkout(self, payload: CheckoutRequest):
order = Order(user_id=self.current_user.id, **payload.model_dump())
self.session.add(order)
await self.session.commit()
return {"order_id": order.id}
In plain FastAPI, session and current_user would be Depends parameters
re-declared on every function in the project. Here they are declared once and
read from self in every method of every subclass. The same base also
composes under CRUD views, so the whole app shares one context layer:
class AppRestView(AppView, fr.AsyncRestView):
"""CRUD resources get the same session + current_user attributes."""
Testing inherits the benefit: FastAPI’s dependency_overrides applies to the
class-level dependencies, so overriding get_current_user reaches
self.current_user in every view at once.
The view hierarchy#
The CRUD rows of the opening table are served by a short inheritance chain, in which each layer adds behavior to the one above it:
View ← class-based view primitive (no CRUD)
└── BaseRestView ← CRUD configuration + helpers (no endpoints)
├── RestView ← sync CRUD endpoints
│ └── ReactAdminView ← + ra-data-simple-rest contract
└── AsyncRestView ← async CRUD endpoints
└── AsyncReactAdminView ← + ra-data-simple-rest contract
Viewis the bare CBV primitive. Use it for non-CRUD endpoints: auth flows, custom RPC, file uploads, or composite-key resources.BaseRestViewadds the model, schema, list, and response helpers shared by the concrete CRUD views. It is an abstract scaffold with no endpoints of its own.RestViewandAsyncRestViewdefine the sync and async CRUD endpoint methods. Using RestView owns their default contract, configuration, and limits.
The API reference classifies the public method surface. Customizing RestView explains the three override tiers and cross-cutting override points.
Override a single tier#
AsyncRestView and RestView split every CRUD verb into three tiers: the
endpoint method (HTTP contract), the handler (authorization + commit
bracket), and the business method (domain logic, auth-free and commit-free).
One behavior change therefore means one method override, while routing,
authorization, and the commit stay framework-owned. The tier model, both
request lifecycles, the override decision table, and task-shaped recipes
live in Customizing RestView.
Dependency injection on class attributes#
A class attribute on a view is wired as a FastAPI dependency only when its annotation either:
carries an
Annotated[..., Depends(...)]marker (Security(...)counts, since it is a kind ofDepends), ornames one of FastAPI’s bare-injectable special types (
Request,Response,BackgroundTasks,WebSocket).
Every other annotation is only a type hint. This differs from a function parameter, where FastAPI reads a plain annotation as a query or body parameter. The following view demonstrates each case:
from typing import Annotated
from fastapi import Depends, Request
class UserView(fr.AsyncRestView):
# Wired: AsyncSessionDep is Annotated[AsyncSession, Depends(...)].
session: fr.AsyncSessionDep
# Wired: Request is one of FastAPI's bare-injectable specials.
request: Request
# Wired: explicit Depends marker.
current_user: Annotated[User, Depends(get_current_user)]
# NOT wired: a bare type is only a hint.
reviewer: User
A wired attribute is set on the view instance before any method runs, so
endpoint methods, business methods, authorize, and the commit hooks all
read it as self.current_user. An attribute that is not wired is never set
from the request: reading self.reviewer raises AttributeError.
The shipped AsyncSessionDep / SessionDep aliases carry Depends. The
request attribute on BaseRestView
relies on the special-type rule.
A request parameter belongs on the endpoint method that reads it. A class
attribute with a Path(), Query(), Header(), Cookie(), Body(),
Form() or File() marker raises
RestlyConfigurationError
when the view is registered, because FastAPI would never set it. A path
segment or header that every route of the view needs comes through a
dependency instead; Nested Resources shows the pattern for
a path segment.
A plain annotation never replaces a wired one. A mixin or subclass that
declares current_user: User so type checkers know the attribute keeps the
base’s Depends wiring. This rule makes mixins safe: a mixin can declare
what it expects from its host without shadowing the host’s wiring. See
Composing views with mixins for the
mixin pattern.
When to use View directly#
View is the right tool when your endpoints do not fit a CRUD shape:
@fr.include_view(app)
class AuthView(fr.View):
prefix = "/auth"
tags = ["auth"]
@fr.post("/login")
async def login(self, credentials: LoginRequest) -> Token:
...
@fr.post("/refresh")
async def refresh(self, token: str) -> Token:
...
@fr.post("/logout")
async def logout(self) -> None:
...
The class gives three related endpoints one shared prefix and tag, and a single place for auth dependencies, with no model, no schema, and no CRUD.
When not to use a CBV#
If you have a single one-off endpoint that does not share anything with others, write a plain function endpoint. CBVs pay off when you have shared metadata, shared dependencies, or related endpoints that benefit from being co-located. Do not reach for them just for the sake of structure.
Next steps#
Using RestView: the default CRUD contract, schemas, and view configuration.
Customizing RestView: the tier model behind every CRUD verb, both request lifecycles, the override decision table, and every override recipe.
Share Behaviour with Base Views: patterns for tenant isolation, role-based filtering, and shared mixins.
API Reference: full
View,BaseRestView,RestView,AsyncRestViewsignatures and class attributes.