Viết API đầu tiên với FastAPI: quản lý sản phẩm, kiểm dữ liệu và tài liệu tự sinh

Mỗi nơi giữ một bản tồn kho nên luôn có bản sai. Viết một API nhỏ bằng FastAPI, kiểm dữ liệu với Pydantic, thấy tài liệu tự hiện ra và gọi thử bằng curl.

Phát LâmĐăng 8 phút đọcCơ bản
Viết API đầu tiên với FastAPI: quản lý sản phẩm, kiểm dữ liệu và tài liệu tự sinh
Mục lục
  1. API giống một quầy phục vụ
  2. Chuẩn bị
  3. Cho bạn nào viết code
  4. Gọi thử bằng curl
  5. Gửi dữ liệu sai thì sao
  6. Tài liệu tự hiện ra
  7. Lỗi hay gặp
  8. Làm tiếp

Cửa hàng có file Excel tồn kho. Website đọc số liệu từ đâu đó. Ứng dụng bán hàng ngoài quầy lại đọc từ chỗ khác. Mỗi nơi giữ một bản, và lúc nào cũng có một bản sai. Cách gọn nhất để thoát cảnh này là có một chỗ duy nhất giữ danh sách sản phẩm, và mọi nơi khác hỏi chỗ đó. Chỗ đó gọi là API.

API (giao diện để các chương trình nói chuyện với nhau) nghe xa vời, nhưng làm một cái nhỏ bằng Python chỉ mất khoảng 50 dòng. Bài này viết một API quản lý sản phẩm bằng FastAPI, chạy thật, gọi thử bằng curl, và bạn sẽ thấy trang tài liệu tự hiện ra mà không phải viết gì thêm.

API giống một quầy phục vụ

Hình dung chiếc quầy của một quán. Bạn đưa tờ giấy gọi món có ghi rõ: "thêm một ly", "cho xem thực đơn", "xoá món này". Nhân viên làm theo, rồi đưa lại tờ giấy trả lời: xong, hoặc không có món đó. Nếu tờ giấy ghi lung tung ("cho tôi một ly giá âm năm đồng"), nhân viên từ chối ngay và nói rõ chỗ nào sai.

API làm đúng như vậy. Tờ giấy gọi món là một yêu cầu (request) gửi qua mạng. Tờ giấy trả lời là phản hồi (response), kèm một con số cho biết kết quả: 200 là ổn, 201 là vừa tạo xong, 404 là không tìm thấy, 422 là dữ liệu gửi lên sai.

Thiết bị khách gửi gói dữ liệu theo đường sáng tới khối máy chủ có kệ hộp sản phẩm, phản hồi quay về theo đường thứ hai
Gửi yêu cầu đi, nhận phản hồi về.

Phần "kiểm tờ giấy gọi món" là việc của Pydantic, thư viện kiểm dữ liệu mà FastAPI dùng sẵn. Bạn khai báo một lần "sản phẩm phải có tên từ 2 ký tự, giá phải lớn hơn 0", nó tự từ chối mọi thứ không đạt.

Cổng kiểm ở giữa, gói vuông vắn đi qua còn gói méo bị trả lại
Pydantic chặn dữ liệu sai trước khi vào code.

Chuẩn bị

Cần Python và uv, xem bài Bắt đầu với Python năm 2026: cài đặt, môi trường riêng và chạy chương trình đầu tiên nếu bạn chưa có. Theo tài liệu FastAPI, cách cài chuẩn là:

mkdir kho-hang && cd kho-hang
uv init --bare --python 3.15
uv add "fastapi[standard]"

Mình kiểm ngày 21/10/2026: FastAPI 0.156.4 (yêu cầu Python từ 3.10 trở lên), Pydantic 2.14.1, Uvicorn 0.54.0. Gói [standard] kèm sẵn lệnh fastapi để chạy máy chủ.

Cho bạn nào viết code

Lưu file main.py. Dữ liệu tạm giữ trong bộ nhớ để ví dụ gọn; ở dự án thật bạn thay bằng database.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

app = FastAPI(title="Kho hàng nhỏ", version="1.0.0")


class SanPhamMoi(BaseModel):
    ten: str = Field(min_length=2, max_length=100, description="Tên sản phẩm")
    gia: int = Field(gt=0, description="Giá bán, đơn vị đồng")
    ton_kho: int = Field(default=0, ge=0)


class SanPham(SanPhamMoi):
    id: int


kho: dict[int, SanPham] = {}
id_tiep_theo = 1


@app.get("/san-pham", response_model=list[SanPham])
def danh_sach(gia_toi_da: int | None = None):
    ds = list(kho.values())
    if gia_toi_da is not None:
        ds = [sp for sp in ds if sp.gia <= gia_toi_da]
    return ds


@app.post("/san-pham", response_model=SanPham, status_code=201)
def them(sp: SanPhamMoi):
    global id_tiep_theo
    moi = SanPham(id=id_tiep_theo, **sp.model_dump())
    kho[moi.id] = moi
    id_tiep_theo += 1
    return moi


@app.get("/san-pham/{sp_id}", response_model=SanPham)
def xem(sp_id: int):
    if sp_id not in kho:
        raise HTTPException(status_code=404, detail="Không có sản phẩm này")
    return kho[sp_id]


@app.put("/san-pham/{sp_id}", response_model=SanPham)
def sua(sp_id: int, sp: SanPhamMoi):
    if sp_id not in kho:
        raise HTTPException(status_code=404, detail="Không có sản phẩm này")
    kho[sp_id] = SanPham(id=sp_id, **sp.model_dump())
    return kho[sp_id]


@app.delete("/san-pham/{sp_id}", status_code=204)
def xoa(sp_id: int):
    if kho.pop(sp_id, None) is None:
        raise HTTPException(status_code=404, detail="Không có sản phẩm này")

Có hai lớp sản phẩm vì một lý do đơn giản: khi thêm mới, người gọi không được tự đặt số id, máy chủ cấp. Lớp SanPhamMoi là "tờ giấy gọi món", lớp SanPham là "món đã ghi sổ".

Chạy máy chủ ở chế độ phát triển:

uv run fastapi dev --port 8010

Màn hình báo máy chủ đã chạy ở http://127.0.0.1:8010. Chế độ này tự nạp lại mỗi khi bạn sửa file, rất tiện lúc học. Khi đưa lên chạy thật thì dùng fastapi run, theo hướng dẫn của tài liệu.

Gọi thử bằng curl

Mở cửa sổ dòng lệnh khác. Thêm sản phẩm đầu tiên:

curl -i -X POST http://127.0.0.1:8010/san-pham \
  -H 'Content-Type: application/json' \
  -d '{"ten":"Chuột không dây","gia":190000,"ton_kho":12}'

Mình nhận được HTTP/1.1 201 Created và nội dung:

{"ten":"Chuột không dây","gia":190000,"ton_kho":12,"id":1}

Thêm tiếp "Bàn phím cơ" giá 650000, không ghi tồn kho; máy chủ tự điền ton_kho là 0. Xem danh sách và lọc theo giá:

curl http://127.0.0.1:8010/san-pham
curl "http://127.0.0.1:8010/san-pham?gia_toi_da=300000"

Lệnh thứ hai chỉ trả về chuột, vì bàn phím giá cao hơn 300000. Sửa giá và xoá cũng tương tự: curl -X PUT .../san-pham/1 -H 'Content-Type: application/json' -d '{...}' trả lại bản đã sửa, curl -X DELETE .../san-pham/2 trả mã 204 và không có nội dung.

Gửi dữ liệu sai thì sao

Thử gửi tên một chữ và giá âm:

curl -i -X POST http://127.0.0.1:8010/san-pham \
  -H 'Content-Type: application/json' -d '{"ten":"A","gia":-5}'

Phản hồi là 422 kèm lời giải thích rõ từng lỗi, bạn không phải viết dòng kiểm tra nào:

{"detail":[{"type":"string_too_short","loc":["body","ten"],"msg":"String should have at least 2 characters",...},
 {"type":"greater_than","loc":["body","gia"],"msg":"Input should be greater than 0",...}]}

Gửi "gia":"rẻ" thì được lỗi int_parsing, báo giá phải là số nguyên. Xin sản phẩm số 99 không có thì nhận 404 với dòng "Không có sản phẩm này".

Tài liệu tự hiện ra

Mở trình duyệt vào http://127.0.0.1:8010/docs. FastAPI dựng sẵn một trang tài liệu có thể bấm thử ngay: liệt kê từng đường dẫn, các trường cần gửi, ví dụ phản hồi, và nút "Try it out" để gọi luôn. Trang /redoc là một kiểu hiển thị khác của cùng thông tin. Mình kiểm cả hai trả mã 200, và /openapi.json báo chuẩn OpenAPI 3.1.0.

Khối mã nguồn sinh ra bảng tài liệu có công tắc, nút chạy và các thanh đường dẫn bo tròn
Viết code xong, trang tài liệu tự có.

Điều này có ích hơn bạn tưởng. Người làm giao diện web hay ứng dụng di động đọc trang này là biết gọi thế nào, không phải hỏi bạn từng câu. Bài Thiết kế API cho người khác dùng: báo lỗi, chia trang, thử lại và giới hạn lượt gọi nói tiếp về cách làm API dễ dùng hơn.

Lỗi hay gặp

Lỗi đầu tiên: dữ liệu mất khi khởi động lại. Vì mình giữ trong bộ nhớ, tắt máy chủ là sạch, và ở chế độ dev chỉ cần bạn lưu file là nó nạp lại và quên hết. Dùng database khi cần giữ lâu.

Lỗi thứ hai: quên dòng Content-Type: application/json trong curl. Máy chủ không hiểu bạn đang gửi gì và trả 422. Trên Windows PowerShell, dấu nháy và dấu \ xuống dòng cũng khác, nên bạn dùng nháy kép và gộp lệnh thành một dòng.

Lỗi thứ ba: ai cũng gọi được API này. Bản mẫu không có đăng nhập. Khi đưa ra internet, cần thêm bước xác thực, và nên đọc bài Thẻ đăng nhập JWT dùng chung giữa Node.js, PHP, Python và C# để biết một cách làm.

Làm tiếp

Bạn có API chạy được rồi. Việc tiếp theo hợp lý nhất là thay danh sách trong bộ nhớ bằng database, thêm đăng nhập, rồi thử gọi API từ một trang web hay file Excel của chính bạn. Nếu cửa hàng cần một API như thế kết nối website, sàn bán hàng và kho, cứ nhắn mình qua trang liên hệ.

PythonREST APIpythonfastapipydanticapicurlopenapi

Nguồn tham khảo

  1. FastAPI tutorial
  2. Pydantic documentation
  3. uv projects guide (Astral)
  4. OpenAPI Specification