FastAPI tự chuyển tham số đường dẫn sang đúng kiểu, nhưng chỉ kiểm tra kiểu thì chưa đủ: /items/0 hay /items/-3 vẫn lọt qua dù ID trong database không bao giờ âm hay bằng 0. Dùng Path() với ge, le hoặc pattern, FastAPI sẽ chặn giá trị sai bằng lỗi 422 ngay từ cổng API, trước khi code của bạn chạm tới database. Cách này áp dụng cho mọi endpoint có tham số đường dẫn dạng ID hoặc username.
Vấn đề
Giả sử bạn có endpoint lấy sản phẩm theo ID, chỉ khai báo kiểu int:
from fastapi import FastAPIapp = FastAPI()@app.get("/items/{item_id}")def read_item(item_id: int): return {"item_id": item_id}
Kiểu int chỉ đảm bảo giá trị parse được thành số nguyên: /items/abc bị chặn bằng 422, nhưng /items/0 và /items/-3 vẫn chạy ngon lành vào hàm xử lý. Từ đó code phải tự kiểm tra, hoặc tệ hơn là mang giá trị vô nghĩa xuống database rồi mới nhận lại 404. Lỗi nằm ở chỗ cổng API chưa đặt ra “luật chơi” cho tham số của chính nó.
Giải pháp
Khai báo ràng buộc ngay trên tham số bằng Path() và Annotated:
from typing import Annotatedfrom fastapi import FastAPI, Pathapp = FastAPI()@app.get("/items/{item_id}")def read_item( item_id: Annotated[int, Path(ge=1, le=100000, title="ID sản phẩm")],): return {"item_id": item_id}
Giờ /items/5 trả về 200 như bình thường, còn /items/0 nhận lỗi 422 với chi tiết loc là ["path", "item_id"] và thông báo giá trị phải lớn hơn hoặc bằng 1. Việc kiểm tra chạy trước hàm xử lý, nên database không bao giờ thấy ID sai. Tham số title còn hiện trong tài liệu OpenAPI tại /docs.
Cùng cơ chế này áp dụng cho chuỗi, ví dụ username chỉ gồm chữ thường, số và gạch dưới:
@app.get("/users/{username}")def read_user( username: Annotated[str, Path(min_length=3, max_length=20, pattern="^[a-z0-9_]+$")],): return {"username": username}
Với đoạn này, /users/ab bị chặn vì quá ngắn, /users/Nguyen_Van bị chặn vì sai mẫu, còn /users/nguyen_van đi qua bình thường.
Áp dụng vào dự án của bạn
Hãy gắn Path() cho mọi tham số đường dẫn dạng ID ngay khi viết route. Chọn ngưỡng ge/le theo miền dữ liệu thật, đừng copy mù con số 1–100000 trong ví dụ. Path() chỉ kiểm tra hình thức và khoảng giá trị; việc ID có tồn tại vẫn thuộc về tầng database, nơi bạn trả 404 khi không tìm thấy bản ghi.
Bài viết liên quan
- FastAPI: gom cả chục query param vào một Pydantic model — validate tập trung trong 5 phút
- FastAPI: validate dữ liệu đầu vào chuẩn chỉnh bằng Pydantic Field và validator tùy chỉnh
- FastAPI: bảo vệ route bằng dependency JWT/OAuth2 tái sử dụng, không lặp code xác thực
Câu hỏi thường gặp
Path() khác Query() ở điểm nào?
Path() dùng cho tham số nằm trong đường dẫn như /items/{item_id}, còn Query() dùng cho tham số sau dấu ? như /items?skip=0. Cả hai nhận cùng bộ ràng buộc ge, le, min_length, pattern nên bạn không phải học hai cách khác nhau.
Không dùng Annotated thì viết Path() thế nào?
Vẫn được: item_id: int = Path(ge=1) là cách viết cũ và còn hoạt động. Cách Annotated được khuyến nghị vì nó tách rõ kiểu dữ liệu và metadata, đồng thời tương thích tốt hơn với các công cụ kiểm tra kiểu.
Path() có thay được việc kiểm tra bản ghi tồn tại không?
Không. Path() chỉ biết giá trị có hợp lệ về hình thức hay không, ví dụ ID bằng 5 là số hợp lệ, nhưng không biết ID 5 có trong database hay không. Hãy giữ bước truy vấn và trả 404 như bình thường sau lớp validate này.
Chọn pattern cho chuỗi cần lưu ý gì?
pattern là biểu thức chính quy Python, neo đầu và cuối bằng ^ và $ nếu muốn khớp toàn bộ chuỗi. Hãy thử pattern bằng TestClient trước khi lên production, vì mẫu quá chặt sẽ chặn cả username hợp lệ.
💬 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
- FastAPI Documentation — Path Parameters and Numeric Validations: https://fastapi.tiangolo.com/tutorial/path-params-numeric-validations/
