DEV Community

alphadev
alphadev

Posted on

Building a Generic CRUD Router for Automatic API Endpoints

👉 alphadev3296/fastapi-prototype-healtymeal-copilot-api

In the previous article, we built a Generic CRUD Base Class that abstracts the logic for Create, Read, Update, and Delete operations on MongoDB collections. This gave us a consistent, type-safe data layer with minimal code repetition.

Now, we’ll take it a step further — exposing these CRUD operations automatically as FastAPI routes.

By the end of this article, you’ll be able to add a fully functional REST API for any entity in your backend just by plugging in its schemas and CRUD class — no repetitive endpoint definitions required.


Motivation: The Problem with Hand-Written Routes

Even after creating a reusable CRUD layer, most projects still define routes manually:

router.post("/user")
router.get("/user/{user_id}")
router.put("/user/{user_id}")
router.delete("/user/{user_id}")
Enter fullscreen mode Exit fullscreen mode

If you have multiple models (User, Order, Product, Plan, Subscription, etc.), your API layer quickly fills up with identical methods that only differ by schema and collection names.

The ideal would be:

🪄 “Generate all CRUD endpoints automatically for any model class.”

That’s exactly what the GenericRouter pattern does.


Introducing the GenericRouter

The GenericRouter class is a meta-factory — it dynamically builds FastAPI routers for any generic CRUD class. It uses generics and Python descriptors to infer model types while keeping type safety.

Here’s the high-level concept:

  • Accept the CRUD class (which knows how to talk to the DB).
  • Accept Pydantic schemas — for input, reading, and updating.
  • Automatically construct REST endpoints (GET, POST, PUT, DELETE, and optionally SEARCH).
  • Return a ready-to-plug APIRouter instance.

Router Creation

class GenericRouter[
    CRUDClass: GenericCRUDBase,
    DBSchema: BaseModel,
    ReadSchema: BaseModel,
    UpdateSchema: BaseModel
]:
    @classmethod
    def create_crud_router(
        cls,
        name: str,
        crud: type[CRUDClass],
        db_schema: type[DBSchema],
        read_schema: type[ReadSchema],
        update_schema: type[UpdateSchema],
        filter_schema: Optional[type[Any]] = None
    ) -> APIRouter:
        router = APIRouter(tags=[f"CRUD: '{name}' Model"])
        ...
        return router
Enter fullscreen mode Exit fullscreen mode

The method dynamically registers route handlers for all standard CRUD operations.

Each endpoint injects a database instance and delegates to the GenericCRUDBase you built before.


Automatic Endpoints in Action

When you call GenericRouter.create_crud_router, it generates these routes automatically:

Method Route Description
GET / Get all records
GET /{obj_id} Get by ID
POST / Create new record
PUT /{obj_id} Update by ID
DELETE /{obj_id} Delete by ID
POST /search (Optional) Search by filter schema

Each route performs validation with your Pydantic schemas and provides automatic OpenAPI documentation — perfect for developers exploring your API through Swagger UI.


Example: Plugging in the MealPlan Model

Let’s revisit the MealPlanCRUD class from the previous article.

Assume you’ve already defined these schemas:

class MealPlanCreate(BaseModel): ...
class MealPlanRead(BaseModel): ...
class MealPlanUpdate(BaseModel): ...
Enter fullscreen mode Exit fullscreen mode

and a CRUD layer like:

class MealPlanCRUD(TimeStampedCRUDBase[MealPlanCreate, MealPlanRead, MealPlanUpdate]):
    def __init__(self, db: Database):
        super().__init__(db, "meal_plan", MealPlanRead)
Enter fullscreen mode Exit fullscreen mode

Registering an API for it now becomes trivial:

meal_plan_router = GenericRouter[
    MealPlanCRUD,
    MealPlanCreate,
    MealPlanRead,
    MealPlanUpdate
].create_crud_router(
    name="meal_plan",
    crud=MealPlanCRUD,
    db_schema=MealPlanCreate,
    read_schema=MealPlanRead,
    update_schema=MealPlanUpdate,
)
Enter fullscreen mode Exit fullscreen mode

Then, simply include it in your FastAPI app:

app.include_router(meal_plan_router, prefix="/meal-plans")
Enter fullscreen mode Exit fullscreen mode

That one line gives you a complete REST API for MealPlan:

  • GET /meal-plans
  • GET /meal-plans/{id}
  • POST /meal-plans
  • PUT /meal-plans/{id}
  • DELETE /meal-plans/{id} …and any extra /search filters if you define a FilterSchema.

Optional Filtering Support

Some collections need complex searches: find all users by role, filter meals under 500 calories, etc.

For that, you can pass a filter_schema argument — a Pydantic model that builds Mongo-compatible queries.

Example:

class MealPlanFilter(BaseModel):
    min_calories: int | None = None
    max_calories: int | None = None

    def query(self) -> dict:
        q = {}
        if self.min_calories:
            q["total_calories"] = {"$gte": self.min_calories}
        if self.max_calories:
            q.setdefault("total_calories", {}).update({"$lte": self.max_calories})
        return q
Enter fullscreen mode Exit fullscreen mode

Now, just register it:

GenericRouter.create_crud_router(
    name="meal_plan",
    crud=MealPlanCRUD,
    db_schema=MealPlanCreate,
    read_schema=MealPlanRead,
    update_schema=MealPlanUpdate,
    filter_schema=MealPlanFilter,
)
Enter fullscreen mode Exit fullscreen mode

You’ll instantly have a /meal-plans/search endpoint accepting complex query bodies.


Why It Matters

This architecture closes the loop:

  1. Generic CRUD Base — encapsulates persistence logic.
  2. Generic Router — converts business operations into ready-to-serve HTTP endpoints.
  3. Schemas — enforce data validation and structure.

With this trio, adding a new resource to your API becomes effortless:

1️⃣ Define schemas
2️⃣ Implement CRUD subclass
3️⃣ Register GenericRouter
Enter fullscreen mode Exit fullscreen mode

No repeated boilerplate, no missed edge cases, fully documented API.


Benefits Recap

Code Reuse: Define once, apply everywhere.

Consistency: All endpoints behave uniformly.

Type Safety: Pydantic schemas ensure valid data flow.

Maintainability: Small global changes propagate instantly.

Speed: Add new models in minutes, not hours.


When to Use

This pattern fits best when:

  • You maintain multiple similar models.
  • Your API routes follow standard CRUD patterns.
  • You need consistent OpenAPI documentation.
  • You want to minimize manual endpoint registration.

If your models include custom behaviors, you can subclass and override any route before registration — flexibility without losing structure.


Wrapping Up

With the GenericRouter, your backend becomes truly modular:

  • GenericCRUDBase handles persistence logic.
  • GenericRouter auto-generates complete REST APIs.
  • FastAPI auto-documents everything out of the box.

You’ve effectively built a mini-framework layer inside FastAPI — one that scales cleanly as your project grows.


Next Steps

In the next article, we’ll explore integrating role-based access control (RBAC) into these generic routes — enforcing permissions dynamically while keeping code DRY and consistent.


Full Source Code:

👉 alphadev3296/fastapi-prototype-healtymeal-copilot-api

Top comments (0)