Khi nhiều endpoint cùng dùng bộ tham số q, skip, limit, đừng lặp lại chúng ở từng hàm. Hãy gom chúng vào một class dependency: FastAPI sẽ tự tạo instance của class đó từ query param mỗi request, và mọi endpoint chỉ cần khai báo Depends(CommonQueryParams). Cách này áp dụng khi bạn có từ hai endpoint trở lên chia sẻ cùng bộ tham số phân trang hoặc lọc.

Vấn đề

Một API danh sách thường lặp lại cùng ba tham số ở mọi endpoint: từ khóa tìm kiếm, vị trí bắt đầu và số lượng trả về. Viết tay ba tham số này trong từng hàm vừa dài dòng, vừa dễ lệch nhau: endpoint này đặt limit mặc định 100, endpoint kia đặt 20, và khi muốn thêm một tham số mới, bạn phải sửa từng chỗ một. Lâu dần, bộ tham số của các endpoint không còn nhất quán, tài liệu OpenAPI cũng khó đọc hơn.

@app.get("/items/")
def read_items(q: str | None = None, skip: int = 0, limit: int = 100):
...
@app.get("/users/")
def read_users(q: str | None = None, skip: int = 0, limit: int = 100):
...

Giải pháp

FastAPI chấp nhận bất kỳ “callable” nào làm dependency, và một class chính là một callable: khi gọi class, Python chạy phương thức __init__. Vì vậy bạn có thể khai báo các tham số ngay trong __init__, FastAPI sẽ đọc chúng như query param bình thường và đưa instance đã tạo sẵn vào endpoint.

from fastapi import Depends, FastAPI, Query
class CommonQueryParams:
def __init__(
self,
q: str | None = None,
skip: int = 0,
limit: int = Query(default=100, le=100),
):
self.q = q
self.skip = skip
self.limit = limit
app = FastAPI()
ITEMS = [{"name": "ao"}, {"name": "quan"}, {"name": "mu"}, {"name": "giay"}]
@app.get("/items/")
def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
result = ITEMS[commons.skip : commons.skip + commons.limit]
if commons.q:
result = [item for item in result if commons.q in item["name"]]
return {"items": result}

Cách này tốt hơn ba điểm. Thứ nhất, bộ tham số chỉ định nghĩa một lần nên không còn chuyện lệch mặc định giữa các endpoint. Thứ hai, endpoint đọc commons.skip hay commons.limit rõ nghĩa hơn nhiều so với việc moi từng khóa từ một dict. Thứ ba, mọi validation quen thuộc vẫn hoạt động: Query(default=100, le=100) trong __init__ vẫn khiến limit vượt 100 trả về lỗi 422 trước khi code của bạn chạy. Nếu dự án dùng cú pháp Annotated, bạn viết Annotated[CommonQueryParams, Depends(CommonQueryParams)] với kết quả tương tự.

Áp dụng vào dự án của bạn

Hãy đặt class này vào một module riêng, ví dụ dependencies.py, rồi import dùng chung cho mọi router cần phân trang và lọc. Khi cần một biến thể khác, ví dụ trang quản trị cho phép limit tới 500, đừng sửa class chung làm ảnh hưởng endpoint cũ: tạo một class con hoặc một class riêng kế thừa logic đó. Giữ class dependency gọn nhẹ, chỉ lo gom tham số và validation; logic nghiệp vụ vẫn nằm trong endpoint hoặc service, như vậy class mới thật sự tái sử dụng được lâu dài.

Bài viết liên quan

Câu hỏi thường gặp

Class dependency khác function dependency ở điểm nào?

Về bản chất không khác: FastAPI chỉ cần một callable để gọi mỗi request. Function trả về dict hoặc object, class trả về instance của chính nó. Class thắng ở chỗ gom được tham số thành thuộc tính có tên rõ ràng và dễ mở rộng bằng kế thừa.

Mỗi request có được tạo instance mới không?

Có. FastAPI tạo một instance mới của class cho mỗi request, tương tự như cách nó gọi lại function dependency mỗi request. Vì vậy đừng dùng thuộc tính của instance để lưu trạng thái dùng chung giữa các request.

Có dùng được cú pháp Annotated không?

Có. Bạn viết commons: Annotated[CommonQueryParams, Depends(CommonQueryParams)] và nhận đúng instance như cách viết mặc định. Tài liệu chính thức của FastAPI khuyên dùng dạng Annotated khi có thể.

Validation như Query() có chạy trong __init__ không?

Có. Các tham số của __init__ được FastAPI xử lý giống hệt tham số của function dependency, nên Query, Header hay Path đặt trong đó đều validate và trả lỗi 422 như bình thường.

💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.

Nguồn tham khảo