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¶
| 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 UploadFile và Form |
python-dotenv |
Load file .env vào os.environ |
Lưu dependencies¶
Cài trên máy khác / CI¶
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:
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)¶
.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¶
src.main= filesrc/main.py:app= biếnapp = 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 --reloadlà đủ 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-urlencodedhoặcmultipart/form-data) -fetch()vớiJSON.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), session và user 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 |