Views API#
fastapi_restly.views implements the class-based view layer: RestView
and AsyncRestView generate CRUD endpoints from a model, View is the
bare primitive for hand-written endpoint groups, and include_view()
registers either on a FastAPI app.
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 endpoint method: the@route, FastAPI signature,response_model, andto_response. Replace only to change the HTTP contract.handle_<verb>— the 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 method: 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 “Customize RestView” in the docs):
<verb>_endpoint— the endpoint method. Owns the HTTP signature,response_model, andto_response. Rarely overridden.handle_<verb>— the 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 method 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 /endpoint method. Overridecreatefor domain logic (it is commit-free; the handler owns the commit),handle_createfor orchestration,to_responsefor the response shape; replace this method 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}endpoint method. Overridedeletefor domain logic (e.g. soft delete),handle_deletefor orchestration; replace this method 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 /endpoint method. Overrideget_manyfor domain logic,handle_get_manyfor orchestration,to_responsefor the response shape; replace this method 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}endpoint method. Overrideget_onefor domain logic (visibility lives inbuild_query),handle_get_onefor orchestration,to_responsefor the response shape; replace this method only to change the HTTP contract.
- async handle_create(schema_obj: CreateSchemaT) ModelT#
- async handle_get_many(query_params: Any) ListingResult[ModelT]#
List 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}endpoint method. Overrideupdatefor domain logic,handle_updatefor orchestration,to_responsefor the response shape; replace this method 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] = 50#
Default
page_sizewhen the client omits it (paginated views only).
- 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.
- get_relationship_loader_options() list[Any]#
Loader options for the relationships the response schema names.
Returns recursive
selectinload(...)options derived fromself.schema, applied on reads (get_one/get_many) and on the post-write reload insave_object. Override to eager-load relationships the schema does not name on both paths; append tosuper().get_relationship_loader_options()to keep the schema-derived loads. See the “Relationship Loading and Async” how-to.
- max_page_size: ClassVar[int] = 1000#
Maximum
page_sizeaccepted on list endpoints. Above this returns 422.
- model: ClassVar[type[DeclarativeBase]]#
- paginated: ClassVar[bool] = True#
Whether list endpoints paginate.
True(the default) wraps the list in aPaginatedEnvelope(dataplustotal_count/page/page_size/total_pages), emitspage/page_sizequery parameters, and runs the count query.Falsereturns every matching row in a plainEnvelope(dataonly) with no count. For a bare array or another non-envelope shape, replaceget_many_endpointwith a matchingresponse_model(seeAsyncReactAdminView).
- 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#
Serialize a
ListingResultinto the list response body.The default shape is the
{"data": [...]}envelope – with pagination metadata (total_count/page/page_size/total_pages) whenpaginatedis true. This builds the body only; the route’sresponse_modelis fixed frompaginatedseparately. For a bare array or aContent-Rangeheader, replaceget_many_endpointwith a matchingresponse_model(asAsyncReactAdminViewdoes), not by overriding this method alone, which would fail response validation.
- to_response(obj_or_list: Any, shape: ResponseShape = ResponseShape.SINGLE) Any#
Endpoint-method 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 endpoint method.
- 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.Envelope(*, data: Sequence[DataT])#
Bases:
BaseModel,Generic[DataT]List response wrapper –
{"data": [...]}.The response shape for an unpaginated view. A paginated view uses
PaginatedEnvelope, which adds the pagination metadata.- model_config: ClassVar[ConfigDict] = {}#
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class fastapi_restly.views.ListingResult(objects: Sequence[ModelT], total_count: int | None = None, query_params: Any = None)#
Bases:
Generic[ModelT]Result returned by
get_manybefore HTTP response formatting.total_countisNonefor an unpaginated listing, which does not run the count query.
- class fastapi_restly.views.PaginatedEnvelope(*, data: Sequence[DataT], total_count: int, page: int, page_size: int, total_pages: int)#
Bases:
EnvelopePaginated list response –
dataplus pagination metadata.- model_config: ClassVar[ConfigDict] = {}#
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- 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 an endpoint method asks
BaseRestView.to_response()to produce.This is separate from write-action names such as
"publish". Endpoint methods 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 “Customize RestView” in the docs): the endpoint method
<verb>_endpoint, the handlerhandle_<verb>(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 method 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 /endpoint method. Overridecreatefor domain logic (it is commit-free; the handler owns the commit),handle_createfor orchestration,to_responsefor the response shape; replace this method only to change the HTTP contract.
- delete_endpoint(id: Any) Any#
DELETE /{id}endpoint method. Overridedeletefor domain logic (e.g. soft delete),handle_deletefor orchestration; replace this method 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 /endpoint method. Overrideget_manyfor domain logic,handle_get_manyfor orchestration,to_responsefor the response shape; replace this method only to change the HTTP contract.
- get_one(id: IdT) ModelT#
- get_one_endpoint(id: Any) Any#
GET /{id}endpoint method. Overrideget_onefor domain logic (visibility lives inbuild_query),handle_get_onefor orchestration,to_responsefor the response shape; replace this method 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}endpoint method. Overrideupdatefor domain logic,handle_updatefor orchestration,to_responsefor the response shape; replace this method 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): ...
Registering a view on several parents mounts its routes on each; calling
include_viewagain with a parent the view is already registered on is a no-op.
- 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 introduces the class-based view concept and hierarchy, and Customize RestView explains the three-tier override model and collects the task-shaped override recipes.