Cookbook
Task-oriented recipes. Every one is exercised by a test in the suite — the "Tested by" line points at it, so the code here is known to run (and runs against every configured ORM backend).
Examples assume you already have an adapter:
Disable endpoints
Drop the routes you don't want to expose.
The path stays registered for its remaining methods, so a call to a
disabled one returns 405, not 404.
Tested by tests/integration/test_disable.py.
Expose only some fields
Restrict which model fields feed the generated schemas instead of writing a schema by hand.
crud = CRUDCollection(
orm_adapter=adapter,
public_fields={"id", "title", "summary"}, # what GET returns
create_fields={"title", "content"}, # what POST accepts
update_fields={"title", "content", "summary"},
)
The same sets can live on the model as a crud_config class variable.
Tested by tests/test_sqlmodel_schema_generation.py.
Use a custom primary-key path
Supply your own pk_fields model to route on a non-id column.
class SlugPK(BaseModel):
slug: str
crud = CRUDCollection(orm_adapter=adapter, pk_fields=SlugPK)
# -> GET /{slug}
Tested by tests/test_sqlmodel_crud_collection_pk.py.
Nested collections
Mount one collection under another; the child is automatically scoped to its parent by the foreign key, and the parent id is taken from the URL, not the request body.
thread_crud = CRUDCollection(orm_adapter=make_adapter(Thread))
post_crud = CRUDCollection(orm_adapter=make_adapter(Post))
thread_crud.add_nested_collection("/posts", post_crud)
app.include_router(thread_crud.get_router(prefix="/threads"))
# -> /threads/{thread_id}/posts, /threads/{thread_id}/posts/{post_id}
Nesting works several levels deep, in either FK direction, and with composite keys.
Tested by tests/integration/test_nested.py, test_root_posts.py,
test_composite.py.
A /me singleton collection
Serve /users/me — where the record is identified by the auth token,
not a path id. Combine two overrides: replace the PK dependency with one
that reads the current user, and pin the path segment to /me.
class UserPK(BaseModel):
id: int
async def get_me() -> UserPK:
return UserPK(id=current_user_id()) # from your auth
class UserMeCRUD(CRUDCollection):
dependency_overrides = {
PKFieldsDependency: Annotated[UserPK, Depends(get_me)],
}
def get_id_path(self, exclude=frozenset()) -> str:
return "/me"
user_me_crud = UserMeCRUD(orm_adapter=make_adapter(User))
user_me_crud.add_nested_collection("/posts", CRUDCollection(orm_adapter=make_adapter(Post)))
app.include_router(user_me_crud.get_router(prefix="/users"))
# -> /users/me, /users/me/posts (no {user_id} anywhere)
Tested by tests/integration/test_user_posts.py.
Return a fallback instead of 404
Override get_one_not_found to return a value when the row is missing.
The same hook is how you create-on-first-access.
class WithFallback(CRUDCollection):
orm_adapter = adapter
async def get_one_not_found(self, pk_values, include_data):
return self.public_schema(id=0, name="fallback")
# GET a missing id -> 200 with the fallback body
Tested by tests/integration/test_override_hooks.py.
Pre-process input in a handler
Override a route handler to run logic around the adapter call. Keep the marker annotations — they are what turns the parameters into real request inputs; drop them and the request 422s.
class PrefixName(CRUDCollection):
orm_adapter = adapter
async def create_one_handler(
self,
create_data: Annotated[BaseModel, CreateSchemaDependency],
parent_refs: Annotated[list, ParentPKFieldsDependency],
):
create_data.name = f"PFX-{create_data.name}"
return await super().create_one_handler(create_data, parent_refs)
Tested by tests/integration/test_override_hooks.py.
Restrict which relations can be included
Pass an include_schema that only allows chosen relations; anything else
is rejected with 422.
class PostIncludeThreadOnly(BaseModel):
include: list[Literal["thread"]] | None = None
crud = CRUDCollection(orm_adapter=adapter, include_schema=PostIncludeThreadOnly)
# ?include=thread -> 200 ; ?include=posts -> 422
Tested by tests/integration/test_include.py.
Load several relations at once
Repeat the include query parameter — one entry per relation. A
comma-joined value is not supported and returns 422.
GET /threads/{id}?include=category&include=posts # both loaded
GET /threads/{id}?include=category,posts # 422
Tested by tests/integration/test_include_multi.py.