DEV Community

bigrivi
bigrivi

Posted on

fastapi-crudrouter is Dead. Here's How to Migrate to BetterCRUD

fastapi-crudrouter is Dead. Here's How to Migrate to BetterCRUD

fastapi-crudrouter was the de-facto CRUD library for FastAPI — generating 6 CRUD routes from a model in a few lines. It's been unmaintained since November 2023. No FastAPI 0.141+ support, no new features, and its SQLAlchemy backend doesn't keep up with SQLAlchemy 2.0 async best practices.

If you're still on it, it's time to migrate. The good news: the route layout is nearly identical, so the move is mostly drop-in.

Why migrate?

fastapi-crudrouter BetterCRUD
Maintained (2026) ❌ stalled since Nov 2023 ✅ active
FastAPI 0.141+
SQLAlchemy 2.0 async ✅ (basic)
Filter operators ✅ 27 operators
Pagination modes ❌ basic only ✅ always/optional/disabled
Relationship queries & storage ✅ joins, M2M, O2M, O2O
Soft delete + recover
ACL hooks
Lifecycle hooks
Custom endpoints @crud_action
Test coverage ✅ 99%+, 177 tests

The migration

Before (fastapi-crudrouter)

from fastapi_crudrouter import SQLAlchemyCRUDRouter
from database import get_session
from models import Pet, PetCreate, PetUpdate

router = SQLAlchemyCRUDRouter(
    schema=PetUpdate,
    create_schema=PetCreate,
    update_schema=PetUpdate,
    db_model=Pet,
    db=get_session,
    prefix="pet",
)
Enter fullscreen mode Exit fullscreen mode

After (BetterCRUD)

from fastapi import APIRouter, Depends
from better_crud import crud
from better_crud.service.sqlalchemy import SqlalchemyCrudService
from models import Pet, PetCreate, PetUpdate, PetPublic

pet_router = APIRouter()

class PetService(SqlalchemyCrudService[Pet]):
    def __init__(self):
        super().__init__(Pet)

@crud(
    pet_router,
    dto={"create": PetCreate, "update": PetUpdate},
    serialize={"base": PetPublic},
)
class PetController():
    service: PetService = Depends(PetService)
Enter fullscreen mode Exit fullscreen mode

That's the whole migration. Register pet_router with app.include_router(pet_router, prefix="/pet") and you're live.

What you get for free after migrating

1. Filtering you actually need

fastapi-crudrouter had no filtering. BetterCRUD's GET /pet supports 27 operators out of the box:

GET /pet?filter=name||$cont||Re        # contains
GET /pet?filter=age||$between||1,5     # range
GET /pet?filter=species||$in||dog,cat  # in list
GET /pet?s={"$or":[{"age":{"$gt":3}},{"species":{"$eq":"cat"}}]}  # nested logic
Enter fullscreen mode Exit fullscreen mode

2. Pagination that fits your API

BetterCrudGlobalConfig.init(pagination_mode="optional")
Enter fullscreen mode Exit fullscreen mode

Three modes — always, optional, disabled — so small reference datasets can return plain arrays while large tables stay paginated.

3. Relationships, done properly

Store nested payloads (many-to-many, one-to-many, one-to-one) automatically, and query them with ?load= / ?join=:

class UserCreate(UserBase):
    profile: Optional[UserProfileCreate] = None
    roles: Optional[List[int]] = None
    tasks: Optional[List[UserTaskCreate]] = None
Enter fullscreen mode Exit fullscreen mode

4. Soft delete + recover

@crud(router, query={"soft_delete": True, "allow_recover": True})
Enter fullscreen mode Exit fullscreen mode

DELETE becomes soft delete; PATCH /pet/{id}/recover restores records.

5. Security hooks

Every generated route exposes feature/action on the request state — wire your existing permission guards directly:

from better_crud import get_feature, get_action

async def acl(request: Request):
    feature = get_feature(request)
    action = get_action(request)
    # your ACL logic
Enter fullscreen mode Exit fullscreen mode

6. Business logic without leaving the CRUD pattern

@crud_action(method="POST", path="/{id}/adopt", action="adopt")
async def adopt(self, id: int):
    return await self.service.do_adopt(id)
Enter fullscreen mode Exit fullscreen mode

Registers POST /pet/{id}/adopt with service injection, ACL, and response wrapping — no manual router wiring.

Migration checklist

  1. ✅ Add better_crud to requirements (remove fastapi-crudrouter)
  2. ✅ Add a PetService(SqlalchemyCrudService[Pet]) class (thin, no logic needed initially)
  3. ✅ Replace the SQLAlchemyCRUDRouter(...) call with @crud(...) on a controller class
  4. ✅ Register the router — prefix stays the same, routes are identical
  5. ✅ Point your frontend at the same endpoints — no client changes needed
  6. ✅ Optionally enable soft delete, ACL, and custom actions as you go

The route layout (GET/POST /resource, GET/PUT/DELETE /resource/{id}) is preserved, so your existing clients keep working while the backend gains capabilities fastapi-crudrouter never had.

Resources


Migrating? If BetterCRUD saves you time, give it a ⭐ on GitHub.

Top comments (0)