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 implemented in three tiers (see Customizing RestView for the full model):

The business method is the natural home for shared behaviour, so most of the examples below override it.

Share a CRUD override across multiple views#

To run the same logic on several resources, override a business method on a base class; 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 = UserSchema

@fr.include_view(app)
class OrderView(AuditBase):
    prefix = "/orders"
    model = Order
    schema = OrderSchema

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#

When one view needs its own logic on top of the shared behaviour, call super() to extend the 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 = OrderSchema

    def create(self, schema_obj):
        schema_obj.created_by = current_user()
        return super().create(schema_obj)

OrderView.create runs first and delegates to AuditBase.create, which in turn delegates to RestView.create; all three layers run in order.

Share post-commit behavior#

When shared behaviour should happen only after a durable write, override the after-hook on the base class:

class NotifyBase(fr.RestView):
    def after_action_commit(self, action, new, old=None):
        if action == "create":
            notify_created(new)

Every subclass of NotifyBase now fires notify_created after the write is durable. This remains true when a custom endpoint uses shared_write_action_commit(), which queues the hook until the outermost block commits.

Inherit a shared dependency#

To make a value such as the current user available in every subclass, declare the dependency as an instance annotation on the base class:

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 = NoteSchema

self.current_user is available in every subclass method, and because create runs before commit, stamping owner_id persists. The injection mechanism itself is described in Dependency injection on class attributes.

Apply router-level dependencies to all routes#

Setting dependencies = [Depends(fn)] applies fn to every route, and subclasses inherit it:

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.

A subclass that sets its own dependencies adds them after the base’s, the way nested FastAPI routers add up, so it cannot drop a guard the base declares. To run its own dependency first, it lists the base’s again: dependencies = [own, *ProtectedBase.dependencies]. Each one still runs once. responses add up the same way, and a subclass’s entry for a status code wins. Other class attributes, such as tags, replace the base’s value.

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"     # becomes /api/v1/users
    model = User
    schema = UserSchema

@fr.include_view(app)
class OrderView(ApiV1):
    prefix = "/orders"    # becomes /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"   # becomes /admin/v2/reports
    model = Report
    schema = ReportSchema

A prefix can also contain a path parameter, such as /projects/{project_id}. Nested Resources builds child resources this way.

Inherit custom routes#

Custom routes defined with @fr.get, @fr.post, and friends 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 to apply 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 = ProductSchema

ProductView only exposes GET /products and GET /products/{id}.

Implement soft-delete once#

A base class can override the delete business method once for every subclass, exactly like the audit example above but with the soft-delete body. The full deleted_at recipe is in Customizing RestView. The reusable mixin that also hides flagged rows on read is in Compose Views with Mixins.

Cross-references#

The patterns above build on two neighbouring pages: