🐍 Ngày 45 Python 365 ngày | Xây dựng ứng dụng web đầu tiên với Flask
🗓 Ngày 45: Chuyển Python từ terminal lên web
Từ ngày 1 đến 44, mọi thứ bạn viết đều chạy trong terminal. Nhưng để chia sẻ với người khác — cho đồng nghiệp xem dashboard, cho khách hàng tra cứu dữ liệu — bạn cần web. Hôm nay chúng ta bắt đầu với Flask, framework web Python nhỏ gọn và dễ học nhất.
🎯 Mục tiêu bài học
- Hiểu web framework là gì và Flask nằm ở đâu.
- Tạo route, trả HTML và JSON.
- Dùng template Jinja2 để tách giao diện khỏi logic.
- Nhận dữ liệu từ form và query string.
- Dùng session để nhớ trạng thái người dùng.
- Xây một app hoàn chỉnh: Tra cứu tỷ giá crypto.
🧠 Flask là gì và khi nào nên dùng?
Flask là micro-framework: nó cung cấp đúng những gì cần thiết (routing, request/response, template) và để bạn tự chọn phần còn lại.
| Framework | Triết lý | Phù hợp |
|---|---|---|
| Flask | Nhỏ, linh hoạt, tự chọn thư viện | API nhỏ, prototype, microservice, học web |
| Django | Pin kèm mọi thứ (ORM, admin, auth) | Ứng dụng lớn, nhiều CRUD |
| FastAPI | Async, tự sinh tài liệu API | API hiệu năng cao, ML serving |
💡 Quy tắc chọn nhanh: Cần API → FastAPI. Cần web có giao diện + admin → Django. Cần thứ gì đó nhỏ, nhanh, dễ hiểu → Flask. Flask cũng là nền tảng tốt nhất để hiểu web hoạt động ra sao trước khi học hai cái còn lại.
🧰 Cài đặt và tạo dự án
# Tạo môi trường ảo — luôn luôn làm bước này
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # macOS / Linux
pip install flask
Cấu trúc dự án:
crypto_app/
├── app.py
├── requirements.txt
├── templates/
│ ├── base.html
│ ├── index.html
│ └── ket_qua.html
└── static/
└── style.css
🚀 Bước 1: App Flask nhỏ nhất
# app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def trang_chu():
return "<h1>Xin chào từ Flask! 🐍</h1>"
if __name__ == "__main__":
app.run(debug=True)
Chạy:
python app.py
Mở http://127.0.0.1:5000 — bạn vừa có một web server. Chỉ 7 dòng code.
⚙️
debug=Truegiúp tự reload khi sửa code và hiện traceback chi tiết. Tuyệt đối tắt khi deploy — nó cho phép chạy code tùy ý trên server.
🛣️ Bước 2: Route — bản đồ URL của ứng dụng
from flask import Flask, jsonify, request
app = Flask(__name__)
# Route tĩnh
@app.route("/gioi-thieu")
def gioi_thieu():
return "<p>Đây là trang giới thiệu.</p>"
# Route động: <ten> là biến nhận từ URL
@app.route("/chao/<ten>")
def chao(ten):
return f"<h1>Chào {ten}, chúc bạn học Python vui vẻ!</h1>"
# Route nhận số, có ràng buộc kiểu dữ liệu
@app.route("/binh-phuong/<int:so>")
def binh_phuong(so):
return f"{so}² = {so ** 2}"
# Route trả JSON (dùng cho API)
@app.route("/api/trang-thai")
def trang_thai():
return jsonify({"trang_thai": "ok", "phien_ban": "1.0"})
# Nhiều method trên cùng một URL
@app.route("/api/du-lieu", methods=["GET", "POST"])
def du_lieu():
if request.method == "POST":
return jsonify({"da_nhan": request.get_json()})
return jsonify({"method": "GET"})
Quy tắc vàng: tên hàm phải duy nhất trong toàn app, vì Flask dùng tên hàm để sinh URL.
🎨 Bước 3: Template Jinja2 — tách giao diện khỏi logic
Trả HTML bằng chuỗi Python sẽ nhanh chóng biến thành ác mộng. Jinja2 giải quyết việc đó.
templates/base.html — bộ khung dùng chung:
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Crypto App{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<nav>
<a href="{{ url_for('trang_chu') }}">Trang chủ</a> |
<a href="{{ url_for('gioi_thieu') }}">Giới thiệu</a>
</nav>
<main>
{% block noi_dung %}{% endblock %}
</main>
<footer>© 2026 Hướng Nghiệp Python</footer>
</body>
</html>
templates/index.html — kế thừa khung trên:
{% extends "base.html" %}
{% block title %}Tra cứu tỷ giá{% endblock %}
{% block noi_dung %}
<h1>Tra cứu tỷ giá Crypto</h1>
<form method="POST" action="{{ url_for('tra_cuu') }}">
<label for="symbol">Cặp giao dịch:</label>
<input type="text" id="symbol" name="symbol" value="BTCUSDT" required>
<button type="submit">Tra cứu</button>
</form>
{% if loi %}
<p class="loi">⚠️ {{ loi }}</p>
{% endif %}
{% endblock %}
Cập nhật Python:
from flask import Flask, render_template
@app.route("/")
def trang_chu():
return render_template("index.html")
Cú pháp Jinja2 cần nhớ:
| Cú pháp | Ý nghĩa |
|---|---|
{{ bien }} | In giá trị |
{% if %} / {% else %} | Rẽ nhánh |
{% for x in ds %} | Vòng lặp |
{% extends "base.html" %} | Kế thừa template |
{% block ten %} | Vùng có thể ghi đè |
{{ url_for('ten_ham') }} | Sinh URL (không hard-code) |
📝 Bước 4: Nhận dữ liệu từ form
from flask import Flask, render_template, request, jsonify
@app.route("/tra-cuu", methods=["GET", "POST"])
def tra_cuu():
if request.method == "POST":
# Dữ liệu từ <form> nằm trong request.form
symbol = request.form.get("symbol", "").strip().upper()
if not symbol:
return render_template("index.html", loi="Bạn chưa nhập cặp giao dịch.")
# Dữ liệu từ query string ?symbol=BTCUSDT nằm trong request.args
return render_template("ket_qua.html", symbol=symbol, gia=lay_gia(symbol))
return render_template("index.html")
Ba nguồn dữ liệu đầu vào — phân biệt rõ:
| Nguồn | Thuộc tính | Ví dụ |
|---|---|---|
Form (application/x-www-form-urlencoded) | request.form | <input name="symbol"> |
| Query string trên URL | request.args | /tra-cuu?symbol=BTCUSDT |
| JSON body | request.get_json() | fetch() từ JavaScript |
🔌 Bước 5: Gọi API bên ngoài (CoinGecko)
import urllib.request
import json
def lay_gia(symbol: str) -> dict:
"""Lấy giá crypto từ CoinGecko (không cần API key)."""
bang_doi = {"BTCUSDT": "bitcoin", "ETHUSDT": "ethereum", "SOLUSDT": "solana"}
coin_id = bang_doi.get(symbol)
if not coin_id:
return {"loi": f"Chưa hỗ trợ cặp {symbol}"}
url = (
"https://api.coingecko.com/api/v3/simple/price"
f"?ids={coin_id}&vs_currencies=usd&include_24hr_change=true"
)
try:
with urllib.request.urlopen(url, timeout=10) as r:
data = json.loads(r.read().decode())["bitcoin" if coin_id == "bitcoin" else coin_id]
return {
"gia_usd": data["usd"],
"thay_doi_24h": round(data.get("usd_24h_change", 0), 2),
}
except Exception as e:
return {"loi": f"Không lấy được dữ liệu: {e}"}
templates/ket_qua.html:
{% extends "base.html" %}
{% block title %}{{ symbol }} — Kết quả{% endblock %}
{% block noi_dung %}
<h1>Kết quả: {{ symbol }}</h1>
{% if gia.loi %}
<p class="loi">⚠️ {{ gia.loi }}</p>
{% else %}
<table>
<tr><th>Giá (USD)</th><td>${{ "{:,.2f}".format(gia.gia_usd) }}</td></tr>
<tr><th>Thay đổi 24h</th>
<td class="{{ 'xanh' if gia.thay_doi_24h >= 0 else 'do' }}">
{{ "{:+.2f}".format(gia.thay_doi_24h) }}%
</td></tr>
</table>
{% endif %}
<p><a href="{{ url_for('trang_chu') }}">← Tra cứu cặp khác</a></p>
{% endblock %}
🔐 Bước 6: Session — nhớ trạng thái người dùng
HTTP là stateless: mỗi request là độc lập. session là cách Flask lưu dữ liệu nhỏ qua nhiều request.
from flask import session, redirect, url_for
# BẮT BUỘC: khóa bí mật để ký session
app.secret_key = "doi-khoa-nay-trong-thuc-te-va-luu-trong-env"
@app.route("/luu-lich-su", methods=["POST"])
def luu_lich_su():
symbol = request.form.get("symbol", "").upper()
lich_su = session.get("lich_su", [])
lich_su.append(symbol)
session["lich_su"] = lich_su[-5:] # chỉ giữ 5 lần gần nhất
return redirect(url_for("tra_cuu"))
@app.route("/lich-su")
def xem_lich_su():
return jsonify({"lich_su": session.get("lich_su", [])})
⚠️ Session mặc định lưu ở cookie phía client — người dùng có thể đọc nhưng không sửa được (nhờ chữ ký). Không bao giờ lưu mật khẩu hay API key vào session. Với dữ liệu lớn, hãy dùng Redis hoặc database.
🏗️ Bước 7: File hoàn chỉnh
# app.py
import json
import urllib.request
from flask import Flask, render_template, request, jsonify, session, redirect, url_for
app = Flask(__name__)
app.secret_key = "thay-bang-os.environ.get('SECRET_KEY')"
BANG_DOI = {"BTCUSDT": "bitcoin", "ETHUSDT": "ethereum", "SOLUSDT": "solana"}
def lay_gia(symbol: str) -> dict:
coin_id = BANG_DOI.get(symbol)
if not coin_id:
return {"loi": f"Chưa hỗ trợ cặp {symbol}. Thử: {', '.join(BANG_DOI)}"}
url = (
"https://api.coingecko.com/api/v3/simple/price"
f"?ids={coin_id}&vs_currencies=usd&include_24hr_change=true"
)
try:
with urllib.request.urlopen(url, timeout=10) as r:
data = json.loads(r.read().decode())[coin_id]
return {"gia_usd": data["usd"],
"thay_doi_24h": round(data.get("usd_24h_change", 0), 2)}
except Exception as e:
return {"loi": f"Không lấy được dữ liệu: {e}"}
@app.route("/")
def trang_chu():
return render_template("index.html", lich_su=session.get("lich_su", []))
@app.route("/tra-cuu", methods=["POST"])
def tra_cuu():
symbol = request.form.get("symbol", "").strip().upper()
if not symbol:
return render_template("index.html", loi="Bạn chưa nhập cặp giao dịch.")
session["lich_su"] = (session.get("lich_su", []) + [symbol])[-5:]
return render_template("ket_qua.html", symbol=symbol, gia=lay_gia(symbol))
@app.route("/api/gia/<symbol>")
def api_gia(symbol):
"""Endpoint JSON để JavaScript hoặc app khác gọi."""
return jsonify({"symbol": symbol.upper(), **lay_gia(symbol.upper())})
@app.errorhandler(404)
def khong_tim_thay(e):
return render_template("404.html"), 404
if __name__ == "__main__":
app.run(debug=True)
🌐 Bước 8: Deploy lên internet
pip install gunicorn
pip freeze > requirements.txt
Trên server Linux:
gunicorn -w 4 -b 0.0.0.0:8000 app:app
| Nền tảng | Ghi chú |
|---|---|
| Vercel | Cần adapter; phù hợp app nhỏ, serverless |
| Render / Railway | Đơn giản nhất: push Git → tự deploy |
| VPS (Ubuntu) | gunicorn + nginx reverse proxy — kiểm soát hoàn toàn |
Checklist trước khi deploy:
-
debug=False -
secret_keylấy từ biến môi trường - Có file
requirements.txt - Đã bật HTTPS
- Không có key/secret nào trong code
🧪 Bài tập thực hành
- Thêm route
/tin-tuchiển thị danh sách 5 bài viết từ một list Python. - Tạo trang
/so-sanhcho phép nhập nhiều cặp cùng lúc và hiển thị bảng so sánh. - Viết endpoint
/api/lich-sutrả JSON lịch sử tra cứu. - Dùng
@app.template_filtertạo filtervndđể hiển thị1234.5thành1.234,50 ₫. - Thêm trang 404 tùy chỉnh.
⚠️ Lỗi thường gặp
| Lỗi | Nguyên nhân | Cách sửa |
|---|---|---|
TemplateNotFound | Sai thư mục | Template phải nằm trong templates/ |
BuildError: Could not build url for endpoint | Tên hàm trong url_for sai | Khớp đúng tên hàm Python |
RuntimeError: Working outside of request context | Dùng request ngoài route | Truyền dữ liệu qua tham số |
session không lưu | Thiếu secret_key | Đặt app.secret_key |
Address already in use | Port 5000 bị chiếm | app.run(port=5001) |
| Trang trắng, không lỗi | Template render nhưng block rỗng | Kiểm tra {% block %} khớp tên |
📝 Ghi chú
- Flask không có sẵn ORM. Khi cần database, bạn thêm SQLAlchemy (hoặc Flask-SQLAlchemy).
url_forluôn tốt hơn hard-code URL. Đổi route chỉ cần sửa một chỗ.- Tách logic khỏi route. Hàm
lay_gia()ở trên nên nằm trong module riêng khi app lớn lên. - Blueprint là cách Flask chia app lớn thành nhiều module — học ở bài sau khi bạn đã quen.
🎯 Tóm tắt ngày 45
✅ Hiểu Flask và biết khi nào chọn Flask/Django/FastAPI
✅ Tạo route tĩnh, động, có ràng buộc kiểu
✅ Dùng Jinja2 với extends/block để tái sử dụng giao diện
✅ Nhận dữ liệu từ form, query string, JSON
✅ Lưu trạng thái bằng session
✅ Deploy với gunicorn