How-To: Share Behaviour with Base Views#
FastAPI-Restly views are plain Python classes. There are no decorator wrappers or metaclass tricks that would prevent normal inheritance from working. This means you can build base view classes that capture shared logic — CRUD overrides, dependencies, access control, URL namespaces — and reuse it across every view in your project without repetition.
Share a CRUD override across multiple views#
Override any on_* hook on a base class and every subclass picks it up automatically:
class AuditBase(fr.RestView):
def on_create(self, schema_obj):
obj = super().on_create(schema_obj)
audit_log.record("created", obj)
return obj
@fr.include_view(app)
class UserView(AuditBase):
prefix = "/users"
model = User
schema = UserSchema
@fr.include_view(app)
class OrderView(AuditBase):
prefix = "/orders"
model = Order
schema = OrderSchema
audit_log.record is called on every POST to /users/ and /orders/ without repeating the override. The base class itself is never registered — only the concrete subclasses are passed to include_view.
Call super() to layer overrides#
A subclass can override an on_* hook and call super() to build on top of the base implementation rather than replace it:
class AuditBase(fr.RestView):
def on_create(self, schema_obj):
obj = super().on_create(schema_obj)
audit_log.record("created", obj)
return obj
@fr.include_view(app)
class OrderView(AuditBase):
prefix = "/orders"
model = Order
schema = OrderSchema
def on_create(self, schema_obj):
schema_obj.created_by = current_user()
return super().on_create(schema_obj)
The call chain is OrderView.on_create → AuditBase.on_create → RestView.on_create. All three layers run in order.
Inherit a shared dependency#
Dependencies declared as instance annotations on a base class are injected into every subclass. This is a clean way to make the current user, tenant, or request context available to all views without repeating the annotation.
from typing import Annotated
from fastapi import Depends
class AuthBase(fr.RestView):
current_user: Annotated[User, Depends(get_current_user)]
def on_create(self, schema_obj):
schema_obj.owner_id = self.current_user.id
return super().on_create(schema_obj)
@fr.include_view(app)
class NoteView(AuthBase):
prefix = "/notes"
model = Note
schema = NoteSchema
self.current_user is available in every method of NoteView and any other subclass of AuthBase.
Apply router-level dependencies to all routes#
dependencies = [Depends(fn)] on a view applies fn to every route the view registers. Subclasses inherit this, so you can enforce authentication or rate-limiting once on a base class:
class ProtectedBase(fr.RestView):
dependencies = [Depends(require_auth)]
@fr.include_view(app)
class UserView(ProtectedBase):
prefix = "/users"
model = User
schema = UserSchema
@fr.include_view(app)
class OrderView(ProtectedBase):
prefix = "/orders"
model = Order
schema = OrderSchema
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 = UserSchema
@fr.include_view(app)
class OrderView(ApiV1):
prefix = "/orders" # → /api/v1/orders
model = Order
schema = OrderSchema
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 = ReportSchema
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 = UserSchema
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 = ("post", "patch", "delete")
@fr.include_view(app)
class ProductView(ReadOnlyBase):
prefix = "/products"
model = Product
schema = ProductSchema
ProductView only exposes GET /products/ and GET /products/{id}.
Implement soft-delete once#
Override delete_object on a base class to change how deletion works for every subclass:
class SoftDeleteBase(fr.RestView):
def delete_object(self, obj):
obj.deleted = True
self.session.flush()
@fr.include_view(app)
class ArticleView(SoftDeleteBase):
prefix = "/articles"
model = Article
schema = ArticleSchema
DELETE /articles/{id} now sets deleted = True instead of removing the row. Every subclass of SoftDeleteBase gets this behaviour.