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

text
before_request → view → after_request → (response sent) → teardown_request

Multiple hooks run in registration order for before_request; reverse for teardown_request.

before_request

python
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:

python
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

python
@app.after_request
def add_security_headers(response):
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "SAMEORIGIN"
    return response

Must return the response object (possibly modified).

Log duration:

python
@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 response

teardown_request

Runs even if the view raised an exception—good for cleanup:

python
@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

python
auth_bp = Blueprint("auth", __name__)
 
@auth_bp.before_request
def auth_before():
    pass

Runs only for routes on that blueprint.

Application Context Hooks

python
@app.before_app_first_request  # deprecated in Flask 2.3+
def deprecated_hook():
    pass

Prefer explicit init in create_app or @app.cli.command instead of deprecated first-request hooks.

Extension Pattern: init_app

app/extensions.py:

python
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:

python
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 app

Code explanation:

  • Extensions are created without app at import time
  • init_app binds them when the factory runs—supports tests with different configs

Flask-Mail (Concept)

config.py:

python
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:

python
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

python
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

python
@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:

python
from flask_cors import CORS
CORS(app, resources={r"/api/*": {"origins": "*"}})

Tighten origins in production.

Choosing Extensions

ExtensionUse case
Flask-SQLAlchemyORM database
Flask-MigrateSchema migrations
Flask-LoginSession auth
Flask-WTFForms + CSRF
Flask-CORSSPA on another origin
Flask-CachingExpensive view cache
Flask-MailTransactional email
Flask-LimiterRate 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.