Skip to content

Release Notes

v0.2.2

BetterCRUD v0.2.2 adds custom endpoints via a new crud_action decorator, makes bulk operations atomic, and completes the soft-delete story with an opt-in recover route — all fully tested (177 tests).

🚀 Highlights

  • crud_action decorator — attach business endpoints (adopt, approve, reset, ...) to your generated routes while keeping them inside the CRUD ecosystem: service injection, ACL action names, response schema wrapping, and request.state scoping all work automatically.
  • Atomic bulk operationscrud_create_many / crud_update_many now run in a single transaction. A mid-batch failure rolls back every item instead of leaving partial data.
  • Soft-delete recover routePATCH /{resource}/{id}/recover restores soft-deleted records. Opt-in via query={"soft_delete": True, "allow_recover": True}; routes are not generated otherwise (zero footprint).

✨ Enhancements

crud_action custom endpoints

from better_crud import crud, crud_action

@crud(pet_router, serialize={"base": PetPublic})
class PetController():
    service: PetService = Depends(PetService)

    @crud_action(method="POST", path="/{id}/adopt", action="adopt")
    async def adopt(self, id: int):
        return {"id": id, "adopted": True}

See Custom Actions for the full guide.

Atomic bulk operations

crud_create_many and crud_update_many now wrap the batch in a single transaction. On any failure, all changes are rolled back — no partial writes.

Recover route

@crud(
    router,
    serialize={"base": UserPublic},
    query={
        "soft_delete": True,
        "allow_recover": True,     # exposes PATCH /{id}/recover
    }
)

🐛 Bug Fixes

  • Bulk create_many/update_many were non-atomic (per-item commits) — a mid-batch failure left partial data. Now fully transactional.

📦 Installation

pip install better-crud==0.2.2

For the complete history, see the Changelog.


v0.2.1

BetterCRUD v0.2.1 adds optional pagination to the get_many route. List endpoints can now return a full array (no pagination) when the caller omits pagination params — ideal for small datasets like enum-like tables, reference data, and dropdown options.

🚀 Highlights

  • Optional paginationGET /{resource} now supports three modes via pagination_mode, configurable globally or per-route.
  • Backward compatible by default — the default "optional" mode matches previous behavior exactly (no pagination params → full array; page/size → paginated response). All 165 tests pass.

✨ Enhancements

pagination_mode global config

BetterCrudGlobalConfig.init() accepts a new pagination_mode option:

BetterCrudGlobalConfig.init(
    backend_config={"sqlalchemy": {"db_session": get_session}},
    pagination_mode="optional",   # "always" | "optional" | "disabled"
)
Mode No page/size ?page=1&size=20
always Paginated (default page=1, size=50) Paginated
optional (default) Full array Paginated
disabled Full array Full array (params ignored)

Per-route override

Individual @crud decorators can override the global mode:

@crud(
    router,
    serialize={"base": PetPublic},
    pagination_mode="always",
)
class PetController():
    service: PetService = Depends(PetService)

Response format

  • Paginated: {items: [...], total, page, size, pages}
  • Non-paginated: [...] — plain array of all matching records

filter/s/sort work identically in both modes. Frontends should check whether the response is an array or an object:

const data = await res.json();
const items = Array.isArray(data) ? data : data.items;

📦 Installation

pip install better-crud==0.2.1

For the complete history, see the Changelog.


v0.2.0

BetterCRUD v0.2.0 brings full compatibility with the latest FastAPI releases, more powerful lifecycle hooks, and a cleaner dependency footprint — all backed by a fully passing test suite (154 tests).

🚀 Highlights

  • FastAPI 0.141+ compatibility — fixes route registration failures caused by pydantic serializing Depends objects into plain dicts. Routes now register correctly across FastAPI >=0.111.0,<1.0.
  • Richer lifecycle hookson_after_create and on_after_update now receive the validated model payload, not just the persisted entity, giving you full access to the input data in your hook logic.
  • $notany filter now supports empty lists, making the operator consistent with $in/$nin and simpler to use from dynamic query builders.

✨ Enhancements

Lifecycle hooks receive the input model

on_after_create / on_after_update gained a new model parameter carrying the original validated create/update schema:

async def on_after_create(
    self,
    entity: Entity,            # persisted instance
    model: EntityCreate,       # NEW: validated create payload
    background_tasks: BackgroundTasks,
) -> None:
    ...

Warning

This is a breaking change for anyone overriding these two hooks — update your signatures to accept the new model argument.

$notany supports empty list

query = {"filter": {"$notany": {"tags": []}}}   # previously an error, now valid

🐛 Bug Fixes

  • FastAPI >=0.141 route registrationDepends instances restored after pydantic model_dump() serialization so routes.dependencies attach correctly.
  • $notany operator — no longer fails when given an empty list.

🔧 Dependency & Compatibility

  • FastAPI: >=0.111.0,<1.0 (previously pinned versions could not resolve from older PyPI mirrors)
  • SQLAlchemy: >=2.0.30,<3.0
  • fastapi-pagination: >=0.12.24,<1.0
  • Pydantic: >=2.7.3,<3.0
  • Development requirements locked to versions reachable from all PyPI mirrors (fastapi 0.128.8, sqlalchemy 2.0.30, pydantic 2.7.3), with greenlet and bcrypt<5 added for SQLAlchemy async and passlib compatibility.

✅ Verified Combinations

All tested with the full suite — 154 passed:

FastAPI SQLAlchemy fastapi-pagination Pydantic SQLModel
0.111.0 2.0.30 0.12.24 2.7.3 0.0.22
0.128.8 2.0.30 0.12.24 2.7.3 0.0.22
0.128.8 2.0.51 0.15.16 2.13.4
0.135.1 2.0.48 0.12.24 2.13.4 0.0.14
0.140.13 2.0.51 0.15.16 2.13.4
0.141.1 2.0.51 0.15.16 2.13.4

📦 Installation

pip install better-crud==0.2.0

For the complete history, see the Changelog.