Skip to content

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:

adapter = SQLModelAdapter(model=Category, get_session=get_session)

Disable endpoints

Drop the routes you don't want to expose.

crud = CRUDCollection(
    orm_adapter=adapter,
    disable_delete=True,
    disable_create=True,
)

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.