Share Behaviour with Base Views#
FastAPI-Restly views are plain Python classes. Use base classes for shared CRUD overrides, dependencies, access control, and URL namespaces.
Each CRUD verb is three tiers (see Override Endpoints for the full model):
The route shell (
create_endpoint,get_one_endpoint, …) — the wire boundary. Rarely overridden on a base class.The request handler (
handle_create,handle_get_one, …) — runsauthorizeand the commit bracket.The business verb (
create,get_one,update,delete,get_many) — the auth-free, commit-free domain operation.
The business verb is the natural home for shared behaviour, so most of the examples below override it.
Share a CRUD override across multiple views#
Override a business verb on a base class and every subclass picks it up automatically:
class AuditBase(fr.RestView):
def create(self, schema_obj):
obj = super().create(schema_obj)
audit_log.record("created", obj)
return obj
@fr.include_view(app)
class UserView(AuditBase):
prefix = "/users"
model = User
schema = UserRead
@fr.include_view(app)
class OrderView(AuditBase):
prefix = "/orders"
model = Order
schema = OrderRead
audit_log.record now runs for both /users/ and /orders/. Register only concrete subclasses, not the base.
Because create is commit-free, the handler persists the same object the base method recorded.
Call super() to layer overrides#
A subclass can call super() to extend a base implementation:
class AuditBase(fr.RestView):
def create(self, schema_obj):
obj = super().create(schema_obj)
audit_log.record("created", obj)
return obj
@fr.include_view(app)
class OrderView(AuditBase):
prefix = "/orders"
model = Order
schema = OrderRead
def create(self, schema_obj):
schema_obj.created_by = current_user()
return super().create(schema_obj)
The call chain is OrderView.create → AuditBase.create → RestView.create. All three layers run in order.
Share an orchestration override#
When shared behavior is about timing, override the request handler instead of the business verb. This keeps the route shell unchanged:
class NotifyBase(fr.RestView):
def handle_create(self, schema_obj):
obj = super().handle_create(schema_obj)
# super().handle_create has already committed, so the row is durable.
notify_created(obj)
return obj
Every subclass of NotifyBase now fires notify_created after commit. For most post-commit side effects, prefer after_commit; use a handler override when control flow must change.
Inherit a shared dependency#
Dependencies declared as instance annotations on a base class are injected into every subclass.
from typing import Annotated
from fastapi import Depends
class AuthBase(fr.RestView):
current_user: Annotated[User, Depends(get_current_user)]
def create(self, schema_obj):
obj = super().create(schema_obj)
obj.owner_id = self.current_user.id
return obj
@fr.include_view(app)
class NoteView(AuthBase):
prefix = "/notes"
model = Note
schema = NoteRead
self.current_user is available in every subclass method. Because create runs before commit, stamping owner_id persists.
Apply router-level dependencies to all routes#
dependencies = [Depends(fn)] applies fn to every route. Subclasses inherit it:
class ProtectedBase(fr.RestView):
dependencies = [Depends(require_auth)]
@fr.include_view(app)
class UserView(ProtectedBase):
prefix = "/users"
model = User
schema = UserRead
@fr.include_view(app)
class OrderView(ProtectedBase):
prefix = "/orders"
model = Order
schema = OrderRead
Every route on /users/ and /orders/ now requires authentication.
Concatenate URL prefixes#
When a base class defines prefix, subclass prefixes are appended to it. This lets you declare a shared URL namespace once:
class ApiV1(fr.RestView):
prefix = "/api/v1"
@fr.include_view(app)
class UserView(ApiV1):
prefix = "/users" # → /api/v1/users
model = User
schema = UserRead
@fr.include_view(app)
class OrderView(ApiV1):
prefix = "/orders" # → /api/v1/orders
model = Order
schema = OrderRead
Prefixes concatenate across as many levels as you have:
class AdminBase(fr.RestView):
prefix = "/admin"
class V2Base(AdminBase):
prefix = "/v2"
@fr.include_view(app)
class ReportView(V2Base):
prefix = "/reports" # → /admin/v2/reports
model = Report
schema = ReportRead
Inherit custom routes#
Custom routes defined with @fr.get, @fr.post, etc. on a base class are inherited by all registered subclasses:
class HealthBase(fr.RestView):
@fr.get("/health")
def health(self):
return {"ok": True}
@fr.include_view(app)
class UserView(HealthBase):
prefix = "/users"
model = User
schema = UserRead
GET /users/health is registered alongside the standard CRUD endpoints.
Restrict available endpoints on a base class#
Set exclude_routes on a base class to make every subclass read-only (or whatever restriction you need):
class ReadOnlyBase(fr.RestView):
exclude_routes = (fr.ViewRoute.CREATE, fr.ViewRoute.UPDATE, fr.ViewRoute.DELETE)
@fr.include_view(app)
class ProductView(ReadOnlyBase):
prefix = "/products"
model = Product
schema = ProductRead
ProductView only exposes GET /products/ and GET /products/{id}.
Implement soft-delete once#
A base class can override the delete business verb once for every subclass
— exactly like the audit example above, with the soft-delete body. The
canonical recipe (idiom: a deleted_at timestamp) lives in
Override CRUD Behavior;
the reusable mixin that also hides flagged rows on read is in
Compose Views with Mixins.
Cross-references#
Override Endpoints — the three-tier model and the call chain.
Compose Views with Mixins — layering structural concerns cooperatively, the richer cousin to single-base inheritance.