Bài 1: Khởi tạo Project FastAPI từ con số 0¶
Lưu ý: Hãy đọc bài này sau khi bạn đã thẩm thấu xong Bài 0 (Nguyên lý) và trước khi bước sang Bài 2. Ở bài này, chúng ta khoan bàn về những khái niệm sâu xa. Mục tiêu cốt lõi là giúp bạn biết cách setup một môi trường làm việc chuẩn chỉnh và nắm được những pattern (khuôn mẫu) nền tảng nhất. Đọc xong, bạn hoàn toàn đủ trình độ tự tay dựng một project FastAPI mới toanh từ một màn hình đen kịt.
1. Virtual Environment (Môi trường ảo) — Tại sao lại bắt buộc?¶
Theo mặc định, khi bạn gõ lệnh pip install, Python sẽ cài đặt package đó thẳng vào hệ thống dùng chung (globally) của máy tính. Hãy thử tưởng tượng: Project A của bạn cần xài thư viện fastapi==0.100, nhưng Project B lại nằng nặc đòi bản fastapi==0.115. Nếu cài chung, hai dự án này sẽ "đánh nhau" vỡ đầu ngay lập tức.
Đó là lý do Virtual Environment (venv) ra đời. Nó tạo ra một "căn nhà riêng" biệt lập cho từng project.
# Tạo môi trường ảo trong thư mục có tên là .venv (đây là quy chuẩn chung)
python -m venv .venv
# Kích hoạt trên macOS / Linux
source .venv/bin/activate
# Kích hoạt trên Windows
.venv\Scripts\activate
# Kiểm tra xem đã dùng đúng Python của môi trường ảo chưa
which python # Kết quả trả về phải trỏ vào thư mục .venv/bin/python
Sau khi kích hoạt thành công, bạn sẽ thấy chữ (.venv) xuất hiện chễm chệ ở đầu dòng lệnh terminal. Thấy nó rồi thì mới yên tâm gõ code tiếp nhé.
2. Dependencies — Sắm sửa và Đóng gói "đồ nghề"¶
Cài đặt các package thiết yếu¶
Dành cho những ai chưa rõ rổ "đồ nghề" này làm nhiệm vụ gì:
| Package | Vai trò thực chiến |
|---|---|
fastapi |
Web framework chính, ngôi sao của dự án. |
uvicorn[standard] |
Web server ASGI (đóng vai trò như chiếc động cơ để chạy FastAPI). |
sqlalchemy |
Thư viện thao tác với Database (Ở project này chúng ta chỉ dùng bản Core thuần túy, tuyệt đối không xài ORM). |
aiosqlite |
Driver giúp Python có thể nói chuyện với SQLite theo kiểu bất đồng bộ (async). |
python-multipart |
Bắt buộc phải cài để FastAPI có thể đọc được file upload (UploadFile) và Form data. |
python-dotenv |
Dùng để móc các biến môi trường từ file .env nạp vào hệ thống. |
Chốt sổ danh sách dependencies¶
Để đồng nghiệp (hoặc server lúc đem đi deploy) có thể cài lại chính xác những version mà bạn đang xài, hãy đóng gói chúng lại:
Cài lại trên máy khác hoặc lúc Deploy¶
Nguyên tắc sống còn: Bạn PHẢI đưa file requirements.txt lên Git (commit). Nhưng với thư mục .venv/ thì tuyệt đối KHÔNG BAO GIỜ (hãy nhớ ném nó vào file .gitignore).
3. Cấu trúc thư mục tối thiểu¶
Thực ra FastAPI rất dễ tính, nó không ép bạn phải theo một cấu trúc cố định nào cả. Nhưng để code không biến thành một bãi chiến trường khi dự án phình to, đây là cấu trúc thực chiến được dùng trong KCDS và rất nhiều dự án chuyên nghiệp khác:
my_project/
├── .env ← File chứa mật khẩu, token thật (TUYỆT ĐỐI KHÔNG commit lên Git)
├── .env.example ← Bản mẫu chứa tên các biến, nhưng để trống giá trị (Có commit)
├── .gitignore
├── requirements.txt
└── src/
├── main.py ← Trái tim của app (Khởi tạo FastAPI, lifespan, mount routers)
├── config.py ← Gom TẤT CẢ biến môi trường của dự án về đây
├── models.py ← Chứa các Pydantic schemas (định dạng dữ liệu đầu vào/ra)
├── deps.py ← Nơi chứa các hàm Dependency dùng chung cho nhiều router
├── db/
│ └── engine.py ← Kết nối database, tạo session, định nghĩa bản vẽ cấu trúc bảng
├── routers/
│ └── items.py ← Mỗi file đảm nhận một tài nguyên (API endpoint) riêng
├── services/
│ └── item_svc.py ← Bộ não xử lý logic nghiệp vụ (Business logic)
└── repositories/
├── base.py ← Các bản hợp đồng trừu tượng (Abstract interfaces)
└── item_repo.py← Lệnh gọi database cụ thể (Ví dụ: SQLite query)
Quy tắc ngầm trong giới thực chiến: Cứ mỗi khi có thêm 1 tài nguyên mới (ví dụ: Audio), bạn sẽ phải tạo ra 3 file mới tương ứng nằm ở routers, services, và repositories, sau đó gắn (mount) cái router đó vào file main.py. Nhờ quy tắc này, bạn không bao giờ phải đục phá làm nát các file code cũ đang chạy ổn định.
File .gitignore tối thiểu nên có những dòng này:
4. Config — Quy mọi quyền lực về một mối¶
Tại sao lại cần sắm riêng một file config.py?¶
Nhiều anh em dev có thói quen tiện đâu thì gọi os.getenv("XXX") ở đó. Hậu quả nhãn tiền là:
- Người vào team sau đọc code sẽ mù tịt, không biết app này cần cấu hình những biến gì mới chạy nổi.
- Ngày đẹp trời sếp đổi tên biến trên server, bạn sẽ phải dùng "Find and Replace" đi lùng sục khắp cả codebase.
- Lỡ quên setup một biến môi trường trên server, app cứ chạy nửa chừng đến đoạn đó mới nổ lỗi sập nguồn, debug cực kỳ mất thời gian.
Gom tất cả vào file config.py sẽ cho bạn một cái nhìn toàn cảnh: Chỉ cần liếc mắt là biết hệ thống này "ăn" những nhiên liệu gì.
Pattern chuẩn từ dự án KCDS¶
# src/config.py
import os
from dotenv import load_dotenv
load_dotenv() # Nạp các biến từ file .env vào hệ thống
# Khai báo tất cả ở đây.
# Từ giờ, code ở các file khác chỉ được import biến từ file này, cấm xài os.getenv() rải rác nữa.
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:
"""Nguyên lý Fail-fast: Cắt cầu dao điện ngay từ đầu nếu thiếu biến quan trọng."""
required = [
("SECRET_KEY", SECRET_KEY),
# Thêm các biến sống còn khác vào đây
]
missing = [name for name, val in required if not val]
if missing:
raise RuntimeError(f"Server không thể khởi động vì thiếu env: {missing}")
Cách dùng trong file khởi động chính:
# src/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from src import config
@asynccontextmanager
async def lifespan(app: FastAPI):
config.validate() # Kiểm tra sức khỏe cấu hình ngay lúc vừa bật server
yield # Xong xuôi thì cho chạy app
app = FastAPI(lifespan=lifespan)
Cách dùng trong các module khác cực kỳ mượt mà:
# src/services/audio_svc.py
from src.config import AUDIO_DIR, MAX_UPLOAD_MB # Import thẳng biến từ file config
def check_size(size: int) -> None:
if size > MAX_UPLOAD_MB * 1024 * 1024:
raise ValueError(f"Dung lượng file vượt mức cho phép: {MAX_UPLOAD_MB}MB")
Quản lý file .env¶
File .env (Chứa giá trị thật, TUYỆT ĐỐI KHÔNG COMMIT):
File .env.example (Bản nháp để báo cho đồng đội biết cần điền gì, CÓ COMMIT):
# Mẹo tạo SECRET_KEY ngẫu nhiên: mở terminal chạy lệnh `python -c "import secrets; print(secrets.token_hex(32))"`
SECRET_KEY=
DATABASE_URL=sqlite+aiosqlite:///./data/app.db
MAX_UPLOAD_MB=50
Team mate mới clone code về chỉ việc copy file .env.example đổi tên thành .env rồi điền số vào là chạy ngon ơ.
5. Chạy thử Server khi code (Dev mode)¶
src.main: Trỏ thẳng vào filemain.pynằm trong thư mụcsrc.app: Chính là cái tên biếnapp = FastAPI()khai báo trong file đó.--reload: Auto-restart server mỗi khi bạn bấm lưu file (Chỉ xài lúc dev ở local, đem lên production thì tuyệt đối bỏ cờ này đi).
Món quà tặng kèm từ FastAPI: Mở trình duyệt lên và vào http://localhost:8000/docs. FastAPI đã tự động đọc lướt code của bạn và vẽ ra một trang tài liệu OpenAPI (Swagger) siêu xịn. Bạn có thể test gọi API trực tiếp trên web mà không cần cài Postman hay gõ cURL rườm rà.
💡 Tip cho Newbie: Lúc dev, bạn không cần phải dựng Docker lên làm gì cho nặng máy. Docker sinh ra là để dành cho bước đem lên production. Chạy lệnh
uvicornthẳng trên máy local là quá đủ và cực kỳ thần tốc.
6. Pydantic — "Người gác cổng" dữ liệu kiên ngạnh¶
Một ứng dụng web thường bị "dội bom" dữ liệu từ 5 đường khác nhau. Pydantic (thư viện xương sống đính kèm của FastAPI) sẽ đứng ra làm bảo vệ kiểm duyệt tất cả.
6a. Dữ liệu bám trên đường dẫn (Path Parameter)¶
@router.get("/{item_id}")
async def get_item(item_id: int):
# Nếu client gọi: /items/42 → item_id = 42 (kiểu số nguyên int)
# Nếu client gọi: /items/abc → FastAPI tự động chặn lại và ném thẳng lỗi 422 vào mặt client
...
Phần nào nằm gọn trong cặp ngoặc nhọn {} thì nó đích thị là Path Parameter.
6b. Dữ liệu đu bám ở đuôi URL (Query Parameter)¶
@router.get("")
async def list_items(
q: str | None = None, # URL: /items?q=foo → q = "foo"
page: int = 1, # URL: /items → page mặc định tự động là 1
active: bool = True, # URL: /items?active=false → active = False
):
...
Cái gì không nằm trong ngoặc nhọn {} trên route thì FastAPI tự động ngầm hiểu đó là tham số truy vấn nằm sau dấu ?.
6c. Dữ liệu giấu trong thân (JSON Body)¶
# src/models.py
from pydantic import BaseModel
class ItemCreate(BaseModel):
name: str
price: float
category: str | None = None # Cho phép bỏ trống, nếu trống mặc định là None
# router
@router.post("")
async def create_item(body: ItemCreate):
# Cứ thấy tham số là một cái BaseModel, FastAPI tự biết phải chui vào body JSON để móc dữ liệu ra.
# Lúc này bạn cứ yên tâm xài body.name và body.price vì chúng đã được đảm bảo đúng kiểu.
...
6d. Dữ liệu đẩy lên từ biểu mẫu (Form Data)¶
from fastapi import Form
@router.post("/login")
async def login(
username: str = Form(...), # Dấu ba chấm (...) mang ý nghĩa pháp lý là "bắt buộc phải có"
password: str = Form(...),
):
# Content-Type của request gửi lên lúc này sẽ là: application/x-www-form-urlencoded
...
Hỏi xoáy đáp xoay: Vậy khi nào dùng Form, khi nào dùng JSON? - Nếu submit từ thẻ
<form>cổ điển của HTML thuần → Xài Form. - Nếu dùng Javascriptfetch()gửi data lên kết hợpJSON.stringify→ Xài JSON. - Lưu ý chí mạng: Bạn KHÔNG THỂ vừa nhét Form vừa nhét JSON body vào chung 1 cái API endpoint được.
6e. Dữ liệu Khủng — Upload File (Multipart)¶
from fastapi import File, UploadFile, Form
@router.post("/upload")
async def upload_audio(
file: UploadFile, # Nhận nguyên cục file vật lý
title: str = Form(...), # Nhận các trường text gửi kèm chung trong cái form đó
):
data = await file.read() # Đọc file ra bytes (Ném hết vào RAM)
filename = file.filename # Lấy cái tên file gốc
content_type = file.content_type # Lấy định dạng file (VD: "audio/mpeg")
...
Lưu ý: Một khi đã có dính dáng đến UploadFile, thì mọi thông tin text đi kèm theo nó đều bắt buộc phải khai báo bằng Form(...).
Bí kíp xử lý file dung lượng bự: Đừng dại dột xài file.read() vì nó sẽ ném thẳng cục file vài GB vào RAM gây sập server ngay tắp lự. Hãy stream (hút) từng chút một:
async with aiofiles.open(dest, "wb") as f:
while chunk := await file.read(1024 * 64): # Mỗi lần cắn một miếng 64KB
await f.write(chunk)
6f. Pydantic Validators — Đặt ra luật chơi cho dữ liệu¶
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("Tên bài không được để trống")
return v # Trả về giá trị đã được cạo sạch khoảng trắng (trim)
Nếu hàm validator bên trên văng lỗi ValueError, FastAPI sẽ tự động đứng ra dịch nó thành mã lỗi HTTP 422 và quăng về mặt người dùng.
Nguyên lý phân chia ranh giới sống còn:
| Cái này giao cho Pydantic kiểm tra | Cái này để dành cho Service Layer kiểm tra |
|---|---|
| Kiểu dữ liệu, định dạng chữ (Shape/Format) | Logic cần móc Database ra để đối chiếu |
| "Giá tiền phải lớn hơn 0" | "User ID này không hề tồn tại trong hệ thống" |
| "Tên bài nhạc không được rỗng" | "Tên file này bị trùng với file cũ rồi" |
| "Email phải gõ đúng định dạng @gmail" | "Tài khoản này là User thường, không có quyền admin" |
Đơn giản vì trong Pydantic lấy đâu ra kết nối Database mà đòi query! Tới Bài 5 chúng ta sẽ mổ xẻ kỹ hơn về lớp Service này.
7. Trộn lộn xộn các kiểu Input (Hỗn hợp)¶
Trên thực địa, một API endpoint có thể há miệng nhận cả rổ input thập cẩm thế này:
@router.put("/{item_id}") # ← Path param (Nhìn ngoặc nhọn là biết)
async def update_item(
item_id: int, # ← Lấy từ URL (Path)
verbose: bool = False, # ← Lấy từ URL (Query)
body: ItemUpdate = Body(...), # ← Móc từ JSON Body
session: AsyncSession = Depends(get_session), # ← Tiêm Dependency (Sẽ học ở Bài 3)
user: dict = Depends(require_editor), # ← Tiêm Phân quyền (Sẽ học ở Bài 3)
):
...
Yên tâm, FastAPI đủ độ "khôn ngoan" để tự bóc tách đống bùi nhùi này và phân loại chúng vào đúng chỗ cho bạn.
Bài tập nhỏ khởi động: Xây dựng "Notes API"¶
Mục tiêu: Ôn tập để ngấm kiến thức bài này trước khi lặn sâu hơn.
Yêu cầu tác chiến:
- Setup venv, gõ lệnh cài
fastapi,uvicorn[standard],python-dotenv. - Tạo file
src/config.pyđọc biếnAPP_TITLEtừ môi trường (Nếu không thấy thì mặc định là "My Notes"). - Tạo file
src/main.py, thiết lập FastAPI vớititle=config.APP_TITLEvà gài cái bẫyconfig.validate()vàolifespan. - Tạo một router
/notestrang bị 2 API Thêm và Xem. Tạm thời lưu dữ liệu vào 1 biến Dictionary kiểunotes = {}(Khoan xài DB thật vội). - Tạo Pydantic model
NoteCreatecótitlevàcontent, gài validator bắt buộccontentphải dài từ 10 ký tự trở lên. - Chạy thử bằng
uvicorn, lên/docsvọc vạch test thử thành quả.
Vượt qua bài này, bạn coi như đã làm chủ hoàn toàn khâu setup hạ tầng và thao tác mở cổng API của FastAPI.
Ánh xạ vào Nguyên lý tổng quát (Ôn tập Bài 0)¶
| Kỹ thuật vừa áp dụng | Đại diện cho Nguyên lý nào? | Ý nghĩa thực tiễn mang đi muôn nơi |
|---|---|---|
Gom toàn bộ biến vào config.py |
DRY (Đừng lặp lại chính mình) | Gom kiến thức "Lấy cấu hình ở đâu?" về đúng 1 mối. Sang framework khác như Django có settings.py hay NodeJS có config/index.js cũng dùng chung một bài này. |
Gọi validate() lúc Lifespan khởi động |
Fail Fast (Đứt cầu chì sớm) | Chặn đứng server khởi động nếu phát hiện cấu hình sai lệch ngay từ những giây đầu tiên. |
| Pydantic không ôm đồm việc check DB | SoC (Tách biệt mối quan tâm) | Nó chỉ rà soát hình dáng dữ liệu (Shape validation). Để phần mệt nhọc nghiệp vụ cho Tầng Service giải quyết. |
Tách riêng .env và .env.example |
DRY kết hợp Bảo mật | Mỗi môi trường chạy một cấu hình riêng, không đụng chạm nhau và tuyệt đối không lộ secret lên Github. |
| Chia Router theo từng file riêng biệt | SoC + OCP (Đóng / Mở) | Thêm tính năng = sinh thêm file mới, không chui vào đập phá các file cũ đang chạy yên ổn. |