Views API#
Class-based views: generated CRUD plus explicit override tiers.
Every CRUD verb on RestView / AsyncRestView exists at three tiers —
name the tier that owns your change and override one method:
<verb>_endpoint— the route shell (wire tier): the@route, FastAPI signature,response_model, andto_response. Replace only to change the HTTP contract.handle_<verb>— the request handler: runsauthorizeand the commit bracket (before_commit-> commit ->after_commit). Override for orchestration or timing.<verb>(get_many,get_one,create,update,delete) — the business verb: the domain operation, auth-free and commit-free. The usual override point.
Cross-cutting seams: build_query (read scope/visibility), authorize
(policy), apply_query_params (URL grammar), to_response (wire shape),
write_action (custom write brackets). View is the bare class-based
primitive for non-CRUD endpoint groups (auth flows, webhooks, RPC).
- class fastapi_restly.views.Action#
Bases:
objectCanonical CRUD action names passed to
authorize/before_commit/after_commit.This is a constants class, not an
Enum: custom actions and mixins add their own names. Use constants for typo checking at import time.- CREATE = 'create'#
- DELETE = 'delete'#
- GET_MANY = 'get_many'#
- GET_ONE = 'get_one'#
- UPDATE = 'update'#
- class fastapi_restly.views.AsyncReactAdminView#
Bases:
_ReactAdminMixin,AsyncRestViewAsyncRestView that speaks the ra-data-simple-rest wire contract.
Use this instead of AsyncRestView when your frontend is react-admin with ra-data-simple-rest.
- class fastapi_restly.views.AsyncRestView#
Bases:
BaseRestView[ModelT,SchemaT,CreateSchemaT,UpdateSchemaT,IdT]AsyncRestView creates an async CRUD/REST interface for database objects. Basic usage:
class FooView(AsyncRestView): prefix = "/foo" schema = FooRead model = Foo
Each verb is three tiers (see “the handle design” in the docs):
<verb>_endpoint— the route shell (wire). Owns the HTTP signature,response_model, andto_response. Rarely overridden.handle_<verb>— the request handler. Ownsauthorizeand the commit bracket (before_commit-> commit ->after_commit); returns the domain object. Reuse from custom actions to get the bracket.<verb>(get_many/get_one/create/update/delete) — the domain operation. Auth-free, commit-free; the common override point (hash a password, derive a slug, …).
- async after_commit(action: str, new: ModelT | None, old: dict[str, Any] | None = None) None#
Post-commit side effect (email, webhook, cache invalidation).
oldenables dirty detection (“notify only if the status changed”).For external effects only: the write is already durable, so mutating
newor the database here is NOT persisted. A mutation tonewalso leaks into this request’s response (which serializesnewafter this hook) while being silently discarded from storage – do the mutation in the business verb orbefore_commitinstead.
- apply_query_params(query: Select, query_params: Any) Select#
Apply URL filter/sort/pagination to
query. Override for a non-default URL grammar; the common case is driven by configuration.
- async authorize(action: str, obj: ModelT | None = None, data: Any = None) None#
Gate a verb. Called by
handle_<verb>at the right phase: before the write forcreate, and after the scoped load forupdate/delete/get_one(soobjis available for row-level checks).The default is a no-op – override to enforce policy, raising
fr.exc.Forbidden/fr.exc.NotFoundto reject (actionsays which verb;obj/datacarry the loaded row and the request payload). Row visibility – hiding a row from every caller – belongs inbuild_query, not here.
- async before_commit(action: str, new: ModelT | None, old: dict[str, Any] | None = None) None#
In-transaction side effect (outbox rows, audit rows), committed atomically with the write.
oldis the pre-mutation snapshot dict.
- build_query() Select#
Return the base SQLAlchemy
Selectused by every read on this view’s model – list, count, and retrieve. Override to addWHEREclauses that should apply to all of them (tenant scope, soft-delete filtering, row-level permission visibility). Callsuper().build_query()and chain.where(...)to compose with base-class or mixin filters.Retrieve also routes through this query, so a row hidden from the list returns 404 from
GET /{id}.
- async count(query: Select) int#
Total for the list, ignoring presentation-layer ordering/pagination.
The stripped query is made
DISTINCTand wrapped as a subquery, so the total is correct across user-provided query shapes – including abuild_querythat joins a to-many relationship, whose row fan-out would otherwise inflate the count. Override for estimated counts on huge tables.
- async create(schema_obj: CreateSchemaT) ModelT#
Build a new object and save it. Override from scratch for domain logic (e.g. hash a password): never commits, so the bracket can’t break.
- async create_endpoint(schema_obj: Any) Any#
POST /route shell (wire tier). Overridecreatefor domain logic (it is commit-free; the handler owns the commit),handle_createfor orchestration,to_responsefor the response shape; replace this shell only to change the HTTP contract.
- async delete(obj: ModelT) None#
Delete
obj. Override (e.g. on a soft-delete mixin) to flip a timestamp instead of removing the row.
- async delete_endpoint(id: Any) Any#
DELETE /{id}route shell (wire tier). Overridedeletefor domain logic (e.g. soft delete),handle_deletefor orchestration; replace this shell only to change the HTTP contract (e.g. return the deleted object instead of 204).
- async get_many(query_params: Any) ListingResult[ModelT]#
Return the scoped, filtered, paginated page plus the total count.
Routes through
build_query()(scope) +apply_query_params()(filter/sort/page) +count(). Auth-free;handle_get_manyadds theauthorizecall.
- async get_many_endpoint(query_params: Any) Any#
GET /route shell (wire tier). Overrideget_manyfor domain logic,handle_get_manyfor orchestration,to_responsefor the response shape; replace this shell only to change the HTTP contract.
- async get_one(id: IdT) ModelT#
Load one object through
build_query()(scope + 404).Auth-free: visibility comes from
build_query, so a row hidden by the scope is a clean 404 for every caller.handle_get_oneadds read-auth.
- async get_one_endpoint(id: Any) Any#
GET /{id}route shell (wire tier). Overrideget_onefor domain logic (visibility lives inbuild_query),handle_get_onefor orchestration,to_responsefor the response shape; replace this shell only to change the HTTP contract.
- async handle_create(schema_obj: CreateSchemaT) ModelT#
- async handle_get_many(query_params: Any) ListingResult[ModelT]#
List request handler:
authorizethen theget_manydomain op.
- async handle_get_one(id: IdT) ModelT#
Retrieve handler: scoped load (404 by visibility) then read-auth.
Reusable from custom actions as “load with scope + 404 + read-auth”.
- async handle_update(id: IdT, schema_obj: UpdateSchemaT) ModelT#
- session: Annotated[AsyncSession, Depends(dependency=_async_generate_session, use_cache=True, scope=function)]#
- async update(obj: ModelT, schema_obj: UpdateSchemaT) ModelT#
Apply the update payload to
objand save it.
- async update_endpoint(id: Any, schema_obj: Any) Any#
PATCH /{id}route shell (wire tier). Overrideupdatefor domain logic,handle_updatefor orchestration,to_responsefor the response shape; replace this shell only to change the HTTP contract.
- write_action(action: str, *, obj: ~typing.Any = <object object>, data: ~typing.Any = None)#
Run a custom write action through the standard write bracket.
Use this for non-CRUD actions such as publish or change-password:
async with self.write_action("publish", obj=article): # in-place article.status = "published"
For create-shaped actions, omit
objand setw.objbefore exit:async with self.write_action("create", data=req) as w: w.obj = await self.make_new_object(req)
Pass
obj=Nonefor writes with no single object. Exceptions skip the commit.
- class fastapi_restly.views.BaseRestView#
Bases:
View,Generic[ModelT,SchemaT,CreateSchemaT,UpdateSchemaT,IdT]Base class for RestView implementations.
This class contains the common functionality shared between AsyncRestView and RestView, including schema definitions, model configuration, and common CRUD operation logic.
- classmethod before_include_view()#
Apply type annotations needed for FastAPI, before creating an APIRouter from this view and registering it.
This function can be overridden to further tweak the endpoints before they are added to FastAPI.
- default_page_size: ClassVar[int | None] = None#
Default
page_sizefor list endpoints.Nonemeans “no implicit cap” (the framework default). Override per-view.
- extra_query_params: ClassVar[Iterable[str]] = ()#
Extra query-parameter keys to allow on the listing endpoint beyond those derived from the response schema. Use this when a view consumes a custom parameter (e.g.
?include_deleted=trueon a soft-delete mixin). Without this, the strict unknown-key guard rejects the request with 422.
- max_page_size: ClassVar[int] = 1000#
Maximum
page_sizeaccepted on list endpoints. Above this returns 422.
- model: ClassVar[type[DeclarativeBase]]#
- snapshot(obj: Any) dict[str, Any]#
Frozen capture of an object’s already-loaded column values, passed as
oldtobefore_commit/after_commitfor dirty detection. Override to change whatoldcaptures (e.g. include a relationship’s prior state); the default delegates tofastapi_restly.snapshot().
- to_listing_response(query_params: Any, listing_result: ListingResult[ModelT]) Any#
- to_paginated_listing_response(query_params: Any, listing_result: ListingResult[Any]) dict[str, Any]#
- to_response(obj_or_list: Any, shape: ResponseShape = ResponseShape.SINGLE) Any#
Route-shell response boundary.
shapeselects the wire form: single object, listing, or empty. It is not the write-action name. Override for envelopes or shape-wide status behavior; per-endpoint projections belong in the route shell.
- to_response_schema(obj: ModelT | SchemaT) SchemaT#
Serialize an ORM object to the configured response schema.
WriteOnly fields are stripped from responses by
exclude=Trueon the marker itself (recursively, at serialization time), so a pre-built schema instance is safe to return as-is. The ORM path below still validates through the WriteOnly-omitting response schema, so a read schema that declares a WriteOnly field the ORM object doesn’t carry (e.g.passwordbacked by apassword_hashcolumn) doesn’t fail response validation.
- class fastapi_restly.views.ListingResult(objects: Sequence[ModelT], total_count: int, query_params: Any = None)#
Bases:
Generic[ModelT]Result returned by
get_manybefore HTTP response formatting.
- class fastapi_restly.views.ReactAdminView#
Bases:
_ReactAdminMixin,RestViewRestView that speaks the ra-data-simple-rest wire contract.
Use this instead of RestView when your frontend is react-admin with ra-data-simple-rest.
- class fastapi_restly.views.ResponseShape(*values)#
-
The wire shape a route shell asks
BaseRestView.to_response()to produce.This is separate from write-action names such as
"publish". Route shells choose one of these three response shapes; custom actions remain an open string namespace.- EMPTY = 'empty'#
- LISTING = 'listing'#
- SINGLE = 'single'#
- class fastapi_restly.views.RestView#
Bases:
BaseRestView[ModelT,SchemaT,CreateSchemaT,UpdateSchemaT,IdT]RestView creates a sync CRUD/REST interface for database objects. Basic usage:
class FooView(RestView): prefix = "/foo" schema = FooRead model = Foo
Each verb is three tiers (see “the handle design” in the docs): the
<verb>_endpointroute shell, thehandle_<verb>request handler (authorize + commit bracket), and the bare verb<verb>(the domain operation – the common override point).- after_commit(action: str, new: ModelT | None, old: dict[str, Any] | None = None) None#
Post-commit side effect (email, webhook, cache).
For external effects only: the write is already durable, so mutating
newor the database here is NOT persisted (and a mutation tonewleaks into this request’s response while being discarded from storage). Do the mutation in the business verb orbefore_commitinstead.
- apply_query_params(query: Select, query_params: Any) Select#
Apply URL filter/sort/pagination to
query.
- authorize(action: str, obj: ModelT | None = None, data: Any = None) None#
Gate a verb. Sync counterpart of
AsyncRestView.authorize()– a no-op by default; override to enforce policy and raisefr.exc.Forbidden/fr.exc.NotFoundto reject. Row visibility belongs inbuild_query.
- before_commit(action: str, new: ModelT | None, old: dict[str, Any] | None = None) None#
In-transaction side effect (outbox/audit), atomic with the write.
- build_query() Select#
Return the base SQLAlchemy
Selectused by every read – list, count, and retrieve. Override to addWHEREclauses (tenant scope, soft-delete, row-level visibility) that apply to all three.
- count(query: Select) int#
Total for the list, ignoring presentation ordering/pagination.
Made
DISTINCTbefore counting so abuild_querythat joins a to-many relationship doesn’t inflate the total via row fan-out.
- create(schema_obj: CreateSchemaT) ModelT#
- create_endpoint(schema_obj: Any) Any#
POST /route shell (wire tier). Overridecreatefor domain logic (it is commit-free; the handler owns the commit),handle_createfor orchestration,to_responsefor the response shape; replace this shell only to change the HTTP contract.
- delete_endpoint(id: Any) Any#
DELETE /{id}route shell (wire tier). Overridedeletefor domain logic (e.g. soft delete),handle_deletefor orchestration; replace this shell only to change the HTTP contract (e.g. return the deleted object instead of 204).
- get_many(query_params: Any) ListingResult[ModelT]#
- get_many_endpoint(query_params: Any) Any#
GET /route shell (wire tier). Overrideget_manyfor domain logic,handle_get_manyfor orchestration,to_responsefor the response shape; replace this shell only to change the HTTP contract.
- get_one(id: IdT) ModelT#
- get_one_endpoint(id: Any) Any#
GET /{id}route shell (wire tier). Overrideget_onefor domain logic (visibility lives inbuild_query),handle_get_onefor orchestration,to_responsefor the response shape; replace this shell only to change the HTTP contract.
- handle_create(schema_obj: CreateSchemaT) ModelT#
- handle_get_many(query_params: Any) ListingResult[ModelT]#
- handle_get_one(id: IdT) ModelT#
- handle_update(id: IdT, schema_obj: UpdateSchemaT) ModelT#
- update(obj: ModelT, schema_obj: UpdateSchemaT) ModelT#
- update_endpoint(id: Any, schema_obj: Any) Any#
PATCH /{id}route shell (wire tier). Overrideupdatefor domain logic,handle_updatefor orchestration,to_responsefor the response shape; replace this shell only to change the HTTP contract.
- write_action(action: str, *, obj: ~typing.Any = <object object>, data: ~typing.Any = None)#
Run a custom write action through the standard write bracket.
Use this for non-CRUD actions such as publish or change-password:
with self.write_action("publish", obj=article): article.status = "published"
For create-shaped actions, omit
objand setw.objbefore exit. Passobj=Nonefor writes with no single object. Exceptions skip the commit.
- class fastapi_restly.views.View#
Bases:
objectClass-based view primitive for FastAPI.
Group related endpoints on a class, share dependencies and metadata via class attributes, and let subclasses override individual handlers. Routes are bound at
include_view()time, not at class-definition time, so subclassing works the way Python developers expect: override a method on a subclass and the override is what runs.Most users will subclass
RestVieworAsyncRestView, which extendViewwith CRUD scaffolding. UseViewdirectly for grouped non-CRUD endpoints (auth flows, custom RPC routes, etc.).- classmethod before_include_view()#
- class fastapi_restly.views.ViewRoute(*values)#
-
Generated CRUD routes that can be referenced by view options.
Values are the route-shell method names so
exclude_routescan drop them.- CREATE = 'create_endpoint'#
- DELETE = 'delete_endpoint'#
- GET_MANY = 'get_many_endpoint'#
- GET_ONE = 'get_one_endpoint'#
- UPDATE = 'update_endpoint'#
- async fastapi_restly.views.async_run_write_action(host: AsyncWriteHost, action: str, *, obj: Any = None, data: Any = None, mutate: Callable[[], Awaitable[T]]) T#
Run
mutateinside the async write bracket and return its result.
- fastapi_restly.views.delete(path: str, **api_route_kwargs: Any) Callable[[...], Any]#
Decorator to mark a View method as a DELETE endpoint.
Equivalent to:
@route(path, methods=["DELETE"], status_code=204, ... )
- fastapi_restly.views.get(path: str, **api_route_kwargs: Any) Callable[[...], Any]#
Decorator to mark a View method as a GET endpoint.
Equivalent to:
@route(path, methods=["GET"], status_code=200, ... )
- fastapi_restly.views.include_view(parent_router: APIRouter | FastAPI, view_cls: V | None = None) V | Callable[[V], V]#
Add a View class’s routes to a FastAPI app or APIRouter.
Prefer the direct call form from your app/router composition layer:
include_view(app, MyView)
For small apps, it can also be used as a decorator:
@include_view(app) class MyView(AsyncRestView): ...
- fastapi_restly.views.patch(path: str, **api_route_kwargs: Any) Callable[[...], Any]#
Decorator to mark a View method as a PATCH endpoint.
Equivalent to:
@route(path, methods=["PATCH"], status_code=200, ... )
- fastapi_restly.views.post(path: str, **api_route_kwargs: Any) Callable[[...], Any]#
Decorator to mark a View method as a POST endpoint.
Equivalent to:
@route(path, methods=["POST"], status_code=201, ... )
- fastapi_restly.views.put(path: str, **api_route_kwargs: Any) Callable[[...], Any]#
Decorator to mark a View method as a PUT endpoint.
Equivalent to:
@route(path, methods=["PUT"], status_code=200, ... )
- fastapi_restly.views.route(path: str, **api_route_kwargs: Any) Callable[[...], Any]#
Decorator to mark a View method as an endpoint. The path and api_route_kwargs are passed into APIRouter.add_api_route(), see for example: https://fastapi.tiangolo.com/reference/apirouter/#fastapi.APIRouter.get
Endpoints methods are later added as routes to the FastAPI app using include_view()
- fastapi_restly.views.run_write_action(host: WriteHost, action: str, *, obj: Any = None, data: Any = None, mutate: Callable[[], T]) T#
Sync variant of
async_run_write_action().
See also
Class-Based Views — the class-based view concept and hierarchy; How Overrides Work: The Three Tiers — the three-tier override model; Override CRUD Behavior and Add Custom Endpoints — task-shaped override recipes.