Behavior and Configuration Reference#

CRUD views define default HTTP routes, schema rules, and transaction behavior. This page lists those defaults, view configuration attributes, and public methods. API Reference lists the public modules. API Modules documents complete signatures.

Default CRUD Routes#

Register a view with fr.include_view(app, ViewClass) or @fr.include_view(app). fr.AsyncRestView and fr.RestView define the same default CRUD endpoint methods. include_view registers them as these routes:

Method

Path

Purpose

Default Status

GET

/{prefix}

List resources

200

POST

/{prefix}

Create resource

201

GET

/{prefix}/{id}

Get resource by ID

200

PATCH

/{prefix}/{id}

Partial update

200

DELETE

/{prefix}/{id}

Delete resource

204

OpenAPI uses the collection path without a trailing slash as the canonical form. Restly also accepts /{prefix}/ as a hidden compatibility alias. Use /{prefix} in templates, examples, and contract tests.

The default routes share these conventions:

  • Updates use PATCH, not PUT. React Admin views also expose PUT /{id} for ra-data-simple-rest; see React Admin Integration.

  • GET /{id} and DELETE /{id} return 404 when the object is not found.

  • Built-in integer and UUID item routes use typed path matching. Values that do not match fall through to another route or return 404.

  • Read-only schema fields are ignored on create/update.

  • *_id: fr.MustExist[int, Model] inputs are validated against the database: the referenced row must exist. The scalar id is the related primary-key type, such as int or UUID.

List Endpoint Behavior#

GET /{prefix} accepts filter, sort, and pagination parameters derived from the view’s response class; keys use public field names (aliases included), and dotted paths filter on relations. The table below gives the grammar in one line each; the full description, including comma semantics, LIKE escaping, foreign-key filtering, and alias rules, is in Filter, Sort, and Paginate Lists:

Kind

Form

Equality / OR

?name=John, ?status=active,pending

Operators

__in, __gte, __lte, __gt, __lt, __ne, __isnull, __contains, __icontains

Relation paths

?writer.authorName=Alice (aliases per segment)

Sorting

?sort=name,-created_at

Pagination

?page=2&page_size=10

Unknown keys

rejected with 422

Pagination is on by default, and list responses are wrapped in a data envelope. Two class attributes on RestView / AsyncRestView tune this behavior:

Attribute

Type

Default

Purpose

pagination

ClassVar[NumberedPagination | NoPagination | None]

NumberedPagination()

How list endpoints paginate. A NoPagination returns every row and runs no count query; None is short for NoPagination(), a plain Envelope (data only). Views inherit it, so a project base view sets it once.

extra_query_params

ClassVar[Iterable[str]]

()

Query keys to allow beyond those derived from the response class, for view-specific parameters read from the request outside the list grammar (e.g. ?include_deleted=true). A key that the endpoint method or a dependency declares, or an APIKeyQuery key, needs no entry.

fr.NumberedPagination holds the settings. Each is a keyword argument, and replace(**changes) returns a checked copy:

Setting

Default

Purpose

default_page_size

50

Page size when the client sends none. Lower it and cap max_page_size on public endpoints.

max_page_size

1000

Largest page size a client may ask for; a larger one is rejected with 422.

max_page

None

Largest page number a client may ask for; a larger one is rejected with 422.

page_query_param

"page"

Query parameter for the page number.

page_size_query_param

"page_size"

Query parameter for the page size.

envelope

PaginatedEnvelope

The list response model: a generic model filled by field name from data, total_count, page, page_size and total_pages.

fr.NoPagination has one setting, envelope, Envelope by default, filled from data and total_count, the number of rows.

The envelope’s shape and custom alternatives are covered in Response Envelopes and List Metadata.

At a lower level, fr.query.derive_schema_list_params(...) and fr.query.apply_list_params(...) power the default list endpoint. Use the view classes for normal CRUD. Call these helpers directly only for custom endpoints that need the same list grammar, and pass validated list params instead of raw QueryParams. Pass the view’s settings to derive_schema_list_params as pagination=self.pagination; apply_list_params then reads them from the list params model. It takes (query, list_params, model, schema_response), the same arguments in the same order as the view method apply_list_params(query, list_params). Pass the view’s self.schema_response, not self.schema: the list params follow the response class.

Endpoint Decorators#

Use these decorators on methods in a view class:

Decorator

HTTP Method

Default Status

@fr.get(path)

GET

200

@fr.post(path)

POST

201

@fr.patch(path)

PATCH

200

@fr.put(path)

PUT

200

@fr.delete(path)

DELETE

204

@fr.route(path, ...)

Custom

As configured

The shorthand decorators explicitly set the default status code shown. Pass status_code= to override it.

Other keyword arguments pass through to FastAPI route registration: response_model=, dependencies=, responses=, tags=, and other APIRouter.add_api_route() options.

@fr.put(...) is available for custom endpoints, but the default update route uses PATCH.

Route Exclusion#

To disable default CRUD routes on a view, set exclude_routes:

@fr.include_view(app)
class UserView(fr.AsyncRestView):
    prefix = "/users"
    model = User
    exclude_routes = [fr.ViewRoute.DELETE, fr.ViewRoute.UPDATE]

The valid route values for exclusion are fr.ViewRoute.GET_MANY, fr.ViewRoute.GET_ONE, fr.ViewRoute.CREATE, fr.ViewRoute.UPDATE, and fr.ViewRoute.DELETE.

exclude_routes accepts ViewRoute values or the equivalent endpoint method names, such as "delete_endpoint". Worked examples are in Exclude CRUD routes.

Response Modeling#

The inherited CRUD endpoint methods derive their request and response schemas from the view’s configuration:

  • The response schema is schema_response. By default, Restly builds it from schema: the schema without its WriteOnly fields, named like UserResponse. Without a schema, Restly generates one from the model, named like UserSchema (see Generated Class Names).

  • The input schema for POST defaults to the schema without read-only fields (schema_create, generated as *Create).

  • The input schema for PATCH defaults to the optionalized schema (schema_update, generated as *Update).

  • Alias-aware serialization is applied, so response payload keys follow schema aliases.

The derivation rules are described in Generated Input Schemas.

Key Public Symbols#

The tables below summarize public symbols by module. API Modules documents their complete signatures.

Model Base Classes#

These base classes and mixins form the declarative foundation for SQLAlchemy models:

Symbol

Description

fr.DataclassBase

SQLAlchemy declarative base with dataclass semantics and auto snake_case table names. Mixes in SQLAlchemy’s AsyncAttrs, so every model has awaitable_attrs.

fr.IDBase

Convenience alias combining DataclassBase with an auto-incrementing integer id primary key.

fr.TimestampsMixin

Dataclass mixin adding created_at / updated_at to any DataclassBase subclass.

fr.models.IDMixin

Dataclass mixin adding integer id to a custom DataclassBase subclass.

fastapi_restly.models.CASCADE_ALL_ASYNC

Cascade string for use with relationship(cascade=...) in async SQLAlchemy models. Equivalent to "save-update, merge, delete, expunge". SQLAlchemy’s default "all" includes "refresh-expire", which makes a plain session.refresh(obj) expire related objects that then lazy-load on access. Restly’s own save_object refreshes by attribute name and does not cascade, so "all" is safe on Restly’s write paths; use this constant where your own code refreshes on an async session. Import from fastapi_restly.models (not exposed at the top level).

fastapi_restly.models.CASCADE_ALL_DELETE_ORPHAN_ASYNC

Like CASCADE_ALL_ASYNC but also includes "delete-orphan".

FastAPI-Restly also works with ordinary SQLAlchemy models that inherit from your own DeclarativeBase. Use fr.IDBase for Restly’s dataclass convenience base; bring your own base for standard constructor semantics or existing model layers.

On DataclassBase and IDBase, a plain Mapped[datetime] represents a UTC instant and maps to DateTime(timezone=True). PostgreSQL enforces that through its timestamptz column type. SQLite does not preserve timezone metadata and returns naive datetime values. Use mapped_column(DateTime()) to opt a specific wall-clock field out of timezone-aware storage.

RestView and AsyncRestView assume one scalar resource identifier at /{id}. The column can have another name when you provide explicit schemas, but the default CRUD routes, IDSchema, IDRef, React Admin, and OpenAPI identity shape all remain scalar-id contracts. A composite key is addressed from a custom route on the view, which loads with a predicate (handle_get_one(sa.and_(Model.a == a, Model.b == b)), see Look a row up by another key); fr.View with explicit routes such as @fr.get("/{tenant_id}/{slug}") remains the option for a hand-written group.

Schema Classes and Utilities#

These classes and markers define how model data crosses the wire; the reference-field types (MustExist, IDRef, IDSchema) are treated in depth in Work with Foreign Keys and Relationships:

Symbol

Description

fr.BaseSchema

Thin Pydantic base equivalent to class BaseSchema(pydantic.BaseModel): model_config = pydantic.ConfigDict(from_attributes=True). Plain Pydantic models are also accepted for explicit create/update schemas.

fr.IDSchema

Response-schema base class that adds the resource’s own read-only id field.

fr.MustExist[int, Model]

Existence-checked scalar FK type for a *_id column. Primary-key type first, target model second (fr.MustExist[UUID, Account] for a UUID key; drop the model to infer it from a single ForeignKey). The value stays a plain id on request and response, and Restly validates the referenced row exists.

fr.RefExists(Model, scope=...)

Existence check with the target named explicitly and a per-field scope: Annotated[int, fr.RefExists(Item, scope=ItemClauses.trashed)] checks against that predicate instead of the model’s default_scope, and scope=fr.clauses.UNSCOPED checks unscoped. See References: overriding per field.

fr.IDRef[Model]

Relationship reference with a flat-id wire; resolves the id to the related object. Wire format is the raw id (5) on request and response; dict input ({"id": 5}) is also accepted. Use this for a relationship field and React Admin scalar id arrays.

fr.IDSchema[Model]

Nested relationship-object field type. Wire format is {"id": 5} on request and response. Use this when a client expects relationship objects instead of flat scalar ids.

fr.TimestampsSchemaMixin

Pydantic mixin adding read-only created_at / updated_at fields to a schema.

fr.ReadOnly[T]

Type annotation marker. Fields annotated ReadOnly[T] are left out of the generated create/update input schemas, and never written from a payload whose own schema marks them. An explicit write schema decides for itself.

fr.WriteOnly[T]

Type annotation marker. Fields are accepted on input and excluded from Pydantic serialization, including CRUD responses and direct model_dump() calls.

fastapi_restly.schemas.derive_schema(model)

Auto-generate a Pydantic schema from a SQLAlchemy model. With its defaults, it returns the schema that a view generates when it has none. Useful for scaffolding, prototypes, and internal tools; prefer explicit schemas for stable public API contracts. Import from fastapi_restly.schemas; it is intentionally not exported at the top level.

fastapi_restly.schemas.derive_schema_response(schema)

The response class that a view builds from its schema: the schema without its WriteOnly fields, named like UserResponse. Returns the same class as the view, so a custom route can name it.

fastapi_restly.schemas.derive_schema_list_response(schema_response, pagination=...)

The list response class of a view: the envelope of its pagination, filled with its response class, named like UserListResponse. With the view’s pagination, it returns the same class as the view, so a custom list route can name it.

View Classes#

Views are the routing layer; each class below is a registration entry point:

Symbol

Description

fr.View

Base class for all class-based views. Subclass this directly when you do not need CRUD; add endpoints with @fr.get, @fr.post, etc.

fastapi_restly.views.BaseRestView

Supported advanced base class for custom CRUD foundations shared by sync and async views. Import from fastapi_restly.views; it is intentionally not exported at the top level.

fr.AsyncRestView

Async CRUD view. Use with async SQLAlchemy sessions.

fr.RestView

Sync CRUD view. Use with sync SQLAlchemy sessions.

fr.ListResult

Value object returned by get_many (and handle_get_many), with .objects, .total_count, and .list_params, before to_list_response formats the HTTP response.

fr.AsyncReactAdminView

Async CRUD view that speaks the ra-data-simple-rest wire contract used by react-admin. See React Admin Integration.

fr.ReactAdminView

Sync variant of AsyncReactAdminView.

View Method Surface#

Each CRUD verb on RestView / AsyncRestView is split into three tiers: the endpoint method (<verb>_endpoint), the handler (handle_<verb>), and the business method (<verb>). You override the endpoint method or the business method, whichever owns your change; the handler is final. The model and the decision table live in Customizing RestView.

Alongside the tiers are the declared read scope (the scope class attribute) and cross-cutting override points (apply_list_params, count, authorize, before_action_commit / after_action_commit, to_response, get_relationship_loader_options, snapshot). The handlers and the domain utilities (make_new_object, update_object, save_object) are final: you call them rather than override them, and a view class that defines one fails at class definition.

On AsyncRestView, the handlers, the business methods, the domain utilities, count, authorize and the transaction hooks are async def; the response methods, apply_list_params, snapshot and get_relationship_loader_options are plain def, and the two brackets are entered with async with. Argument names are identical between variants.

Tier / kind

Method

Signature

Return

Purpose

Endpoint method

get_many_endpoint

(list_params)

the list response, such as UserListResponse: the pagination’s envelope (PaginatedEnvelope by default, or Envelope) filled with the response class

GET /; validates query parameters and serializes the list result via to_response.

Endpoint method

get_one_endpoint

(id)

response schema

GET /{id}; serializes one retrieved object.

Endpoint method

create_endpoint

(schema_obj)

response schema

POST /; serializes the created object.

Endpoint method

update_endpoint

(id, schema_obj)

response schema

PATCH /{id}; serializes the updated object.

Endpoint method

delete_endpoint

(id)

fastapi.Response

DELETE /{id}; returns 204 by default.

Handler

handle_get_many

(list_params, *, scope=None, where=None)

ListResult[Model]

Run authorize("get_many"), then get_many, forwarding scope=. A route names its own scope here (a trash list); fr.clauses.UNSCOPED reads past the view scope. where= takes an expression or a clause and narrows inside the scope; it reaches get_many folded into scope. Final.

Handler

handle_get_one

(id, *, scope=None)

Model

Load through get_one (scoped, 404), forwarding scope=, then authorize("get_one", obj=...). Reusable from a custom read route as “scoped load + 404 + read-auth”; a write action loads with get_one and gates its own action. id takes a predicate the way get_one does, so a natural-key route is this handler with Item.slug == slug. Final.

Handler

handle_create

(schema_obj)

Model

Authorize, run create, then the commit bracket. Inside shared_write_action_commit, return after flush but before commit and the after-hook. Final.

Handler

handle_update

(id, schema_obj)

Model

Load, authorize, snapshot, run update, then the commit bracket. id also takes a predicate, so an update by natural key runs the same bracket. Inside shared_write_action_commit, return after flush but before commit and the after-hook. Final.

Handler

handle_delete

(id)

None

Load, authorize, snapshot, run delete, then the commit bracket. id also takes a predicate. Inside shared_write_action_commit, return after flush but before commit and the after-hook. Final.

Custom-action bracket

write_action

(action, *, obj=..., data=None)

context manager

Entered as async with self.write_action("publish", obj=...):, it runs the full bracket around your inline mutation: authorize and snapshot on enter; before_action_commit, commit, and after_action_commit on exit. Use it for a custom write action that is not a plain create/update/delete; deposit a create’s new object on the yielded handle’s .obj; omit obj for that create-shaped form, and pass obj=None for a write with no single object. The implementation is shared with the CRUD handlers via the self-free run_write_action (in fastapi_restly.views).

Commit bracket

shared_write_action_commit

()

context manager

Share one commit across write actions on the session. The outermost block commits, then runs their after-hooks. See Commit several writes together.

Business method

get_many

(list_params, *, scope=None)

ListResult[Model]

Scoped and filtered list via scope when given, else the view scope, + apply_list_params. Paginated views return one page plus a total count; unpaginated views return every matching row with total_count=None and skip count. Auth-free. The handlers always forward scope=, so an override declares the parameter and passes it on.

Business method

get_one

(id, *, scope=None)

Model

Load one row through scope when given, else the view scope, or raise fr.exc.NotFound. Visibility comes from the scope, so a hidden row is a clean 404 for every caller. Auth-free. The handlers always forward scope=, so an override declares the parameter and passes it on. id is the primary key by default; a SQLAlchemy boolean expression replaces that criterion (get_one(Item.slug == slug)), which is also how a composite key is addressed. A criterion narrows inside the scope, and more than one match raises MultipleResultsFound. See Look a row up by another key.

Business method

create

(schema_obj)

Model

Build a new object and save it. Commit-free: the usual create override point.

Business method

update

(obj, schema_obj)

Model

Apply the update payload to obj and save it. Commit-free.

Business method

delete

(obj)

None

Remove obj and flush. Override (e.g. on a soft-delete mixin) to flip a timestamp instead.

Configuration

scope

class attribute

WhereClause | Unscoped | None

The clause every read applies, shared by get_many, count, and get_one. None falls back to the model’s declared default_scope; fr.clauses.UNSCOPED reads unscoped. See Scopes.

Function

fr.resolve_scope(view_or_model)

(view_or_model)

WhereClause | Unscoped

The scope a read applies, resolved down the ladder: the view’s scope, the model’s default_scope, then fr.clauses.UNSCOPED. Takes a view class or instance, or a mapped model class for the model rung alone. Apply it with fr.apply_clauses in a route that builds its own query (a count, a nested list) and the route sees the rows get_one and get_many see. See Reading the resolved scope.

Override point

apply_list_params

(query, list_params)

sqlalchemy.Select

Apply URL filter/sort/pagination to query. Override for a non-default URL grammar.

Override point

count

(query)

int

Total for a paginated list: receives the same params-applied query, strips ORDER BY, LIMIT, and OFFSET, and counts it DISTINCT through a subquery, so a query that joins a to-many relationship does not inflate the total. Unpaginated views skip it. Override for estimated counts on huge tables.

Override point

authorize

(action, obj=None, data=None)

None

Gate a verb. A no-op by default; override to enforce policy and raise fr.exc.Forbidden / fr.exc.NotFound to reject. Row visibility belongs in the scope.

Override point

before_action_commit

(action, new, old=None)

None

In-transaction side effect (outbox/audit rows), atomic with the write. old is the pre-mutation snapshot dict.

Override point

after_action_commit

(action, new, old=None)

None

Post-commit side effect (email, webhook, cache invalidation). old enables dirty detection. An exception here fails the request but cannot undo the write; see When after_action_commit raises.

Override point

to_response

(result, shape=ResponseShape.SINGLE)

response payload

The single wire-level response method, called by the endpoint methods with the wire ResponseShape (SINGLE / LIST / EMPTY), not the write action. Override for envelopes or custom status codes; for a per-verb HTTP contract change, override that verb’s endpoint method.

Override point

snapshot

(obj)

dict[str, Any]

Frozen capture of an object’s already-loaded column values, taken after authorize and before the mutation, passed as old to the commit hooks.

Override point

get_relationship_loader_options

()

list[Any]

Loader options (selectinload(...)) for the relationships the response class names, applied on reads (get_one / get_many) and on the write-response reload in save_object. Override to eager-load relationships the response class does not name on both paths; see Relationship Loading and Async.

Helper

to_single_response

(obj)

instance of schema_response

Validate and serialize one ORM object with Restly’s alias/reference/write-only handling. Override for custom projections or an intentional model_construct() fast path.

Helper

to_list_response

(list_result)

envelope model instance

Build the list response body: an instance of the pagination’s envelope, or of Envelope when pagination is None. The page comes from list_result.list_params. To change the shape, set the pagination’s envelope. A shape no envelope model can express, such as a header, needs get_many_endpoint replaced with a matching response_model.

Domain utility

make_new_object

(schema_obj)

Model

Build and stage a new object without flushing, resolving references and skipping read-only fields. Final.

Domain utility

update_object

(obj, schema_obj)

Model

Apply writable fields without flushing, resolving references. Final.

Domain utility

save_object

(obj)

Model

Flush and refresh a staged object, then eager-load the relationships the response class names (via get_relationship_loader_options). Does not commit; handle_<verb> owns the commit. Final.

Internal methods prefixed with _, such as _reject_unknown_query_params, are implementation details even though they are visible on instances.

See Views for the class hierarchy, Customizing RestView for examples of choosing which tier to override, and Use Type Annotations for the typed signatures of these methods.

View Class Attributes#

Every View subclass, CRUD or not, honors these class attributes:

Attribute

Type

Description

prefix

ClassVar[str]

URL prefix for all routes in the view (e.g. "/users"). Required.

tags

ClassVar[Iterable[str | Enum] | None]

OpenAPI tags. When unset, a tag derived from the view class name is used; setting this replaces the derived tag.

dependencies

ClassVar[Sequence[Depends] | None]

FastAPI dependencies applied to every route in the view. Each class in the hierarchy adds its own, base first.

responses

ClassVar[dict[int | str, dict[str, Any]]]

OpenAPI response overrides. View defaults to {}; BaseRestView defaults to {404: {"description": "Not found"}}. Each class in the hierarchy adds its own, and a subclass’s entry for a status code wins.

route_options

ClassVar[Mapping[str | ViewRoute, Mapping[str, Any]]]

Per-route FastAPI keyword arguments. See OpenAPI customization for names, operation IDs, and override rules.

RestView and AsyncRestView add the following:

Attribute

Type

Description

schema

ClassVar[type[pydantic.BaseModel]]

The view’s schema. Restly derives the response, create and update schemas from it. If omitted, auto-generated from model as <Model>Schema.

schema_response

ClassVar[type[pydantic.BaseModel]]

The class of everything that goes out: the responses, the list items, the fields the list params filter on, and the relationships the view loads. Auto-derived by removing WriteOnly fields and named ModelResponse. See Your own response class.

schema_create

ClassVar[type[pydantic.BaseModel]]

Schema for POST input. Auto-derived by removing ReadOnly fields and named ModelCreate.

schema_update

ClassVar[type[pydantic.BaseModel]]

Schema for PATCH input. Auto-derived by making all writable fields optional and named ModelUpdate.

model

ClassVar[type[Any]]

The SQLAlchemy mapped class. A class without a mapper raises at class definition.

id_type

ClassVar[type | None]

Type of the {id} path parameter on the default routes. None (the default) takes the model’s primary key type; a composite key gets int.

exclude_routes

ClassVar[Iterable[str | ViewRoute]]

Route names to suppress.

schema_list_params

ClassVar[type[pydantic.BaseModel]]

The list params (filter, sort, page) as a Pydantic model, generated from schema_response by fr.query.derive_schema_list_params. Any route method that declares a list_params parameter is annotated with it, so a custom list route takes the same parameters as GET /.

The list-tuning attributes (pagination, extra_query_params) are tabulated under List Endpoint Behavior.

Current Request Values#

Current context covers declaration, FastAPI integration, and manual binding.

Symbol

Description

fr.ContextNamespace

Base for the application’s Current class. Declare each value as a member.

fr.ContextParam

A context value, declared as user_id: fr.ContextParam[int]. Read it with Current.user_id().

Current.depends(**sources)

Generate a FastAPI dependency that binds values from existing dependencies per request.

Current.bind(**values)

Bind values for a with block, restoring earlier values on exit.

Current.explain()

Show each member’s bound value and origin, or UNBOUND.

Clauses and Scopes#

Reusable query fragments and the visibility rules built from them. Query Clauses and Scopes own the topics; this table names the symbols.

Symbol

Description

fr.where_clause(condition)

Declare a predicate clause from a SQLAlchemy boolean expression, or from a function without parameters that returns one. Values come from context members.

fr.WhereClause

The type of every clause. Annotate a scope with it. Calling a clause returns its SQLAlchemy expression.

fr.all_of(*clauses) / fr.any_of / fr.none_of

Compose clauses: AND, OR, NOT. fr.clauses.UNSCOPED composes as no restriction; the rules per function are in Query Clauses.

fr.apply_clauses(stmt, *clauses)

Apply clauses to a plain SQLAlchemy statement. fr.clauses.UNSCOPED applies nothing.

fr.ClauseNamespace

Group a model’s clauses under one named class. Declaring model registers the namespace, and its default_scope is the clause every read and reference check on that model applies.

fr.clauses.UNSCOPED / fr.clauses.Unscoped

The explicit unscoped sentinel and its type: the one spelling for reading past a scope on a view, a namespace, a reference, or a single read.

fr.views.ReadScope

The per-read scope type the handlers take and forward: None resolves the view’s scope, a clause replaces it, fr.clauses.UNSCOPED reads unscoped. Annotate the scope parameter of a get_one / get_many override with it.

Advanced Object Helpers#

These helpers build, update, delete, and save ORM objects from schemas. Use them outside view instance methods: custom routes, services, workers, or tests. Sync and async variants are available from fastapi_restly.objects.

Symbol

Description

fr.objects.make_new_object(session, model_cls, schema_obj)

Build a new model_cls instance from schema_obj, existence-check any MustExist[...] FK ids and resolve any IDRef[...] / IDSchema[...] reference fields against the database, and add the object to session. It does not flush; call fr.objects.save_object(session, obj) afterwards to persist.

fr.objects.update_object(session, obj, schema_obj)

Apply the schema’s writable fields onto an existing ORM obj and resolve FK fields. It does not flush; call fr.objects.save_object(session, obj) afterwards to persist.

fr.objects.save_object(session, obj)

Flush the session and refresh obj so server-side defaults and generated columns (PKs, timestamps) are populated. Returns obj. This is where create/update writes hit the database.

fr.objects.delete_object(session, obj)

Delete obj and flush the session.

fr.objects.async_make_new_object(session, model_cls, schema_obj)

Async equivalent of fr.objects.make_new_object. Pass an AsyncSession.

fr.objects.async_update_object(session, obj, schema_obj)

Async equivalent of fr.objects.update_object.

fr.objects.async_save_object(session, obj)

Async equivalent of fr.objects.save_object.

fr.objects.async_delete_object(session, obj)

Async equivalent of fr.objects.delete_object.

The view methods of the same names (in the method surface) wrap these helpers, binding self.session, self.model, and self.schema; reach for the fr.objects forms in custom routes that touch a model other than self.model, and in services, workers, or tests.

Database#

These symbols cover connection configuration and session access:

Symbol

Description

fr.AsyncSessionDep

FastAPI Depends-compatible async session dependency.

fr.SessionDep

FastAPI Depends-compatible sync session dependency.

fr.open_async_session()

Open an async SQLAlchemy session context manager for use outside request handling, for example in background jobs or scripts.

fr.open_session()

Open a sync SQLAlchemy session context manager for use outside request handling, for example in background jobs or scripts.

fr.configure(async_database_url=..., ...)

Configure the framework. Accepts async/sync URLs, engines, session makers, custom session generators, a health= liveness route, and the warn_on_uncommitted / warn_on_misuse settings.

fr.db.get_async_engine()

Return the configured AsyncEngine instance.

fr.db.get_engine()

Return the configured sync Engine instance.

Restly has one public process-wide configuration, described further in Restly Runtime Configuration. Configure it once during application startup:

fr.configure(async_database_url="sqlite+aiosqlite:///app.db")

fr.configure(...) must receive at least one setup option: an app, database URL, engine, session maker, custom session generator, health=, or a warn_on_uncommitted / warn_on_misuse setting. A bare fr.configure() raises TypeError.

Pass warn_on_misuse=True to enable opt-in registration-time misuse warnings (fr.exc.RestlyMisuseWarning): include_view then flags endpoint method overrides, direct session.commit() calls in view methods, CRUD route sets hand-rolled on a bare View, and an IDRef / IDSchema field named like a scalar foreign-key column, each with the idiomatic fix named. It is off by default and intended for development, project templates, and CI.

For multiple databases, use FastAPI and SQLAlchemy directly: add a custom dependency on a view, or pass a custom session generator to fr.configure(...). Restly does not provide a public multi-context or multi-engine API. See Use a custom session dependency on one view.

Restly’s write handlers normally own the commit: each runs before_action_commit, then the commit, then after_action_commit around domain logic. Inside shared_write_action_commit, the outermost block owns that commit and the handlers return before it. Session dependencies do not commit on response; they roll back and close on exit.

A custom write route should use self.write_action(...) or reuse a handle_<verb>. To group their writes under one commit, wrap them in self.shared_write_action_commit(). See Commit several writes together.

Restly warns (RestlyUncommittedChangesWarning) when a request finishes with uncommitted session changes; this is the tell of a custom write route that forgot to commit. Fix the missing commit (write_action(...) or a handle_<verb>), or suppress a deliberate dry run with session.info["_fr_suppress_uncommitted"] = True. The global fr.configure(warn_on_uncommitted=False) opt-out exists but is rarely the right response to the warning.

Exceptions#

There are two families: configuration errors subclass RestlyError, and request-time HTTP errors subclass fastapi.HTTPException via RestlyHTTPError. Typed classes let you target Restly errors with app.add_exception_handler(...); recipes, the app-wide envelope pattern, and the 422-vs-400 boundary are in Shape Error Responses.

Symbol

Description

fr.exc.RestlyError

Base class for FastAPI-Restly framework (configuration-time) errors.

fr.exc.RestlyConfigurationError

Raised when a public Restly helper needs configuration that has not been set up yet, such as calling fr.open_session() before fr.configure(...).

fr.exc.RestlyHTTPError

Base for Restly’s request-time HTTP errors. Subclass of fastapi.HTTPException; each subclass sets a status code.

fr.exc.NotFound

HTTP 404. Raised by get_one when a row does not exist or is outside the scope; also raisable from authorize to hide a row’s existence.

fr.exc.Forbidden

HTTP 403. Raise from an authorize override to reject a verb.

fr.exc.Conflict

HTTP 409. For request conflicts with the current resource state.

fr.exc.BadQueryParam

HTTP 400. For an invalid filter/sort/pagination query parameter.

Testing#

The testing utilities provide a status-asserting client and savepoint-based isolation:

Symbol

Description

fastapi_restly.testing.configure_tests(...)

Add managed testing to an application that has already been configured for its test database. Accepts the app, models (base=), an optional schema step (create_all= or alembic_upgrade=), and cleanup policy, not database URLs, engines, or sessionmakers. db_cleanup= picks "rollback", "delete", or "none"; db_cleanup_exclude= spares seeded tables. A later database reconfiguration raises.

fastapi_restly.testing.RestlyTestClient

Sync test client wrapper around FastAPI’s TestClient with default status-code assertions. It can test async FastAPI routes and AsyncRestView endpoints.

fastapi_restly.testing.AsyncRestlyTestClient

Async HTTPX client with the same default status-code assertions. The restly_async_client fixture runs the application lifespan around it.

The pytest fixtures below are auto-loaded by the testing extra; their full behavior is documented in Testing:

Fixture

Scope

One-liner

restly_app

function

The app passed to configure_tests(app=...); a bare FastAPI() otherwise.

restly_client

function

RestlyTestClient wrapping restly_app, entered so the app’s lifespan runs.

restly_async_client

function

AsyncRestlyTestClient on the same event loop as restly_async_session and, in rollback mode, the same transaction; the app’s lifespan runs.

restly_session

function

The suite’s Session, following db_cleanup: savepoint-isolated under the default rollback mode, a plain committing session under "delete"/"none". Needs a sync sessionmaker. A custom sync_session_generator is bypassed under rollback (which raises if the sessionmaker is absent), rejected under delete, and left untouched under none (the fixture skips if the sessionmaker is absent).

restly_async_session

function

The async equivalent, following db_cleanup the same way. Needs an async sessionmaker; session_generator follows the same rollback/delete/none rules.

restly_project_root

function

Path of the nearest ancestor of the requesting test file with a pyproject.toml.

Default Exception Handling#

FastAPI-Restly installs a default handler for SQLAlchemy IntegrityError on FastAPI apps. The handler translates database integrity conflicts (unique constraint, foreign-key, not-null, and check-constraint violations) into HTTP 409 Conflict responses using FastAPI’s normal error body shape:

{
  "detail": "Unique constraint violated on user.email"
}

The exact detail text is best-effort and depends on the database driver. The handler recognizes common PostgreSQL SQLSTATE integrity codes and SQLite constraint messages; unknown dialects fall back to a generic conflict message. This mapping and the surrounding error-shaping recipes are also covered in Shape Error Responses.

Registration is automatic in either of these cases:

  • fr.configure(app=app, ...) is called with the default install_default_exception_handlers=True.

  • A view is registered directly on a FastAPI app with fr.include_view(app). This fallback covers apps that configure database sessions separately.

Restly skips this default only when the app already has a sqlalchemy.exc.IntegrityError handler. Generic handlers do not block registration.

To opt out, disable the default handlers in fr.configure:

fr.configure(
    app=app,
    async_database_url="sqlite+aiosqlite:///app.db",
    install_default_exception_handlers=False,
)

To use your own response format, register your IntegrityError handler before Restly installs its defaults:

from fastapi.responses import JSONResponse
from sqlalchemy.exc import IntegrityError

@app.exception_handler(IntegrityError)
async def integrity_error_handler(request, exc):
    return JSONResponse(
        status_code=409,
        content={"error": {"code": "constraint_conflict"}},
    )

fr.configure(app=app, async_database_url="sqlite+aiosqlite:///app.db")

Important Limitations and Capabilities#

The default CRUD contract has these boundaries:

  • Nested schemas are supported for responses and relation filtering, including nested aliases.

  • Full nested schemas are not supported for create/update payloads by the default CRUD flow; write payloads must map directly to model fields, or use model-aware reference fields such as *_id: fr.MustExist[int, Model] for FK columns and relationship fields typed as IDRef[Model] or IDSchema[Model].

  • Ordinary SQLAlchemy DeclarativeBase models work with CRUD views.

  • UUID and other non-int scalar primary keys are supported by the default routes, fr.MustExist[UUID, Model], IDRef[Model], and IDSchema[Model].

  • Composite primary keys are not supported by the default RestView / AsyncRestView CRUD routes; a custom route on the view addresses one with a predicate (handle_get_one(sa.and_(...))), or use fr.View for a hand-written route shape.