The Flask patterns you need every day — routing, request handling, Blueprints, database integration, auth, and testing — with copy-ready code in every section.
Quick reference
| Task | Code |
|---|---|
| Install | pip install flask |
| Run dev server | flask run or flask --app app run |
| Debug mode | flask run --debug |
| GET route | @app.get("/path") |
| POST route | @app.post("/path") |
| Multiple methods | @app.route("/path", methods=["GET","POST"]) |
| URL parameter | @app.get("/users/<int:user_id>") |
| Build URL | url_for("view_name", id=1) |
| JSON request body | request.get_json() |
| Form data | request.form["field"] |
| Query string | request.args.get("q", "") |
| Return JSON | jsonify({"key": "value"}) |
| Return with status | jsonify(data), 201 |
| Redirect | redirect(url_for("index")) |
| Abort | abort(404) |
| Render template | render_template("index.html", name=name) |
| Set cookie | resp.set_cookie("name", "value") |
| Read cookie | request.cookies.get("name") |
| App factory | def create_app(): app = Flask(__name__); ... |
| Blueprint | bp = Blueprint("auth", __name__) |
| Register blueprint | app.register_blueprint(bp, url_prefix="/auth") |
| Config from object | app.config.from_object("config.ProductionConfig") |
| Config from env | app.config.from_prefixed_env("FLASK") |
| Request context | with app.test_request_context(): |
Installation and minimal app
pip install flask
# app.py
from flask import Flask
app = Flask(__name__)
@app.get("/")
def index():
return "Hello, Flask!"
if __name__ == "__main__":
app.run(debug=True)
Run it:
flask --app app run --debug
# or
python app.py
Routing
from flask import Flask, url_for
app = Flask(__name__)
# Basic GET (shorthand decorator)
@app.get("/")
def index():
return "Home"
# Multiple HTTP methods
@app.route("/submit", methods=["GET", "POST"])
def submit():
if request.method == "POST":
return "Submitted"
return "Show form"
# URL parameters
@app.get("/users/<int:user_id>")
def get_user(user_id: int):
return f"User {user_id}"
@app.get("/posts/<string:slug>")
def get_post(slug: str):
return f"Post: {slug}"
@app.get("/files/<path:filename>")
def serve_file(filename: str):
return f"File: {filename}"
# URL converters: string (default), int, float, path, uuid
# Building URLs (always use url_for — never hardcode)
with app.app_context():
print(url_for("index")) # "/"
print(url_for("get_user", user_id=5)) # "/users/5"
print(url_for("index", page=2)) # "/?page=2"
print(url_for("index", _external=True)) # "http://localhost/"
Request object
from flask import request
@app.route("/data", methods=["GET", "POST"])
def data():
# Query string: GET /data?q=hello&page=2
q = request.args.get("q", "")
page = request.args.get("page", 1, type=int)
# Form data (application/x-www-form-urlencoded or multipart)
name = request.form.get("name")
email = request.form["email"] # raises 400 if missing
# JSON body (application/json)
body = request.get_json() # dict or None
body = request.get_json(force=True) # parse regardless of content-type
body = request.get_json(silent=True) # return None instead of 400
# File upload
file = request.files.get("photo")
if file and file.filename:
file.save(f"/uploads/{file.filename}")
# Headers and cookies
token = request.headers.get("Authorization")
user = request.cookies.get("user_id")
# Request meta
ip = request.remote_addr
method = request.method # "GET"
path = request.path # "/data"
host = request.host # "localhost:5000"
return "ok"
Response
from flask import jsonify, make_response, redirect, url_for, abort
# Plain text / HTML
@app.get("/hello")
def hello():
return "Hello" # 200 OK, text/html
# JSON response
@app.get("/api/user")
def api_user():
return jsonify({"id": 1, "name": "Alice"}) # 200, application/json
# Custom status code
@app.post("/api/users")
def create_user():
user = {"id": 2, "name": "Bob"}
return jsonify(user), 201
# Tuple shorthand: (body, status) or (body, status, headers)
@app.get("/error")
def error():
return {"error": "not found"}, 404
# make_response — for setting cookies, headers
@app.get("/login")
def login():
resp = make_response(redirect(url_for("index")))
resp.set_cookie("session", "abc123", httponly=True, secure=True, samesite="Lax")
return resp
# Delete cookie
@app.get("/logout")
def logout():
resp = make_response(redirect(url_for("index")))
resp.delete_cookie("session")
return resp
# Abort early (triggers error handler)
@app.get("/admin")
def admin():
if not is_authenticated():
abort(401)
return "Admin panel"
# Custom response headers
@app.get("/csv")
def csv_file():
data = "name,age\nAlice,30"
resp = make_response(data)
resp.headers["Content-Type"] = "text/csv"
resp.headers["Content-Disposition"] = "attachment; filename=data.csv"
return resp
Error handlers
from flask import jsonify
@app.errorhandler(404)
def not_found(e):
return jsonify(error=str(e)), 404
@app.errorhandler(401)
def unauthorized(e):
return jsonify(error="Unauthorized"), 401
@app.errorhandler(500)
def server_error(e):
return jsonify(error="Internal server error"), 500
# Catch all exceptions
@app.errorhandler(Exception)
def handle_exception(e):
# Pass through HTTP errors
from werkzeug.exceptions import HTTPException
if isinstance(e, HTTPException):
return jsonify(error=e.description), e.code
return jsonify(error="Internal server error"), 500
Blueprints (modular apps)
Blueprints split a large app into smaller modules.
# auth/routes.py
from flask import Blueprint, jsonify, request
bp = Blueprint("auth", __name__, url_prefix="/auth")
@bp.post("/login")
def login():
data = request.get_json()
# ... verify credentials
return jsonify(token="jwt-token-here")
@bp.post("/register")
def register():
data = request.get_json()
# ... create user
return jsonify(message="registered"), 201
# users/routes.py
from flask import Blueprint, jsonify
bp = Blueprint("users", __name__, url_prefix="/users")
@bp.get("/")
def list_users():
return jsonify(users=[])
@bp.get("/<int:user_id>")
def get_user(user_id: int):
return jsonify(id=user_id)
# app.py (application factory)
from flask import Flask
def create_app(config_name="development"):
app = Flask(__name__)
app.config.from_object(f"config.{config_name.title()}Config")
from auth.routes import bp as auth_bp
from users.routes import bp as users_bp
app.register_blueprint(auth_bp)
app.register_blueprint(users_bp)
return app
Application factory pattern
project/
├── app/
│ ├── __init__.py ← create_app()
│ ├── models.py
│ ├── auth/
│ │ └── routes.py
│ └── users/
│ └── routes.py
├── config.py
├── run.py
└── requirements.txt
# config.py
import os
class Config:
SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret-change-me")
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = "sqlite:///dev.db"
class ProductionConfig(Config):
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ["DATABASE_URL"]
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
# app/__init__.py
from flask import Flask
from .extensions import db, migrate
def create_app(config_name="development"):
app = Flask(__name__)
app.config.from_object(f"config.{config_name.title()}Config")
db.init_app(app)
migrate.init_app(app, db)
from .auth.routes import bp as auth_bp
from .users.routes import bp as users_bp
app.register_blueprint(auth_bp)
app.register_blueprint(users_bp)
return app
# app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
db = SQLAlchemy()
migrate = Migrate()
Flask-SQLAlchemy
pip install flask-sqlalchemy flask-migrate
# app/models.py
from datetime import datetime, timezone
from .extensions import db
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True, nullable=False)
name = db.Column(db.String(80), nullable=False)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
posts = db.relationship("Post", back_populates="author", lazy="select")
def to_dict(self):
return {"id": self.id, "email": self.email, "name": self.name}
class Post(db.Model):
__tablename__ = "posts"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
body = db.Column(db.Text, nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
author = db.relationship("User", back_populates="posts")
CRUD with SQLAlchemy 2.x style:
from flask import Blueprint, jsonify, request
from app.extensions import db
from app.models import User
bp = Blueprint("users", __name__, url_prefix="/users")
# CREATE
@bp.post("/")
def create_user():
data = request.get_json()
user = User(email=data["email"], name=data["name"])
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201
# READ all
@bp.get("/")
def list_users():
users = db.session.execute(db.select(User)).scalars().all()
return jsonify([u.to_dict() for u in users])
# READ one
@bp.get("/<int:user_id>")
def get_user(user_id: int):
user = db.get_or_404(User, user_id)
return jsonify(user.to_dict())
# UPDATE
@bp.put("/<int:user_id>")
def update_user(user_id: int):
user = db.get_or_404(User, user_id)
data = request.get_json()
user.name = data.get("name", user.name)
db.session.commit()
return jsonify(user.to_dict())
# DELETE
@bp.delete("/<int:user_id>")
def delete_user(user_id: int):
user = db.get_or_404(User, user_id)
db.session.delete(user)
db.session.commit()
return "", 204
# Filtering and ordering
@bp.get("/search")
def search_users():
q = request.args.get("q", "")
users = db.session.execute(
db.select(User)
.where(User.name.ilike(f"%{q}%"))
.order_by(User.created_at.desc())
.limit(20)
).scalars().all()
return jsonify([u.to_dict() for u in users])
Migrations:
flask db init # first time only — creates migrations/
flask db migrate -m "add users table"
flask db upgrade
flask db downgrade
Jinja2 templates
from flask import render_template
@app.get("/profile/<name>")
def profile(name: str):
return render_template("profile.html", name=name, items=[1, 2, 3])
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head><title>{% block title %}My App{% endblock %}</title></head>
<body>
{% block content %}{% endblock %}
</body>
</html>
<!-- templates/profile.html -->
{% extends "base.html" %}
{% block title %}{{ name }}'s Profile{% endblock %}
{% block content %}
<h1>Hello, {{ name | capitalize }}!</h1>
{% if items %}
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
{% else %}
<p>No items.</p>
{% endif %}
{% endblock %}
Common Jinja2 filters: {{ text | upper }}, {{ text | truncate(50) }}, {{ date | datetimeformat }}, {{ value | default("N/A") }}, {{ html | safe }} (use carefully).
Middleware and request hooks
import time
from flask import g, request
# Runs before each request
@app.before_request
def start_timer():
g.start = time.perf_counter()
# Runs after each request (has access to response)
@app.after_request
def add_timing_header(response):
elapsed = time.perf_counter() - g.start
response.headers["X-Response-Time"] = f"{elapsed:.3f}s"
return response
# Runs after request even if exception occurred
@app.teardown_request
def teardown(exc):
db.session.remove() # typical SQLAlchemy teardown
# g is a request-scoped namespace (reset per request)
@app.before_request
def load_user():
token = request.headers.get("Authorization", "").removeprefix("Bearer ")
g.current_user = verify_token(token) if token else None
REST API with JWT auth
pip install flask flask-sqlalchemy PyJWT
# Simple JWT Bearer auth
import jwt
import datetime
from functools import wraps
from flask import request, jsonify, current_app, g
def create_token(user_id: int) -> str:
payload = {
"sub": user_id,
"iat": datetime.datetime.utcnow(),
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=24),
}
return jwt.encode(payload, current_app.config["SECRET_KEY"], algorithm="HS256")
def login_required(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return jsonify(error="Missing token"), 401
token = auth.removeprefix("Bearer ")
try:
payload = jwt.decode(token, current_app.config["SECRET_KEY"], algorithms=["HS256"])
g.user_id = payload["sub"]
except jwt.ExpiredSignatureError:
return jsonify(error="Token expired"), 401
except jwt.InvalidTokenError:
return jsonify(error="Invalid token"), 401
return f(*args, **kwargs)
return decorated
# Usage
@app.post("/login")
def login():
data = request.get_json()
user = verify_credentials(data["email"], data["password"])
if not user:
return jsonify(error="Invalid credentials"), 401
return jsonify(token=create_token(user.id))
@app.get("/me")
@login_required
def me():
user = db.get_or_404(User, g.user_id)
return jsonify(user.to_dict())
Testing
pip install pytest
# tests/conftest.py
import pytest
from app import create_app
from app.extensions import db as _db
@pytest.fixture
def app():
app = create_app("testing")
with app.app_context():
_db.create_all()
yield app
_db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def runner(app):
return app.test_cli_runner()
# tests/test_users.py
def test_create_user(client):
resp = client.post("/users/", json={"email": "a@b.com", "name": "Alice"})
assert resp.status_code == 201
data = resp.get_json()
assert data["email"] == "a@b.com"
def test_get_user_not_found(client):
resp = client.get("/users/999")
assert resp.status_code == 404
def test_list_users_empty(client):
resp = client.get("/users/")
assert resp.status_code == 200
assert resp.get_json() == []
def test_update_user(client):
# create first
post_resp = client.post("/users/", json={"email": "b@b.com", "name": "Bob"})
user_id = post_resp.get_json()["id"]
put_resp = client.put(f"/users/{user_id}", json={"name": "Robert"})
assert put_resp.status_code == 200
assert put_resp.get_json()["name"] == "Robert"
def test_auth_required(client):
resp = client.get("/me")
assert resp.status_code == 401
Run tests:
pytest -v
pytest tests/test_users.py -k "test_create"
pytest --cov=app --cov-report=html
CLI commands
# app/cli.py
import click
from flask import current_app
from .extensions import db
from .models import User
def register_commands(app):
@app.cli.command("seed-db")
def seed_db():
"""Seed the database with test data."""
user = User(email="admin@example.com", name="Admin")
db.session.add(user)
db.session.commit()
click.echo("Seeded database.")
@app.cli.command("create-admin")
@click.argument("email")
@click.argument("name")
def create_admin(email, name):
"""Create an admin user."""
user = User(email=email, name=name)
db.session.add(user)
db.session.commit()
click.echo(f"Admin {email} created.")
flask seed-db
flask create-admin admin@example.com "Admin User"
Deployment
Gunicorn (Linux/Mac)
pip install gunicorn
gunicorn "app:create_app()" -w 4 -b 0.0.0.0:8000
Waitress (Windows)
pip install waitress
waitress-serve --port=8000 "app:create_app()"
Docker
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV FLASK_ENV=production
CMD ["gunicorn", "app:create_app()", "-w", "4", "-b", "0.0.0.0:8000"]
Environment variables
export FLASK_APP=app
export FLASK_ENV=production
export SECRET_KEY=your-secret-key
export DATABASE_URL=postgresql://user:pass@localhost/mydb
Or use a .env file with python-dotenv:
pip install python-dotenv
# run.py
from dotenv import load_dotenv
load_dotenv()
from app import create_app
app = create_app()
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
app.run() in production |
Single-threaded, not secure | Use gunicorn/waitress |
Hardcoded SECRET_KEY |
Security risk if code is public | Load from environment variable |
debug=True in production |
Exposes interactive debugger (RCE risk) | Set FLASK_ENV=production |
Not using url_for() |
Hardcoded URLs break on route changes | Always use url_for("view_name") |
Using request.json |
Raises 400 if content-type is wrong | Use request.get_json(silent=True) |
db.session without commit |
Changes not persisted | Call db.session.commit() |
| No error handler | Werkzeug HTML errors leak in API | Add @app.errorhandler(Exception) |
Missing url_prefix on blueprint |
Routes collide across blueprints | Set url_prefix on register_blueprint |
Flask vs Django vs FastAPI
| Feature | Flask | Django | FastAPI |
|---|---|---|---|
| Philosophy | Micro-framework, batteries optional | Batteries included | Async-first, type-driven |
| Learning curve | Low | Medium | Low-medium |
| Built-in ORM | No (use SQLAlchemy) | Yes (Django ORM) | No (use SQLAlchemy/Tortoise) |
| Auto-generated docs | No | No (DRF optional) | Yes (Swagger/Redoc) |
| Request validation | Manual / Flask-WTF | Forms / DRF serializers | Pydantic automatic |
| Async support | Limited (Flask 2+) | Limited (ASGI optional) | Native async |
| Admin panel | No | Built-in | No |
| Best for | Small APIs, prototypes, custom apps | Large full-stack apps | High-perf APIs, ML services |
FAQ
When should I use Flask instead of FastAPI or Django? Flask is ideal when you want full control over your stack, are building a small-to-medium API, or are adding a web layer to an existing project. FastAPI is better if you need automatic validation and OpenAPI docs. Django is better for full-stack apps with admin, ORM, and auth bundled in.
How do I handle CORS in Flask?
Install flask-cors and register it on the app:
from flask_cors import CORS
CORS(app, origins=["https://yourfrontend.com"])
# or for development
CORS(app)
What is g and when should I use it?
g is a request-scoped namespace that lasts for exactly one request. Use it to store data that multiple functions need during the same request — like the current user after authentication — without passing it as function arguments.
How do I stream a large response?
from flask import stream_with_context, Response
@app.get("/stream")
def stream():
def generate():
for i in range(1000):
yield f"data: {i}\n\n"
return Response(stream_with_context(generate()), mimetype="text/event-stream")
How do I handle file uploads safely?
from werkzeug.utils import secure_filename
import os
ALLOWED = {"png", "jpg", "pdf"}
@app.post("/upload")
def upload():
file = request.files.get("file")
if not file or "." not in file.filename:
abort(400)
ext = file.filename.rsplit(".", 1)[1].lower()
if ext not in ALLOWED:
abort(400)
filename = secure_filename(file.filename)
file.save(os.path.join(app.config["UPLOAD_FOLDER"], filename))
return jsonify(filename=filename), 201
How do I add rate limiting?
pip install flask-limiter
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(get_remote_address, app=app, default_limits=["200/day", "50/hour"])
@app.get("/api/search")
@limiter.limit("10/minute")
def search():
return jsonify(results=[])