Bài 20. Dự án: xây dựng API web với FastAPI

Dự án tổng kết: xây dựng API quản lý sách bằng FastAPI, kiểm tra dữ liệu bằng Pydantic, lưu vào SQLite với SQLModel, tự sinh tài liệu và viết kiểm thử. Đây là nền tảng để làm backend cho web và ứng dụng di động (Flutter).

120 phútNâng cao
Bạn sẽ học được
  • Tạo REST API với FastAPI
  • Kiểm tra dữ liệu vào ra bằng Pydantic
  • Lưu dữ liệu với SQLModel + SQLite
  • Kiểm thử API và chuẩn bị triển khai
Thuật ngữ cần biết
FastAPI
Framework web Python hiện đại, nhanh, dựa trên type hints.
Pydantic
Thư viện kiểm tra dữ liệu theo kiểu.
ORM
Ánh xạ bảng CSDL thành lớp Python.
OpenAPI
Chuẩn mô tả API; FastAPI tự sinh trang /docs.

Khái niệm và lý thuyết

FastAPI tận dụng type hints (Bài 13): khai báo kiểu tham số là đủ để FastAPI tự kiểm tra dữ liệu, chuyển đổi JSON, sinh tài liệu tương tác. Hỗ trợ cả hàm thường và async def (Bài 19).

Kiến trúc REST cho tài nguyên “sách”: GET /sach (danh sách), GET /sach/{id}, POST /sach (thêm), PATCH /sach/{id} (sửa), DELETE /sach/{id}.

Khởi tạo

uv init api-sach && cd api-sach
uv add "fastapi[standard]" sqlmodel
uv add --dev pytest

main.py

from fastapi import FastAPI, HTTPException, Depends
from sqlmodel import SQLModel, Field, Session, create_engine, select

class SachTao(SQLModel):
    ten: str = Field(min_length=1, max_length=200)
    tac_gia: str
    nam: int = Field(ge=1900, le=2100)

class Sach(SachTao, table=True):
    id: int | None = Field(default=None, primary_key=True)

engine = create_engine("sqlite:///sach.db")
SQLModel.metadata.create_all(engine)
def get_session():
    with Session(engine) as s:
        yield s

app = FastAPI(title="API Sách - Tự Học Tin")

@app.get("/sach", response_model=list[Sach])
def danh_sach(tu_khoa: str = "", s: Session = Depends(get_session)):
    q = select(Sach).where(Sach.ten.contains(tu_khoa))
    return s.exec(q).all()

@app.get("/sach/{id}", response_model=Sach)
def chi_tiet(id: int, s: Session = Depends(get_session)):
    sach = s.get(Sach, id)
    if not sach:
        raise HTTPException(404, "Không tìm thấy sách")
    return sach

@app.post("/sach", response_model=Sach, status_code=201)
def them(du_lieu: SachTao, s: Session = Depends(get_session)):
    sach = Sach.model_validate(du_lieu)
    s.add(sach); s.commit(); s.refresh(sach)
    return sach

@app.delete("/sach/{id}", status_code=204)
def xoa(id: int, s: Session = Depends(get_session)):
    sach = chi_tiet(id, s)
    s.delete(sach); s.commit()
uv run fastapi dev main.py
# Mở http://127.0.0.1:8000/docs để thử API ngay trên trình duyệt

Kiểm thử API

from fastapi.testclient import TestClient
from main import app
client = TestClient(app)

def test_them_va_lay():
    r = client.post("/sach", json={"ten": "Học Python", "tac_gia": "TBU", "nam": 2026})
    assert r.status_code == 201
    id = r.json()["id"]
    assert client.get(f"/sach/{id}").json()["ten"] == "Học Python"

def test_du_lieu_sai():
    assert client.post("/sach", json={"ten": "", "tac_gia": "A", "nam": 1800}).status_code == 422

Triển khai

  • Chạy production: uv run fastapi run main.py (Uvicorn).
  • Đóng gói Docker hoặc triển khai lên Render, Railway, Fly.io, máy chủ VPS.
  • Thay SQLite bằng PostgreSQL khi nhiều người dùng: chỉ đổi chuỗi kết nối.
  • Bật CORS (CORSMiddleware) khi front-end JavaScript/Flutter gọi từ tên miền khác.
Mẹo hay

Ứng dụng Flutter (loạt bài Flutter, Bài 9) hoặc trang JavaScript (Bài 7) có thể gọi trực tiếp API này: bạn đã có một hệ thống full-stack hoàn chỉnh.

Lưu ý

Không đưa tệp sach.db, khóa bí mật lên GitHub. Thêm xác thực (OAuth2/JWT của FastAPI) trước khi cho phép thêm/xóa trên Internet.

Thực hành
  1. Hoàn thiện API, thêm PATCH /sach/{id} để sửa từng phần.
  2. Thêm bảng TheLoai và quan hệ một-nhiều với Sach.
  3. Viết đủ test cho mọi endpoint, chạy pytest.
  4. Viết trang HTML + JavaScript hiển thị danh sách sách từ API.
Tóm tắt bài học
  • FastAPI dựa trên type hints, tự kiểm tra dữ liệu và sinh /docs.
  • Pydantic/SQLModel mô tả dữ liệu và bảng CSDL.
  • TestClient kiểm thử API không cần chạy máy chủ.
  • Đây là nền backend cho web và di động.