---
title: "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"
url: https://phatdev.net/vi/blog/python-fastapi-first-api
lang: vi
updated: 2026-10-21
alternate:
  en: https://phatdev.net/en/blog/python-fastapi-first-api
markdown: https://phatdev.net/vi/blog/python-fastapi-first-api.md
---

# 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.

Chuyên mục: Backend | Đăng: 2026-10-21 | Cấp độ: Cơ bản | 8 phút đọc

Tags: python, fastapi, pydantic, api, curl, openapi

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.

![Máy khách gửi yêu cầu tới máy chủ giữ kệ sản phẩm và nhận phản hồi](https://phatdev.net/uploads/blog/python-fastapi-first-api/request-1920.webp)

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: gói đúng đi qua, gói sai bị trả lại](https://phatdev.net/uploads/blog/python-fastapi-first-api/validate-1920.webp)

## 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](https://phatdev.net/vi/blog/python-start-2026) nếu bạn chưa có. Theo tài liệu FastAPI, cách cài chuẩn là:

```bash
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.

```python
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:

```bash
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:

```bash
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:

```json
{"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á:

```bash
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:

```bash
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:

```json
{"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.

![Mã nguồn sinh ra bảng tài liệu có nút bấm thử](https://phatdev.net/uploads/blog/python-fastapi-first-api/docs-1920.webp)

Đ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](https://phatdev.net/vi/blog/rest-api-design-practical) 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#](https://phatdev.net/vi/blog/jwt-across-languages) để 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ệ.

## Nguồn tham khảo

- [FastAPI tutorial](https://fastapi.tiangolo.com/tutorial/)
- [Pydantic documentation](https://docs.pydantic.dev/latest/)
- [uv projects guide (Astral)](https://docs.astral.sh/uv/guides/projects/)
- [OpenAPI Specification](https://spec.openapis.org/oas/latest.html)

## Bài liên quan

- [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](https://phatdev.net/vi/blog/python-start-2026.md)
- [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](https://phatdev.net/vi/blog/rest-api-design-practical.md)
- [Thẻ đăng nhập JWT dùng chung giữa Node.js, PHP, Python và C#](https://phatdev.net/vi/blog/jwt-across-languages.md)
