👉 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}")
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 optionallySEARCH). - Return a ready-to-plug
APIRouterinstance.
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
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): ...
and a CRUD layer like:
class MealPlanCRUD(TimeStampedCRUDBase[MealPlanCreate, MealPlanRead, MealPlanUpdate]):
def __init__(self, db: Database):
super().__init__(db, "meal_plan", MealPlanRead)
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,
)
Then, simply include it in your FastAPI app:
app.include_router(meal_plan_router, prefix="/meal-plans")
That one line gives you a complete REST API for MealPlan:
GET /meal-plansGET /meal-plans/{id}POST /meal-plansPUT /meal-plans/{id}-
DELETE /meal-plans/{id}…and any extra/searchfilters if you define aFilterSchema.
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
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,
)
You’ll instantly have a /meal-plans/search endpoint accepting complex query bodies.
Why It Matters
This architecture closes the loop:
- Generic CRUD Base — encapsulates persistence logic.
- Generic Router — converts business operations into ready-to-serve HTTP endpoints.
- 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
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)