Hooks and Flask Extensions
Introduction
Flask exposes hooks that run before and after each request—ideal for auth checks, timing, and cleanup. Extensions add mail, caching, rate limits, and more via the init_app(app) pattern in your application factory. This chapter covers core hooks and how popular extensions fit a modular project.
Prerequisites
Request Lifecycle
before_request → view → after_request → (response sent) → teardown_requestMultiple hooks run in registration order for before_request; reverse for teardown_request.
before_request
from flask import g, request, redirect, url_for
from flask_login import current_user
@app.before_request
def require_login_for_dashboard():
if request.endpoint and request.endpoint.startswith("main.dashboard"):
if not current_user.is_authenticated:
return redirect(url_for("auth.login"))Return a response to short-circuit the view; return None to continue.
Global timing example:
import time
@app.before_request
def start_timer():
g.start_time = time.perf_counter()Store per-request data on g—created fresh each request.
after_request
@app.after_request
def add_security_headers(response):
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "SAMEORIGIN"
return responseMust return the response object (possibly modified).
Log duration:
@app.after_request
def log_duration(response):
if hasattr(g, "start_time"):
elapsed = time.perf_counter() - g.start_time
app.logger.info("%s %s %.3fs", request.method, request.path, elapsed)
return responseteardown_request
Runs even if the view raised an exception—good for cleanup:
@app.teardown_request
def shutdown_session(exception=None):
db.session.remove()Flask-SQLAlchemy often handles this; custom resources (file handles, locks) belong here.
Blueprint-Scoped Hooks
auth_bp = Blueprint("auth", __name__)
@auth_bp.before_request
def auth_before():
passRuns only for routes on that blueprint.
Application Context Hooks
@app.before_app_first_request # deprecated in Flask 2.3+
def deprecated_hook():
passPrefer explicit init in create_app or @app.cli.command instead of deprecated first-request hooks.
Extension Pattern: init_app
app/extensions.py:
from flask_sqlalchemy import SQLAlchemy
from flask_mail import Mail
from flask_caching import Cache
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
db = SQLAlchemy()
mail = Mail()
cache = Cache()
limiter = Limiter(key_func=get_remote_address)app/__init__.py:
from app.extensions import db, mail, cache, limiter
def create_app(config_name="development"):
app = Flask(__name__)
app.config.from_object(config_map[config_name])
db.init_app(app)
mail.init_app(app)
cache.init_app(app)
limiter.init_app(app)
return appCode explanation:
- Extensions are created without app at import time
init_appbinds them when the factory runs—supports tests with different configs
Flask-Mail (Concept)
config.py:
MAIL_SERVER = "smtp.example.com"
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USERNAME = os.environ.get("MAIL_USERNAME")
MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD")Send in view or background task:
from flask_mail import Message
def send_welcome_email(user):
msg = Message(
"Welcome",
recipients=[user.email],
body=f"Hello, {user.username}",
)
mail.send(msg)Long sends should not block the request—see caching and background tasks.
Flask-Caching
app.config["CACHE_TYPE"] = "SimpleCache"
@main_bp.route("/expensive")
@cache.cached(timeout=60)
def expensive_view():
return render_template("report.html", data=compute_report())Redis backend in production—concept linked in chapter 21.
Flask-Limiter
@api_bp.route("/login", methods=["POST"])
@limiter.limit("5 per minute")
def api_login():
...Protects brute-force on login and public APIs.
Flask-CORS
Brief preview—full chapter CORS and Frontend Integration:
from flask_cors import CORS
CORS(app, resources={r"/api/*": {"origins": "*"}})Tighten origins in production.
Choosing Extensions
| Extension | Use case |
|---|---|
| Flask-SQLAlchemy | ORM database |
| Flask-Migrate | Schema migrations |
| Flask-Login | Session auth |
| Flask-WTF | Forms + CSRF |
| Flask-CORS | SPA on another origin |
| Flask-Caching | Expensive view cache |
| Flask-Mail | Transactional email |
| Flask-Limiter | Rate limiting |
Tip
Prefer Maintained Pallets Ecosystem
Check Flask extensions registry and last release date before adopting.
FAQ
before_request not running?
Hook registered on wrong app instance—ensure hooks attach inside create_app.
after_request not called on error?
Use teardown_request for cleanup; use @app.errorhandler for error responses.
g undefined outside request?
g only exists during request—use current_app for app-wide config.
Circular import with extensions?
Keep extensions.py free of model imports; import models after init_app.
Limiter behind Nginx?
Configure get_remote_address to read X-Forwarded-For only with trusted proxy.
Multiple limiter instances?
One Limiter per app—register limits on routes after init_app.