Sổ tay kỹ thuật

FastAPI cơ bản – route, Pydantic model, dependency

how-to#python#fastapi#api· cập nhật 2026-11-06

Dùng khi cần dựng nhanh một REST API nhỏ (vd inventory thiết bị mạng) bằng FastAPI + Pydantic v2, chạy bằng uvicorn và có OpenAPI tự sinh.

Sửa trang này
Mục lục

Tóm tắt: uv add "fastapi[standard]" → khai báo FastAPI(), route bằng decorator, body/response bằng Pydantic BaseModel, kiểm tra quyền bằng Depends → fastapi dev main.py → mở /docs. Ví dụ dưới đã test với FastAPI 0.115 và Pydantic 2.11.

Luồng hoạt động

flowchart LR C[Client] -->|HTTP JSON| U[uvicorn] U --> F[FastAPI router] F --> D["Depends (auth, settings)"] F --> V["Pydantic validate body/query"] V -->|lỗi| E[422 Unprocessable Entity] V --> H[Handler] H --> R["response_model lọc output"] R --> C

Cấu hình

Cài đặt

uv init netinv && cd netinv
uv add "fastapi[standard]"     # kèm uvicorn, fastapi-cli, httpx

Ứng dụng tối thiểu

main.py
from contextlib import asynccontextmanager
from ipaddress import IPv4Address
from typing import Annotated
 
from fastapi import Depends, FastAPI, Header, HTTPException, Query, status
from pydantic import BaseModel, ConfigDict, Field
 
DEVICES: dict[str, dict] = {}
 
 
@asynccontextmanager
async def lifespan(app: FastAPI):
    # chạy 1 lần khi khởi động: mở DB pool, nạp cache...
    DEVICES["pe1-hni"] = {"hostname": "pe1-hni", "mgmt_ip": "198.51.100.11", "platform": "iosxr"}
    yield
    DEVICES.clear()  # khi tắt
 
 
app = FastAPI(title="NetInventory API", version="0.1.0", lifespan=lifespan)
 
 
class DeviceIn(BaseModel):
    model_config = ConfigDict(extra="forbid")  # field lạ -> 422
    hostname: str = Field(min_length=1, max_length=63, pattern=r"^[a-z0-9-]+$")
    mgmt_ip: IPv4Address
    platform: str = Field(default="iosxr", examples=["iosxr", "junos"])
 
 
class DeviceOut(DeviceIn):
    pass
 
 
async def require_api_key(x_api_key: Annotated[str | None, Header()] = None) -> str:
    # Header x_api_key <-> HTTP header "X-API-Key"
    if x_api_key is None:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="missing X-API-Key")
    return x_api_key
 
 
AuthDep = Annotated[str, Depends(require_api_key)]
 
 
@app.get("/healthz")
async def healthz() -> dict[str, str]:
    return {"status": "ok"}
 
 
@app.get("/devices", response_model=list[DeviceOut])
async def list_devices(
    platform: Annotated[str | None, Query(max_length=20)] = None,
    limit: int = 100,
):
    items = [d for d in DEVICES.values() if platform is None or d["platform"] == platform]
    return items[:limit]
 
 
@app.get("/devices/{hostname}", response_model=DeviceOut)
async def get_device(hostname: str):
    if hostname not in DEVICES:
        raise HTTPException(status_code=404, detail="device not found")
    return DEVICES[hostname]
 
 
@app.post("/devices", response_model=DeviceOut, status_code=status.HTTP_201_CREATED)
async def create_device(device: DeviceIn, _: AuthDep):
    if device.hostname in DEVICES:
        raise HTTPException(status_code=409, detail="already exists")
    DEVICES[device.hostname] = device.model_dump(mode="json")
    return DEVICES[device.hostname]

Quy tắc FastAPI suy ra nguồn tham số:

Khai báo Lấy từ
Tên trùng {hostname} trong path Path parameter
Kiểu đơn (str, int, bool...) không có trong path Query string
Kiểu BaseModel JSON body
Annotated[..., Header()] / Cookie() / Form() Header / cookie / form
Annotated[..., Depends(func)] Kết quả của func (có thể lồng nhau)

Chạy

uv run fastapi dev main.py                        # dev, auto reload, cổng 8000
uv run fastapi run main.py --port 8000            # production mode, listen 0.0.0.0
uv run uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4   # gọi uvicorn trực tiếp

Gọi thử và OpenAPI

curl -s localhost:8000/devices?platform=iosxr
curl -s -X POST localhost:8000/devices \
  -H "X-API-Key: $API_KEY" -H 'Content-Type: application/json' \
  -d '{"hostname":"pe2-hcm","mgmt_ip":"198.51.100.12"}'
curl -s localhost:8000/openapi.json | jq '.paths | keys'
URL Nội dung
/docs Swagger UI, gọi thử API trên trình duyệt
/redoc ReDoc
/openapi.json Schema OpenAPI, dùng để sinh client

Pydantic v2: thay đổi hay gặp so với v1

v1 v2
class Config: model_config = ConfigDict(...)
.dict() / .json() .model_dump() / .model_dump_json()
parse_obj() Model.model_validate(data)
orm_mode = True from_attributes=True
@validator @field_validator("field") (kèm @classmethod)
Field(regex=...) Field(pattern=...)
BaseSettings trong pydantic Gói riêng pydantic-settings

Lỗi thường gặp

Triệu chứng Nguyên nhân Cách xử lý
422 Unprocessable Entity Body/query sai kiểu hoặc thiếu field Đọc detail[].loc và detail[].type trong response
fastapi: command not found Cài fastapi không kèm [standard] uv add "fastapi[standard]" hoặc dùng uvicorn main:app
Error loading ASGI app. Could not import module "main" Sai thư mục hoặc sai module:app Chạy từ thư mục chứa main.py, hoặc --app-dir
API treo khi nhiều request Gọi hàm blocking (requests, time.sleep, SSH) trong async def Dùng def thường (chạy threadpool) hoặc thư viện async
Response lộ field nhạy cảm Trả thẳng dict/ORM object Khai báo response_model chỉ gồm field public
AttributeError: 'Model' object has no attribute 'dict' Code viết cho Pydantic v1 Đổi sang model_dump()
Trình duyệt báo CORS Frontend khác origin Thêm CORSMiddleware với allow_origins cụ thể

Tham khảo