Backends¶
All backends expose the same routes and the same query parameters. They differ
in what Meta requires, how schemas are generated, and what a delete returns.
Common options¶
| Option | Default | Meaning |
|---|---|---|
name |
table, collection, or class name | URL segment and OpenAPI tag |
limit |
0 |
Default page size; 0 disables pagination |
limit_min |
1 |
Smallest limit; 0 allows limit=0 (all rows) |
limit_max |
limit |
Largest accepted limit |
limit_total |
True |
Count rows for the x-total header |
filters |
() |
Allowed where fields |
sorting |
() |
Allowed sort fields |
Schema |
generated | Marshmallow schema |
schema_base |
backend default | Base class for the generated schema |
schema_fields |
{} |
Extra fields for the generated schema |
schema_meta |
{} |
Extra Meta options for the generated schema |
schema_unknown |
EXCLUDE |
Marshmallow unknown policy |
Meta options are inherited. A subclass can override only what it needs:
class BaseResource(PWRESTHandler):
class Meta:
limit = 20
sorting = ("id", {"default": "desc"})
class Pets(BaseResource):
class Meta:
model = Pet
filters = ("status",)
Peewee¶
from muffin_rest.peewee import PWRESTHandler
@api.route
class Pets(PWRESTHandler):
class Meta:
model = Pet
manager = db.manager # optional
model_pk = Pet.id # optional
delete_recursive = False
| Option | Required | Meaning |
|---|---|---|
model |
yes | peewee model, preferably peewee_aio |
manager |
no | peewee_aio.Manager; defaults to model._manager |
model_pk |
no | Primary key field; defaults to model._meta.primary_key |
delete_recursive |
no | Passed to delete_instance(recursive=...) |
The model needs a manager. With muffin_peewee, db.Model and db.register
provide one. A plain peewee.Model works when you pass Meta.manager.
The generated schema uses marshmallow_peewee.ModelSchema, so fields, nullability,
and foreign keys come from the model. Enums built with muffin_peewee fields are
mapped to muffin_rest.schemas.EnumField, and foreign keys are exposed as
integers.
A single resource is looked up with the primary key's Python converter, so
string route params become the column type. An unknown id returns 404; a value
that cannot be converted to the key type returns 400.
Delete returns the deleted id for a detail request and a list of ids for a
batch request. Peewee sorters put NULL values last.
Composite primary keys¶
A model with peewee.CompositeKey is addressed by a single string id built from
the key columns with :: as the separator:
GET /api/pet/acme::7
The detail lookup splits the id and compares each column. A wrong number of
parts returns 400. The generated schema exposes the key columns separately
(tenant, seq). To expose one id field, add CompositePKField to a custom
schema:
import marshmallow as ma
from muffin_rest.peewee.schemas import CompositePKField
class PetSchema(ma.Schema):
id = CompositePKField(Pet)
SQLAlchemy Core¶
from muffin_rest.sqlalchemy import SARESTHandler
@api.route
class Pets(SARESTHandler):
class Meta:
table = pet_table
database = db
table_pk = pet_table.c.id # optional
| Option | Required | Meaning |
|---|---|---|
table |
yes | sqlalchemy.Table |
database |
yes | muffin_databases.Plugin |
table_pk |
no | Primary key column; defaults to table.c.id |
The generated schema extends SQLAlchemyAutoSchema. It includes foreign keys,
marks the primary key as dump_only, and fills column defaults on create. This
matters because SQLAlchemy Core does not apply Python-side defaults on insert;
the schema does it before the query is executed.
Updates merge the validated body into the loaded row, so PUT is a partial
update. Delete returns null for both detail and batch requests.
MongoDB¶
from muffin_rest.mongo import MongoRESTHandler
@api.route
class Pets(MongoRESTHandler):
class Meta:
collection = mongo_collection
collection_id = "_id" # optional
aggregate = None # optional pipeline
| Option | Required | Meaning |
|---|---|---|
collection |
yes | Motor collection |
collection_id |
no | Id field, _id by default |
aggregate |
no | Base aggregation pipeline for list requests |
The generated schema extends MongoSchema. It defines _id as an ObjectId
field but no other fields, so declare them yourself:
class PetSchema(MongoSchema):
name = ma.fields.String(required=True)
status = ma.fields.String()
or use Meta.schema_fields:
class Meta:
collection = pets
schema_fields = {
"name": ma.fields.String(required=True),
"status": ma.fields.String(),
}
MongoSchema merges validated data into the loaded document on update, which is
why PUT behaves as a partial update.
Detail ids are parsed with bson.ObjectId. An invalid id returns 404. Delete
returns null, including batch deletes.
Collections are wrapped in MongoChain, a small lazy wrapper that accumulates
filters and sorting and builds a Motor cursor on demand. Filters are combined
with $and, sorting is appended, and to_list() materializes the result.
Aggregation¶
Meta.aggregate is used for list requests when pagination is enabled
(Meta.limit > 0). Pagination appends $skip and $limit, and the total count
is computed with a second pipeline. Request filters are merged into the first
$match stage, or inserted at the front when there is none. The configured
pipeline is not modified between requests.
With Meta.limit = 0, list requests use a plain find and the pipeline is
ignored.
With an aggregation pipeline, the total is always counted; limit_total only
controls the count for plain find queries.
Mongo filters¶
The Mongo filter class supports only the $-prefixed operators listed in
Querying. Short aliases such as == or > and the logic
operators $or, $and, $not, $nor are not available.
$starts and $ends build a regex from the user value and escape it, so dots
and other regex characters are literal.
To filter by _id, expose it with a schema field so the string is converted to
an ObjectId:
class Meta:
collection = pets
filters = (("_id", {"field": "_id"}),)
Delete behavior¶
| Backend | Detail delete returns | Batch delete returns |
|---|---|---|
| Peewee | the deleted id | array of deleted ids |
| SQLAlchemy | null |
null |
| Mongo | null |
null |
Batch delete always uses the collection route and a JSON array body. Peewee
returns 404 when no id matches; SQLAlchemy and Mongo return 200 even when
nothing was deleted.
Choosing a backend¶
Use Peewee when you already have Peewee models and want model-driven schemas.
Use SQLAlchemy Core when your tables are defined with sqlalchemy.Table and you
use muffin_databases. Use Mongo when the data is document-shaped or when you
need an aggregation pipeline before pagination.
The handler API is the same, so the choice mostly affects schema generation and the query objects you see in hooks.
If your storage is not listed here, Writing a backend walks through the five methods a new backend has to implement.