Chuyển tới nội dung chính

Lộ trình học Python để trở thành Backend Developer

· 26 phút để đọc

Backend developer là người xây dựng "phần chìm của tảng băng": API, database, xác thực, xử lý nghiệp vụ — tất cả những gì người dùng không nhìn thấy nhưng nếu hỏng thì cả sản phẩm sập.

Python là một trong những lựa chọn tốt nhất để bắt đầu, nhờ cú pháp rõ ràng và ba framework trưởng thành: Flask, Django, FastAPI.

Bài viết này là lộ trình 12 tháng thực tế, không phải danh sách từ khoá. Mỗi giai đoạn đều có: cần học gì, code mẫu, dự án phải làm, và tiêu chí để biết đã sẵn sàng bước tiếp.


🗺️ Bức tranh toàn cảnh​

Giai đoạn 1 (tuần 1–8)    Python nền tảng + OOP
Giai đoạn 2 (tuần 9–12) Git + Linux + công cụ
Giai đoạn 3 (tuần 13–20) Web cơ bản + Flask
Giai đoạn 4 (tuần 21–28) Database + SQL + ORM
Giai đoạn 5 (tuần 29–36) API chuyên nghiệp + Auth + Testing
Giai đoạn 6 (tuần 37–44) Django hoặc FastAPI (chọn 1)
Giai đoạn 7 (tuần 45–48) Docker + Deploy + CI/CD
Giai đoạn 8 (tuần 49–52) Dự án tổng hợp + phỏng vấn

Backend developer thực sự làm gì mỗi ngày?

Hoạt độngTỷ lệ thời gian
Viết code tính năng mới30%
Đọc và hiểu code cũ20%
Debug và sửa lỗi20%
Review code của đồng nghiệp10%
Viết test10%
Họp, trao đổi thiết kế, tài liệu10%

💡 Chú ý: chỉ 30% thời gian là "viết code mới". Vì vậy đọc code và debug là kỹ năng quan trọng ngang viết code.


🧱 Giai đoạn 1 (tuần 1–8): Python nền tảng và OOP​

Nội dung​

TuầnChủ đềKỹ năng đạt được
1Biến, kiểu dữ liệu, toán tử, input/printViết script tính toán
2if/elif/else, vòng lặpXử lý logic
3List, tuple, dict, setChọn cấu trúc dữ liệu đúng
4Hàm, tham số, *args/**kwargs, scopeChia nhỏ chương trình
5Xử lý file, CSV, JSON, pathlibĐọc/ghi dữ liệu
6Ngoại lệ, try/except/else/finallyCode không crash
7OOP: class, kế thừa, magic methodsThiết kế đối tượng
8Module, package, virtualenv, pipTổ chức dự án

Điều backend developer phải nắm chắc ở giai đoạn này​

1. Chọn đúng cấu trúc dữ liệu — đây là kỹ năng bị đánh giá thấp nhất:

Cấu trúcTruy cậpThêm/XoáDùng khi
listO(1) theo indexO(n) ở đầu, O(1) ở cuốiDữ liệu có thứ tự
dictO(1) theo keyO(1)Tra cứu theo khoá
setO(1) kiểm tra tồn tạiO(1)Loại trùng, kiểm tra membership
tupleO(1)Không đổiDữ liệu bất biến
# ❌ Tìm kiếm trong list — O(n), chậm khi dữ liệu lớn
danh_sach_email = ["a@x.com", "b@x.com", "c@x.com"]
if "c@x.com" in danh_sach_email: # quét từng phần tử
...

# ✅ Dùng set — O(1)
tap_email = {"a@x.com", "b@x.com", "c@x.com"}
if "c@x.com" in tap_email: # tra trực tiếp
...

Với 1 triệu bản ghi, khác biệt này là giây vs micro-giây.

2. try/except đúng cách — không nuốt lỗi:

# ❌ Bắt mọi lỗi, che giấu bug thật
try:
ket_qua = xu_ly_du_lieu(duong_dan)
except Exception:
pass

# ✅ Bắt lỗi cụ thể, log lại, xử lý đúng cách
import logging

logger = logging.getLogger(__name__)

try:
with open(duong_dan, encoding="utf-8") as f:
ket_qua = xu_ly(f.read())
except FileNotFoundError:
logger.warning("Không tìm thấy file %s, dùng cấu hình mặc định", duong_dan)
ket_qua = CAU_HINH_MAC_DINH
except json.JSONDecodeError as e:
logger.error("File %s không phải JSON hợp lệ: %s", duong_dan, e)
raise

3. OOP dùng cho việc thật, không phải class Animal:

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from decimal import Decimal


class PhuongThucThanhToan(ABC):
"""Interface cho mọi phương thức thanh toán."""

@abstractmethod
def thanh_toan(self, so_tien: Decimal) -> bool:
...

@property
@abstractmethod
def ten(self) -> str:
...


@dataclass
class ThanhToanThe(PhuongThucThanhToan):
so_the: str
han_dung: str

@property
def ten(self) -> str:
return f"Thẻ ****{self.so_the[-4:]}"

def thanh_toan(self, so_tien: Decimal) -> bool:
if so_tien <= 0:
raise ValueError("Số tiền phải dương")
# gọi cổng thanh toán...
return True


@dataclass
class ThanhToanViDienTu(PhuongThucThanhToan):
ma_vi: str
so_du: Decimal = field(default=Decimal("0"))

@property
def ten(self) -> str:
return f"Ví {self.ma_vi}"

def thanh_toan(self, so_tien: Decimal) -> bool:
if self.so_du < so_tien:
return False
self.so_du -= so_tien
return True


for pt in [ThanhToanThe("4111111111111234", "12/28"),
ThanhToanViDienTu("VI-001", Decimal("500000"))]:
ok = pt.thanh_toan(Decimal("200000"))
print(f"{pt.ten}: {'✅ thành công' if ok else '❌ thất bại'}")

Ở đây bạn học: ABC, @abstractmethod, @dataclass, Decimal cho tiền, @property — tất cả đều dùng hàng ngày ở backend.

Dự án giai đoạn 1​

Công cụ dòng lệnh quản lý chi tiêu — lưu vào JSON:

import json
from pathlib import Path
from datetime import date
from decimal import Decimal

FILE = Path("chi-tieu.json")


def doc_du_lieu() -> list[dict]:
if not FILE.exists():
return []
return json.loads(FILE.read_text(encoding="utf-8"))


def luu_du_lieu(ds: list[dict]) -> None:
FILE.write_text(json.dumps(ds, ensure_ascii=False, indent=2), encoding="utf-8")


def them(so_tien: str, danh_muc: str, ghi_chu: str = "") -> None:
ds = doc_du_lieu()
ds.append({
"ngay": date.today().isoformat(),
"so_tien": str(Decimal(so_tien)),
"danh_muc": danh_muc,
"ghi_chu": ghi_chu,
})
luu_du_lieu(ds)
print(f"✅ Đã thêm {so_tien} cho {danh_muc}")


def bao_cao_theo_danh_muc() -> None:
ds = doc_du_lieu()
if not ds:
print("Chưa có dữ liệu.")
return
tong: dict[str, Decimal] = {}
for item in ds:
tong[item["danh_muc"]] = tong.get(item["danh_muc"], Decimal(0)) + Decimal(item["so_tien"])
for danh_muc, so in sorted(tong.items(), key=lambda x: -x[1]):
print(f"{danh_muc:<16} {so:>12,.0f} đ")
print(f"{'TỔNG':<16} {sum(tong.values()):>12,.0f} đ")


if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("Dùng: python chi_tieu.py them <số_tiền> <danh_mục>")
print(" python chi_tieu.py bao-cao")
sys.exit(1)

lenh = sys.argv[1]
if lenh == "them":
them(sys.argv[2], sys.argv[3], " ".join(sys.argv[4:]))
elif lenh == "bao-cao":
bao_cao_theo_danh_muc()
else:
print(f"Lệnh không hợp lệ: {lenh}")

Tiêu chí hoàn thành giai đoạn 1​

  • Viết được chương trình 150+ dòng chia thành nhiều hàm, không cần tra cú pháp
  • Giải thích được khi nào dùng list vs dict vs set
  • Dùng try/except đúng chỗ, không nuốt lỗi
  • Biết tạo venv và cài package

🛠️ Giai đoạn 2 (tuần 9–12): Git, Linux và công cụ​

Nhiều người bỏ qua giai đoạn này và trả giá suốt sự nghiệp.

Git — bắt buộc​

# Cấu hình lần đầu
git config --global user.name "Tên Bạn"
git config --global user.email "email@example.com"

# Workflow cơ bản hàng ngày
git status
git add .
git commit -m "feat: thêm chức năng đăng nhập"
git push origin main

# Làm việc nhánh — KỸ NĂNG QUAN TRỌNG NHẤT
git checkout -b feature/them-dang-nhap
# ... code ...
git commit -m "feat: hoàn thiện đăng nhập"
git push origin feature/them-dang-nhap
# rồi mở Pull Request trên GitHub

Conventional Commits — quy ước commit mà mọi team chuyên nghiệp dùng:

Tiền tốÝ nghĩa
feat:Thêm tính năng
fix:Sửa bug
docs:Sửa tài liệu
refactor:Tái cấu trúc, không đổi hành vi
test:Thêm/sửa test
chore:Việc lặt vặt (deps, config)

Viết commit rõ ràng giúp bạn được đánh giá cao hơn ngay từ ngày đầu đi làm.

Linux / command line cơ bản​

LệnhCông dụng
ls -laLiệt kê file, gồm file ẩn
cd, pwdDi chuyển, xem thư mục hiện tại
grep -rn "text" .Tìm text trong toàn bộ thư mục
find . -name "*.py"Tìm file theo mẫu
tail -f app.logXem log đang chạy (dùng hàng ngày!)
ps aux | grep pythonXem tiến trình Python
chmod +x script.shCấp quyền chạy
ssh user@serverKết nối server
curl -X POST ...Test API từ terminal

Công cụ nên cài​

Công cụVai trò
VS Code hoặc PyCharmIDE
Postman / InsomniaTest API trực quan
TablePlus / DBeaverXem database
Docker DesktopChạy database local dễ dàng

🌐 Giai đoạn 3 (tuần 13–20): Web hoạt động thế nào + Flask​

Kiến thức web bắt buộc​

HTTP request/response — nền tảng của mọi thứ:

GET /api/san-pham?page=2 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGc...
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json

{"data": [...], "page": 2, "total": 47}

Bảng mã trạng thái — phải thuộc lòng:

NhómMãÝ nghĩaKhi nào dùng
2xx200OKGET/PUT thành công
2xx201CreatedPOST tạo mới thành công
2xx204No ContentDELETE thành công
4xx400Bad RequestDữ liệu gửi lên sai định dạng
4xx401UnauthorizedChưa đăng nhập
4xx403ForbiddenĐã đăng nhập nhưng không có quyền
4xx404Not FoundKhông tìm thấy tài nguyên
4xx409ConflictTrùng dữ liệu (email đã tồn tại)
4xx422UnprocessableDữ liệu đúng định dạng nhưng sai logic
4xx429Too Many RequestsVượt rate limit
5xx500Internal Server ErrorLỗi phía server (bug)
5xx503Service UnavailableServer quá tải/bảo trì

⚠️ Lỗi kinh điển của người mới: trả về 200 OK kèm {"success": false, "error": "..."}. Điều này phá vỡ mọi thư viện client. Hãy dùng đúng mã trạng thái.

Flask — hiểu web từ gốc​

from flask import Flask, jsonify, request, abort

app = Flask(__name__)

# "Database" trong bộ nhớ
SACH = {
1: {"id": 1, "ten": "Lập trình Python", "gia": 250000},
2: {"id": 2, "ten": "Cấu trúc dữ liệu", "gia": 180000},
}
_bo_dem = 2


@app.get("/api/sach")
def danh_sach_sach():
return jsonify(list(SACH.values()))


@app.get("/api/sach/<int:sach_id>")
def chi_tiet_sach(sach_id):
sach = SACH.get(sach_id)
if sach is None:
abort(404, description=f"Không tìm thấy sách id={sach_id}")
return jsonify(sach)


@app.post("/api/sach")
def them_sach():
global _bo_dem
du_lieu = request.get_json(silent=True) or {}

if not du_lieu.get("ten"):
abort(400, description="Thiếu trường 'ten'")
if not isinstance(du_lieu.get("gia"), int) or du_lieu["gia"] <= 0:
abort(400, description="'gia' phải là số nguyên dương")

_bo_dem += 1
SACH[_bo_dem] = {"id": _bo_dem, **du_lieu}
return jsonify(SACH[_bo_dem]), 201


@app.put("/api/sach/<int:sach_id>")
def cap_nhat_sach(sach_id):
if sach_id not in SACH:
abort(404)
du_lieu = request.get_json(silent=True) or {}
SACH[sach_id] = {"id": sach_id, **du_lieu}
return jsonify(SACH[sach_id])


@app.delete("/api/sach/<int:sach_id>")
def xoa_sach(sach_id):
if SACH.pop(sach_id, None) is None:
abort(404)
return "", 204


@app.errorhandler(404)
def khong_tim_thay(e):
return jsonify({"loi": str(e.description)}), 404


@app.errorhandler(400)
def du_lieu_sai(e):
return jsonify({"loi": str(e.description)}), 400

Đọc kỹ đoạn này — nó chứa gần hết những gì cần biết về REST API cơ bản: routing, path param, validate, mã trạng thái, error handler.

Dự án giai đoạn 3​

API quản lý thư viện — CRUD đầy đủ cho 2 bảng (sách, người mượn), có validate và error handler. Đây là dự án bạn sẽ nâng cấp qua các giai đoạn sau.


🗄️ Giai đoạn 4 (tuần 21–28): Database và ORM​

SQL — không thể bỏ qua​

Cho dù dùng ORM, bạn phải hiểu SQL. Không hiểu SQL nghĩa là không debug được query chậm.

-- Tạo bảng với ràng buộc đầy đủ
CREATE TABLE tac_gia (
id SERIAL PRIMARY KEY,
ten VARCHAR(120) NOT NULL,
email VARCHAR(120) UNIQUE NOT NULL
);

CREATE TABLE sach (
id SERIAL PRIMARY KEY,
ten VARCHAR(200) NOT NULL,
gia NUMERIC(12,0) NOT NULL CHECK (gia > 0),
tac_gia_id INTEGER NOT NULL REFERENCES tac_gia(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW()
);

-- INDEX — thứ quyết định query nhanh hay chậm
CREATE INDEX idx_sach_tac_gia ON sach(tac_gia_id);

-- JOIN + GROUP BY + HAVING
SELECT t.ten,
COUNT(s.id) AS so_sach,
SUM(s.gia) AS tong_gia_tri
FROM tac_gia t
LEFT JOIN sach s ON s.tac_gia_id = t.id
GROUP BY t.id, t.ten
HAVING COUNT(s.id) > 1
ORDER BY tong_gia_tri DESC
LIMIT 10;

Bốn khái niệm SQL quan trọng nhất:

Khái niệmÝ nghĩaVì sao quan trọng
INDEXCấu trúc tăng tốc tra cứuQuery 5 giây → 5 mili-giây
JOINKết hợp nhiều bảng90% query thật cần JOIN
TRANSACTIONNhóm thao tác "tất cả hoặc không gì cả"Chuyển tiền: trừ A và cộng B phải cùng thành công
EXPLAINXem kế hoạch thực thi queryCông cụ debug hiệu năng số 1
-- Luôn dùng EXPLAIN khi query chậm
EXPLAIN ANALYZE SELECT * FROM sach WHERE tac_gia_id = 5;

ORM với SQLAlchemy​

from datetime import datetime
from decimal import Decimal
from sqlalchemy import String, Numeric, ForeignKey, DateTime, func, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session


class Base(DeclarativeBase):
pass


class TacGia(Base):
__tablename__ = "tac_gia"

id: Mapped[int] = mapped_column(primary_key=True)
ten: Mapped[str] = mapped_column(String(120))
email: Mapped[str] = mapped_column(String(120), unique=True)

sach: Mapped[list["Sach"]] = relationship(back_populates="tac_gia", cascade="all, delete-orphan")

def __repr__(self) -> str:
return f"TacGia(id={self.id}, ten={self.ten!r})"


class Sach(Base):
__tablename__ = "sach"

id: Mapped[int] = mapped_column(primary_key=True)
ten: Mapped[str] = mapped_column(String(200))
gia: Mapped[Decimal] = mapped_column(Numeric(12, 0))
tac_gia_id: Mapped[int] = mapped_column(ForeignKey("tac_gia.id"))
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())

tac_gia: Mapped[TacGia] = relationship(back_populates="sach")


# Sử dụng
with Session(engine) as session:
tg = TacGia(ten="Nguyễn Văn A", email="a@example.com")
tg.sach = [Sach(ten="Python cơ bản", gia=Decimal("250000"))]
session.add(tg)
session.commit()

# Truy vấn với eager loading — tránh N+1 query
stmt = (
select(TacGia)
.join(TacGia.sach)
.where(Sach.gia > 200000)
.options(selectinload(TacGia.sach))
)
for tac_gia in session.scalars(stmt):
print(tac_gia.ten, [s.ten for s in tac_gia.sach])

🔥 Vấn đề N+1 query là lỗi hiệu năng phổ biến nhất với ORM. Nếu bạn lặp qua 100 tác giả và mỗi lần truy cập tac_gia.sach lại phát sinh 1 query → 101 query. selectinload/joinedload giải quyết việc này.

Dự án giai đoạn 4​

Nâng cấp API thư viện: chuyển từ dict trong bộ nhớ sang PostgreSQL + SQLAlchemy, thêm relationship, thêm index, viết 5 query phức tạp có JOIN + GROUP BY.


🔐 Giai đoạn 5 (tuần 29–36): API chuyên nghiệp​

Xác thực với JWT​

from datetime import datetime, timedelta, timezone
from passlib.context import CryptContext
import jwt

SECRET = "đọc-từ-biến-môi-trường-trong-thực-tế"
THUAT_TOAN = "HS256"
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")


def bam_mat_khau(mat_khau: str) -> str:
return pwd_context.hash(mat_khau)


def kiem_tra_mat_khau(mat_khau: str, da_bam: str) -> bool:
return pwd_context.verify(mat_khau, da_bam)


def tao_token(user_id: int, vai_tro: str = "user") -> str:
payload = {
"sub": str(user_id),
"vai_tro": vai_tro,
"exp": datetime.now(timezone.utc) + timedelta(hours=24),
"iat": datetime.now(timezone.utc),
}
return jwt.encode(payload, SECRET, algorithm=THUAT_TOAN)


def giai_ma_token(token: str) -> dict:
return jwt.decode(token, SECRET, algorithms=[THUAT_TOAN])

Ba quy tắc bảo mật không được vi phạm:

  1. Không bao giờ lưu mật khẩu dạng thô — luôn băm bằng bcrypt/argon2 (không dùng MD5/SHA1).
  2. Không bao giờ để secret trong code — dùng biến môi trường.
  3. Luôn kiểm tra quyền — xác thực (bạn là ai) khác uỷ quyền (bạn được làm gì).

Phân quyền​

from functools import wraps
from flask import request, jsonify, g


def yeu_cau_dang_nhap(f):
@wraps(f)
def wrapper(*args, **kwargs):
header = request.headers.get("Authorization", "")
if not header.startswith("Bearer "):
return jsonify({"loi": "Thiếu token"}), 401
try:
g.nguoi_dung = giai_ma_token(header[7:])
except jwt.ExpiredSignatureError:
return jsonify({"loi": "Token đã hết hạn"}), 401
except jwt.InvalidTokenError:
return jsonify({"loi": "Token không hợp lệ"}), 401
return f(*args, **kwargs)
return wrapper


def yeu_cau_quyen(vai_tro_can: str):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
if g.nguoi_dung.get("vai_tro") != vai_tro_can:
return jsonify({"loi": "Không có quyền truy cập"}), 403
return f(*args, **kwargs)
return wrapper
return decorator


@app.delete("/api/sach/<int:sach_id>")
@yeu_cau_dang_nhap
@yeu_cau_quyen("admin")
def xoa_sach(sach_id):
...

Validation với Pydantic​

from pydantic import BaseModel, Field, EmailStr, field_validator
from decimal import Decimal


class SachVao(BaseModel):
ten: str = Field(min_length=1, max_length=200)
gia: Decimal = Field(gt=0, le=100_000_000)
tac_gia_email: EmailStr

@field_validator("ten")
@classmethod
def chuan_hoa_ten(cls, v: str) -> str:
return " ".join(v.split())


sach = SachVao.model_validate({"ten": " Python cơ bản ", "gia": "250000",
"tac_gia_email": "a@example.com"})
print(sach.ten) # "Python cơ bản" (đã chuẩn hoá)

Testing — kỹ năng phân biệt junior và mid​

import pytest
from app import app, SACH


@pytest.fixture
def client():
app.config["TESTING"] = True
with app.test_client() as c:
yield c


@pytest.fixture(autouse=True)
def reset_du_lieu():
"""Mỗi test chạy trên dữ liệu sạch."""
goc = SACH.copy()
yield
SACH.clear()
SACH.update(goc)


def test_lay_danh_sach(client):
r = client.get("/api/sach")
assert r.status_code == 200
assert len(r.get_json()) == 2


def test_lay_sach_khong_ton_tai(client):
r = client.get("/api/sach/9999")
assert r.status_code == 404
assert "loi" in r.get_json()


def test_them_sach_thieu_ten(client):
r = client.post("/api/sach", json={"gia": 100000})
assert r.status_code == 400


def test_them_sach_thanh_cong(client):
r = client.post("/api/sach", json={"ten": "Sách mới", "gia": 99000})
assert r.status_code == 201
assert r.get_json()["ten"] == "Sách mới"


@pytest.mark.parametrize("gia", [0, -100, "abc", None])
def test_them_sach_gia_khong_hop_le(client, gia):
r = client.post("/api/sach", json={"ten": "X", "gia": gia})
assert r.status_code == 400

Chạy: pytest -v --cov=app để xem độ phủ test.


🚀 Giai đoạn 6 (tuần 37–44): Chọn Django hoặc FastAPI​

Sau khi đã hiểu Flask và web từ gốc, hãy chọn một hướng chuyên sâu.

Chọn Django nếu bạn muốn​

  • Làm sản phẩm hoàn chỉnh nhanh: có admin, auth, ORM, form sẵn.
  • Làm việc ở công ty có nhiều dự án web truyền thống.
  • Cần CMS, ERP, hệ thống quản lý nội bộ.

Điểm mạnh độc nhất: Django Admin cho bạn giao diện quản trị hoàn chỉnh miễn phí.

Chọn FastAPI nếu bạn muốn​

  • Làm API cho mobile/frontend hoặc phục vụ model ML.
  • Cần hiệu năng cao, xử lý nhiều request đồng thời.
  • Thích code hiện đại với type hints và tự sinh tài liệu.

Điểm mạnh độc nhất: tài liệu Swagger tự sinh — bạn giao API cho team frontend và họ tự đọc.

Nếu chưa quyết được: học FastAPI trước (nhỏ hơn, học nhanh), rồi học Django sau (dễ hơn nhiều khi đã nắm web).


🐳 Giai đoạn 7 (tuần 45–48): Docker, deploy, CI/CD​

Docker — đóng gói để "chạy được ở mọi máy"​

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential libpq-dev \
&& rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN useradd --create-home appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "4"]
# docker-compose.yml — chạy app + database cùng lúc
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: mydb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s

api:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://app:secret@db:5432/mydb
depends_on:
db:
condition: service_healthy

volumes:
pgdata:
docker compose up --build     # chạy toàn bộ hệ thống bằng 1 lệnh

CI/CD với GitHub Actions​

name: CI

on:
push:
branches: [main]
pull_request:

jobs:
kiem-tra:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 5

steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Cài phụ thuộc
run: pip install -r requirements.txt -r requirements-dev.txt

- name: Kiểm tra định dạng
run: ruff check .

- name: Kiểm tra kiểu
run: mypy app

- name: Chạy test
run: pytest -v --cov=app --cov-report=term-missing
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/testdb

- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .

Checklist bảo mật trước khi deploy​

  • DEBUG = False trên production
  • Mọi secret đọc từ biến môi trường
  • HTTPS bắt buộc
  • CORS chỉ cho domain cụ thể (không *)
  • Rate limiting trên endpoint đăng nhập
  • Validate toàn bộ input, dùng tham số hoá SQL
  • Có logging và health check
  • Backup database tự động

🎯 Giai đoạn 8 (tuần 49–52): Dự án tổng hợp và phỏng vấn​

Dự án tổng hợp — chọn 1, làm thật sâu​

Ý tưởngCông nghệĐiểm mạnh khi phỏng vấn
Nền tảng blog có APIFastAPI + PostgreSQL + JWT + DockerThể hiện mọi kỹ năng
Hệ thống đặt hàngDjango + DRF + Celery + RedisThể hiện xử lý nghiệp vụ phức tạp
API phân tích dữ liệu công khaiFastAPI + pandas + cacheThể hiện xử lý dữ liệu + hiệu năng
Hệ thống quản lý nội bộDjango + Admin + PostgreSQLGần với công việc thật nhất

Checklist dự án đủ tốt:

  • Có README với ảnh chụp màn hình
  • Có Docker, chạy được bằng 1 lệnh
  • Có test với coverage > 70%
  • Có CI chạy tự động
  • Có tài liệu API (Swagger/OpenAPI)
  • Có deploy thật, có link truy cập được
  • Có migration database
  • Có xử lý lỗi và logging tử tế

Bộ câu hỏi phỏng vấn backend Python​

Python:

  1. List comprehension và generator expression khác nhau thế nào?
  2. *args và **kwargs — dùng khi nào?
  3. Vì sao def f(x=[]) là lỗi? Giải thích cơ chế.
  4. Shallow copy và deep copy — phân biệt và ví dụ.
  5. Decorator hoạt động ra sao? Viết decorator đo thời gian.
  6. GIL ảnh hưởng gì tới đa luồng? Khi nào dùng multiprocessing?
  7. __slots__ dùng để làm gì?
  8. Context manager là gì? Viết một cái.

Web & API:

  1. Khi nào dùng PUT và khi nào dùng PATCH?
  2. Làm sao chống spam/brute-force cho endpoint đăng nhập?
  3. Xử lý file upload lớn như thế nào?
  4. Idempotency trong API là gì? Vì sao quan trọng với thanh toán?
  5. CORS là gì? Vì sao browser chặn request?
  6. Làm sao versioning API?

Database:

  1. Index hoạt động thế nào? Khi nào index làm chậm?
  2. Transaction isolation level — có mấy mức?
  3. N+1 query là gì và cách phát hiện?
  4. Vì sao dùng connection pool?
  5. Optimistic vs pessimistic locking?

Kiến trúc:

  1. Khi nào cần cache? Cache invalidation ra sao?
  2. Xử lý tác vụ nặng (gửi email, xử lý ảnh) — dùng gì?
  3. Làm sao scale ứng dụng khi có 1 triệu người dùng?
  4. Monolith vs microservices — khi nào chọn cái nào?

📖 Kỹ năng đọc code — thứ không ai dạy bạn​

Trường học dạy bạn viết code. Không ai dạy bạn đọc code — nhưng đó là 50% công việc thật.

Khi vào công ty, việc đầu tiên của bạn gần như chắc chắn là: "Đọc codebase này đi, rồi sửa một bug nhỏ." Codebase có thể 50.000 dòng, do 20 người viết trong 5 năm, không có tài liệu.

Cách đọc một codebase lạ​

Bước 1 — Chạy được nó trước. Đừng đọc. Hãy chạy:

git clone <repo>
cd <repo>
python -m venv .venv && .venv\Scripts\activate
pip install -r requirements.txt
# đọc README để biết cách chạy
pytest

Nếu test chạy được, bạn đã có "lưới an toàn": sửa sai sẽ bị test phát hiện.

Bước 2 — Tìm điểm vào (entry point). Với web app, đó là nơi route được đăng ký:

# Tìm tất cả route
grep -rn "@app.route\|@app.get\|@app.post" app/
grep -rn "urlpatterns\|APIRouter" .

Bước 3 — Đi theo một luồng duy nhất. Đừng đọc lan man. Chọn một API endpoint, rồi lần theo:

Request → Route → Validation → Business logic → Database → Response

Ví dụ với POST /api/don-hang:

  1. Route nằm ở file nào?
  2. Hàm route gọi hàm nào tiếp?
  3. Dữ liệu được validate ở đâu?
  4. Logic nghiệp vụ nằm ở service nào, hay nằm luôn trong route?
  5. Truy vấn database nằm ở đâu?

Chỉ cần làm điều này với 3–5 endpoint là bạn đã hiểu kiến trúc dự án.

Bước 4 — Vẽ sơ đồ. Vẽ ra giấy cấu trúc thư mục và luồng dữ liệu. Nghe có vẻ trẻ con, nhưng nó giúp bạn nhớ và giúp bạn đặt câu hỏi đúng khi hỏi đồng nghiệp.

Bước 5 — Sửa bug nhỏ đầu tiên. Chọn bug nhỏ nhất trong issue tracker. Sửa nó, viết test cho nó, mở Pull Request. Đây là cách nhanh nhất để được coi là thành viên thật của team.

Đọc source của thư viện bạn dùng​

Đây là cách học nhanh nhất mà ít người làm. Khi bạn không hiểu Flask hoặc FastAPI xử lý một thứ gì đó, hãy mở source:

# Xem source ngay trong terminal
python -c "import flask, inspect; print(inspect.getsourcefile(flask))"

# Hoặc trong Python
import requests, inspect
print(inspect.getsource(requests.Session.request))

Đọc code của thư viện nổi tiếng giúp bạn thấy cách người giỏi viết code: cách họ đặt tên, cách họ xử lý lỗi, cách họ tổ chức module.


🧪 Cách luyện tập hiệu quả trong 12 tháng​

Quy tắc 70/20/10​

Tỷ lệHoạt độngGhi chú
70%Tự viết code, làm dự ánPhần quan trọng nhất
20%Đọc code người khác, đọc docs/sourceHọc từ người giỏi
10%Xem video, đọc sách, học lý thuyếtChỉ để gợi mở

Hầu hết người mới làm ngược lại: 80% xem video, 20% gõ code. Đó là lý do họ học mãi không tiến bộ.

Cách luyện "không copy-paste"​

Cách duy nhất để thực sự nhớ:

  1. Đọc ví dụ trong tài liệu.
  2. Đóng tài liệu lại.
  3. Viết lại từ đầu, không nhìn.
  4. Bí quá thì mở ra xem 5 giây rồi đóng lại.
  5. Sau khi chạy được, so sánh code bạn viết với code mẫu — khác chỗ nào, vì sao?

Cách này đau đớn hơn nhiều so với copy-paste. Nhưng nó là sự khác biệt giữa "biết" và "biết làm".

Lịch luyện tập mẫu theo tuần​

NgàyThời lượngNội dung
Thứ 290 phútHọc khái niệm mới + bài tập
Thứ 390 phútLuyện thuật toán (LeetCode easy)
Thứ 490 phútLàm dự án cá nhân
Thứ 590 phútĐọc code người khác hoặc đọc source thư viện
Thứ 690 phútLàm dự án cá nhân
Thứ 7120 phútHoàn thiện dự án + viết README
Chủ nhậtNghỉNão cần thời gian củng cố

Tổng: khoảng 10–11 giờ/tuần. Con số này thực tế hơn nhiều so với kế hoạch "học 4 giờ mỗi ngày" mà không ai duy trì được quá 2 tuần.

Ba câu hỏi tự kiểm tra mỗi tuần​

  1. Tuần này tôi viết được bao nhiêu dòng code tự nghĩ, không copy?
  2. Tôi có thể giải thích thứ tôi vừa học cho người khác không?
  3. Dự án tôi đang làm có tiến triển nhìn thấy được không?

Nếu cả ba câu đều "không", bạn đang lãng phí thời gian — hãy đổi cách học.


⚠️ 8 sai lầm khiến bạn tốn thêm 1 năm​

  1. Học 3 framework cùng lúc. Chọn 1, đi đến nơi đến chốn.
  2. Không học SQL vì "đã có ORM". ORM không cứu bạn khi query chạy 30 giây.
  3. Không viết test. Bạn sẽ không được nhận vào vị trí mid/senior.
  4. Không học Docker. Mọi team chuyên nghiệp đều dùng.
  5. Chỉ làm dự án todo list. Nhà tuyển dụng đã thấy 1000 cái như thế.
  6. Không đọc code của người khác. Đọc Django/FastAPI source dạy bạn nhiều hơn tutorial.
  7. Học mà không deploy. Dự án không deploy = dự án chưa xong.
  8. Không chuẩn bị phỏng vấn. Biết code mà không diễn đạt được thì vẫn trượt.

❓ Câu hỏi thường gặp​

Backend Python có còn dễ xin việc không? Có, đặc biệt ở mảng API, data platform, automation. Nhu cầu cao nhưng yêu cầu cũng cao hơn 5 năm trước — biết CRUD cơ bản không còn đủ, cần Docker, testing, cloud.

Cần học frontend không? Không cần thành thạo, nhưng hiểu HTML/CSS/JS cơ bản giúp bạn phối hợp với frontend tốt hơn nhiều. Biết React là lợi thế lớn.

Có cần bằng đại học? Không bắt buộc, nhưng nếu không có, portfolio và kinh nghiệm phải thật nổi bật.

Bao lâu để thành mid-level? Thường 2–3 năm làm việc thật. Học 12 tháng đưa bạn tới junior; từ junior lên mid cần kinh nghiệm dự án thật, không phải học thêm khoá học.

Nên học async Python không? Có, nhưng chỉ sau khi vững sync. Hiểu async/await và khi nào không nên dùng async quan trọng hơn là biết cú pháp.


🎯 Tóm lại​

Lộ trình backend Python trong 12 tháng:

8 tuần  → Python + OOP vững
4 tuần → Git + Linux + công cụ
8 tuần → Web + Flask
8 tuần → Database + SQL + ORM
8 tuần → API chuyên nghiệp + Auth + Test
8 tuần → Django HOẶC FastAPI
4 tuần → Docker + Deploy + CI/CD
4 tuần → Dự án tổng hợp + phỏng vấn

Điều duy nhất quyết định thành công không phải là lộ trình này, mà là bạn có gõ code mỗi ngày hay không.


📚 Bài viết liên quan​