Bỏ qua

Bài 1: Khởi tạo Project FastAPI từ Zero

Đọc sau bài 0, trước bài 2. Bài này không giải thích concept — chỉ hướng dẫn setup môi trường và các pattern nền tảng cần có trước khi bắt đầu bất kỳ project Python/FastAPI nào. Mục tiêu: khi đọc xong bài 2-8, bạn có thể tự tạo được project mới từ màn hình trắng.


1. Virtual Environment — Tại sao bắt buộc?

Python cài package globally theo mặc định. Nếu project A cần fastapi==0.100 và project B cần fastapi==0.115, hai cái sẽ xung đột. Virtual environment tạo môi trường Python riêng cho mỗi project.

# Tạo venv trong thư mục .venv (tên convention)
python -m venv .venv

# Kích hoạt (macOS / Linux)
source .venv/bin/activate

# Kích hoạt (Windows)
.venv\Scripts\activate

# Kiểm tra đang dùng đúng Python
which python    # phải trỏ vào .venv/bin/python

Sau khi activate, prompt terminal có thêm (.venv) ở đầu — xác nhận venv đang active.


2. Dependencies — Cài và lưu

Cài package

pip install fastapi uvicorn[standard] sqlalchemy aiosqlite python-multipart python-dotenv
Package Vai trò
fastapi Web framework
uvicorn[standard] ASGI server (chạy FastAPI)
sqlalchemy SQLAlchemy Core + ORM (project này chỉ dùng Core)
aiosqlite SQLite driver async
python-multipart Bắt buộc để FastAPI nhận UploadFileForm
python-dotenv Load file .env vào os.environ

Lưu dependencies

pip freeze > requirements.txt

Cài trên máy khác / CI

pip install -r requirements.txt

requirements.txt phải commit vào git để người khác và CI cài đúng phiên bản. .venv/ thì không — thêm vào .gitignore.


3. Cấu trúc thư mục tối thiểu

Không có cấu trúc "đúng duy nhất", nhưng đây là pattern được cả KCDS và phần lớn FastAPI project dùng:

my_project/
├── .env                ← secrets, giá trị cụ thể của môi trường (KHÔNG commit)
├── .env.example        ← template với key nhưng không có giá trị (commit)
├── .gitignore
├── requirements.txt
└── src/
    ├── main.py         ← FastAPI app + lifespan + mount routers
    ├── config.py       ← TẤT CẢ env vars tập trung ở đây
    ├── models.py       ← Pydantic schemas (request/response shape)
    ├── deps.py         ← shared Depends() factories (dùng ở ≥2 router)
    ├── db/
    │   └── engine.py   ← SQLAlchemy engine, session factory, table defs
    ├── routers/
    │   └── items.py    ← APIRouter per resource (1 file = 1 resource)
    ├── services/
    │   └── item_svc.py ← business logic
    └── repositories/
        ├── base.py     ← abstract interfaces (ABC)
        └── item_repo.py← concrete implementation (SQLite query)

Quy tắc đặt tên file: 1 resource = 1 router file + 1 service file + 1 repo file. Thêm resource mới → tạo 3 file mới, mount router vào main.py — không sửa code cũ.

.gitignore tối thiểu:

.venv/
.env
__pycache__/
*.pyc
data/
*.db

4. Config — Tập trung env vars

Tại sao cần file config.py riêng?

Nếu gọi os.getenv("X") rải rác ở nhiều file: - Không biết app cần những env nào để chạy - Đổi tên env var → phải tìm khắp codebase - Không có chỗ validate env bắt buộc sớm

Tập trung vào config.py: đọc 1 file là biết toàn bộ configuration.

Pattern từ KCDS

# src/config.py
import os
from dotenv import load_dotenv

load_dotenv()  # đọc file .env và set vào os.environ

# Khai báo tất cả env vars ở đây
# Code ở file khác chỉ import từ đây, không gọi os.getenv() trực tiếp
DATABASE_URL  = os.getenv("DATABASE_URL", "sqlite+aiosqlite:///./data/app.db")
SECRET_KEY    = os.getenv("SECRET_KEY", "")
AUDIO_DIR     = os.getenv("AUDIO_DIR", "./data/audio")
MAX_UPLOAD_MB = int(os.getenv("MAX_UPLOAD_MB", "50"))
DEBUG         = os.getenv("DEBUG", "false").lower() == "true"


def validate() -> None:
    """Fail fast — app không bao giờ start khi thiếu env bắt buộc."""
    required = [
        ("SECRET_KEY", SECRET_KEY),
        # thêm các env bắt buộc khác vào đây
    ]
    missing = [name for name, val in required if not val]
    if missing:
        raise RuntimeError(f"Thiếu env vars bắt buộc: {missing}")
# src/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from src import config

@asynccontextmanager
async def lifespan(app: FastAPI):
    config.validate()  # fail fast trước khi nhận bất kỳ request nào
    yield

app = FastAPI(lifespan=lifespan)
# src/services/audio_svc.py
from src.config import AUDIO_DIR, MAX_UPLOAD_MB  # import config, không os.getenv()

def check_size(size: int) -> None:
    if size > MAX_UPLOAD_MB * 1024 * 1024:
        raise ValueError(f"File vượt quá {MAX_UPLOAD_MB}MB")

.env (không commit — giá trị thật)

SECRET_KEY=abc123devkey
DATABASE_URL=sqlite+aiosqlite:///./data/app.db

.env.example (commit — template cho người khác)

# Tạo SECRET_KEY: python -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=

DATABASE_URL=sqlite+aiosqlite:///./data/app.db
MAX_UPLOAD_MB=50

Người mới clone repo chỉ cần cp .env.example .env và điền giá trị.


5. Chạy dev server

# Từ thư mục root của project (ngang với src/)
uvicorn src.main:app --reload
  • src.main = file src/main.py
  • :app = biến app = FastAPI(...) trong file đó
  • --reload = tự restart khi sửa code (CHỈ dùng khi dev, không dùng production)

Xem docs tự sinh: Mở http://localhost:8000/docs — FastAPI generate OpenAPI UI từ code, test endpoint ngay mà không cần Postman.

Docker không bắt buộc khi develop. Docker trong KCDS phục vụ production deployment — bundling nhiều service, volume, network. Khi học, uvicorn --reload là đủ và đơn giản hơn nhiều.


6. Pydantic — Tất cả loại input

FastAPI nhận input từ 5 nguồn khác nhau. Pydantic xử lý validate cho từng loại. Bài 2 sẽ đi sâu vào từng cơ chế — ở đây chỉ cần quen tay với các cách khai báo input trước.

6a. Path parameter — từ URL

@router.get("/{item_id}")
async def get_item(item_id: int):
    # URL: /items/42  →  item_id = 42 (int)
    # URL: /items/abc →  HTTP 422 tự động (không parse được int)
    ...

Path param là phần của URL pattern ({}). FastAPI tự cast và validate kiểu.

6b. Query parameter — từ ?key=value

@router.get("")
async def list_items(
    q: str | None = None,   # /items?q=foo  →  q = "foo"
    page: int = 1,          # /items         →  page = 1 (default)
    active: bool = True,    # /items?active=false  →  active = False
):
    ...

FastAPI tự phân biệt query param và path param dựa vào tên: nếu tên không khớp {} trong route → là query param.

6c. JSON request body — từ Content-Type: application/json

# src/models.py
from pydantic import BaseModel

class ItemCreate(BaseModel):
    name: str
    price: float
    category: str | None = None  # optional, default None
# router
@router.post("")
async def create_item(body: ItemCreate):
    # FastAPI detect: tham số kiểu BaseModel → đọc từ JSON body
    # body.name, body.price đã được validate type
    ...

FastAPI auto-detect: tham số có kiểu là subclass của BaseModel → đọc từ JSON body (không cần khai báo Body(...)).

6d. Form data — từ HTML <form>

from fastapi import Form

@router.post("/login")
async def login(
    username: str = Form(...),  # ... = required (không có default)
    password: str = Form(...),
):
    # Content-Type: application/x-www-form-urlencoded
    ...

JSON body vs Form: Khi nào dùng cái nào? - Browser <form> submit → Form (application/x-www-form-urlencoded hoặc multipart/form-data) - fetch() với JSON.stringify → JSON body (application/json) - Không thể dùng cả Form lẫn JSON body trong cùng 1 endpoint

6e. File upload — multipart

from fastapi import File, UploadFile, Form

@router.post("/upload")
async def upload_audio(
    file: UploadFile,           # multipart file field
    title: str = Form(...),     # text field đi kèm trong cùng form
):
    data = await file.read()          # bytes — đọc toàn bộ vào RAM
    filename = file.filename          # tên gốc từ client
    content_type = file.content_type  # "audio/mpeg", "image/png"...
    ...

Khi upload file, các text field đi kèm bắt buộc dùng Form(...) — không thể mix UploadFile + JSON body.

Xử lý file lớn (streaming):

# Thay vì file.read() một lần (tốn RAM):
async with aiofiles.open(dest, "wb") as f:
    while chunk := await file.read(1024 * 64):  # đọc từng 64KB
        await f.write(chunk)

6f. Pydantic validators — validate logic không cần DB

from pydantic import BaseModel, field_validator

class AudioCreate(BaseModel):
    title: str
    category_id: int | None = None

    @field_validator("title")
    @classmethod
    def title_not_empty(cls, v: str) -> str:
        v = v.strip()
        if not v:
            raise ValueError("title không được rỗng")
        return v  # trả về giá trị đã transform (stripped)

FastAPI tự gọi validator khi parse body. Nếu validator raise ValueError → HTTP 422 tự động với message lỗi.

Quy tắc quan trọng về phạm vi validator:

Validate ở Pydantic Validate ở Service layer
Kiểu dữ liệu, range, format Logic cần query DB
"price > 0" "category_id phải tồn tại trong DB"
"title không rỗng" "filename chưa bị trùng"
"email đúng format" "user đã có role này chưa"

Validator trong Pydantic không có DB session — không thể query. Bài 5 sẽ giải thích tại sao business validation ở Service layer.


7. Kết hợp nhiều loại input

Trong thực tế, 1 endpoint thường mix nhiều loại:

@router.put("/{item_id}")               # ← path param
async def update_item(
    item_id: int,                        # ← path param (từ URL)
    verbose: bool = False,              # ← query param (từ ?verbose=true)
    body: ItemUpdate = Body(...),        # ← JSON body
    session: AsyncSession = Depends(get_session),  # ← DI (bài 3)
    user: dict = Depends(require_editor),           # ← DI (bài 3)
):
    ...

FastAPI biết item_id là path param (tên khớp {item_id} trong route), verbose là query param (không khớp {}, không phải BaseModel), body là JSON body (kiểu BaseModel), sessionuser là dependency (Depends).


Bài tập: Project "Notes API" từ zero

Mục tiêu: áp dụng bài 1 trước khi đọc bài 2-5.

Yêu cầu: 1. Setup venv, install fastapi uvicorn[standard] python-dotenv 2. Tạo src/config.py với APP_TITLE = os.getenv("APP_TITLE", "My Notes") 3. Tạo src/main.py với FastAPI(title=config.APP_TITLE) và lifespan gọi config.validate() 4. Tạo router /notes với CRUD đơn giản (in-memory dict, không cần DB) 5. Tạo Pydantic model NoteCreate(title: str, content: str) với validator content dài ít nhất 10 ký tự 6. Chạy uvicorn src.main:app --reload, test qua http://localhost:8000/docs

Không cần: - DB (dùng notes = {} in-memory) - Auth - Docker

Chốt lại sau khi làm xong: Bạn có thể tạo được cấu trúc project, config env, khai báo router, và validate input — trước khi học bất kỳ concept nào từ bài 2-5.


Nguyên lý tổng quát

Pattern Nguyên lý (bài 0) Chuyển giao
config.py tập trung DRY — "how to get config" là 1 knowledge ở 1 nơi Django settings.py, Node.js config/index.js, Spring application.yaml
validate() trong lifespan Fail Fast — phát hiện misconfiguration trước khi nhận request Mọi framework có startup hook — đây là nơi đặt pre-condition check
Pydantic validator chỉ check không cần DB SoC — shape validation tách khỏi business validation Bài 5 sẽ giải thích: business validation ở Service layer
.env.example commit, .env không commit Security + DRY — mỗi môi trường có giá trị riêng, không hardcode Standard trong mọi project hiện đại
1 resource = 1 router file SoC + OCP — thêm resource mới không sửa code cũ Bất kỳ framework nào cũng nên chia route theo resource