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 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.createAuditBase.createRestView.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#