Flask không áp đặt cấu trúc, nên khi ứng dụng lớn dần bạn phải tự tổ chức. Blueprint và mẫu application factory là hai công cụ giúp dự án Flask không biến thành một file khổng lồ.
Vấn đề của cách viết đơn giản
app = Flask(__name__)
@app.route("/")
def trang_chu(): ...
@app.route("/bai-viet")
def danh_sach(): ...
# ... 50 route nữa trong cùng một file
Cách này còn gây khó khi viết kiểm thử: đối tượng app được tạo ngay lúc import, nên không cấu hình khác đi cho môi trường test được.
Blueprint: chia theo chức năng
# ung_dung/blog/routes.py
from flask import Blueprint, render_template, request
bp = Blueprint("blog", __name__, url_prefix="/blog")
@bp.route("/")
def danh_sach():
trang = request.args.get("trang", 1, type=int)
bai_viet = BaiViet.query.paginate(page=trang, per_page=12)
return render_template("blog/danh_sach.html", bai_viet=bai_viet)
@bp.route("/<slug>")
def chi_tiet(slug):
bai = BaiViet.query.filter_by(slug=slug).first_or_404()
return render_template("blog/chi_tiet.html", bai=bai)
Sinh URL bằng tên blueprint:
url_for("blog.chi_tiet", slug="bai-dau-tien")
<a href="{{ url_for('blog.chi_tiet', slug=bai.slug) }}">{{ bai.tieu_de }}</a>
Application factory
# ung_dung/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
migrate = Migrate()
def tao_ung_dung(cau_hinh="ung_dung.cau_hinh.SanXuat"):
app = Flask(__name__)
app.config.from_object(cau_hinh)
db.init_app(app)
migrate.init_app(app, db)
from ung_dung.blog.routes import bp as blog_bp
from ung_dung.tai_khoan.routes import bp as tk_bp
app.register_blueprint(blog_bp)
app.register_blueprint(tk_bp)
from ung_dung.loi import dang_ky_xu_ly_loi
dang_ky_xu_ly_loi(app)
return app
Giờ bạn tạo được nhiều phiên bản ứng dụng với cấu hình khác nhau — điều này làm việc viết kiểm thử trở nên đơn giản:
import pytest
from ung_dung import tao_ung_dung, db
@pytest.fixture
def app():
app = tao_ung_dung("ung_dung.cau_hinh.KiemThu")
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
def test_trang_chu(client):
r = client.get("/")
assert r.status_code == 200
Cấu hình theo môi trường
# ung_dung/cau_hinh.py
import os
class Chung:
SECRET_KEY = os.environ["SECRET_KEY"]
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ENGINE_OPTIONS = {"pool_pre_ping": True, "pool_recycle": 3600}
class PhatTrien(Chung):
DEBUG = True
SQLALCHEMY_DATABASE_URI = "sqlite:///phat_trien.db"
class KiemThu(Chung):
TESTING = True
SECRET_KEY = "khoa-kiem-thu"
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
class SanXuat(Chung):
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
Dùng os.environ["SECRET_KEY"] chứ đừng dùng .get() kèm giá trị mặc định. Nếu thiếu biến môi trường, ứng dụng phải dừng ngay chứ không được âm thầm chạy với khóa mặc định — đó là lỗ hổng nghiêm trọng.
Cấu trúc thư mục
du_an/
├── ung_dung/
│ ├── __init__.py # factory
│ ├── cau_hinh.py
│ ├── mo_hinh.py
│ ├── loi.py
│ ├── blog/
│ │ ├── __init__.py
│ │ ├── routes.py
│ │ └── form.py
│ ├── tai_khoan/
│ ├── templates/
│ └── static/
├── tests/
├── migrations/
└── wsgi.py
# wsgi.py
from ung_dung import tao_ung_dung
app = tao_ung_dung()
gunicorn wsgi:app --workers 4
Xử lý lỗi tập trung
# ung_dung/loi.py
from flask import render_template, jsonify, request
def dang_ky_xu_ly_loi(app):
@app.errorhandler(404)
def khong_tim_thay(e):
if request.path.startswith("/api/"):
return jsonify(loi="Không tìm thấy"), 404
return render_template("loi/404.html"), 404
@app.errorhandler(500)
def loi_may_chu(e):
app.logger.exception("Lỗi máy chủ")
if request.path.startswith("/api/"):
return jsonify(loi="Lỗi máy chủ"), 500
return render_template("loi/500.html"), 500
Lệnh dòng lệnh riêng
import click
@app.cli.command("tao-admin")
@click.argument("email")
def tao_admin(email):
"""Tạo tài khoản quản trị."""
nd = NguoiDung(email=email, la_admin=True)
db.session.add(nd)
db.session.commit()
click.echo(f"Đã tạo {email}")
flask tao-admin admin@example.com
Trước và sau mỗi request
@app.before_request
def truoc_moi_request():
g.thoi_diem = time.perf_counter()
@app.after_request
def sau_moi_request(response):
thoi_gian = time.perf_counter() - g.thoi_diem
response.headers["X-Thoi-Gian"] = f"{thoi_gian:.3f}"
if thoi_gian > 1.0:
app.logger.warning("Request chậm: %s mất %.2fs", request.path, thoi_gian)
return response
Ghi log các request chậm là cách rẻ tiền để phát hiện vấn đề hiệu năng trước khi người dùng phàn nàn.
Vài điểm bảo mật
- Đừng bao giờ để
DEBUG = Truetrên môi trường thật — trang lỗi của Flask cho phép chạy code Python tùy ý - Jinja2 tự escape biến trong template
.html, nhưng bộ lọc|safetắt bảo vệ đó - Dùng
Flask-WTFđể có bảo vệ CSRF cho biểu mẫu - Đặt
SESSION_COOKIE_SECUREvàHTTPONLYkhi chạy qua HTTPS