- Flask is a WSGI micro-framework built on Werkzeug (routing/request) and Jinja2 (templating)
- Use the application factory pattern (
create_app()) for scalable, testable apps - Blueprints let you modularize routes into separate files — essential for large projects
- Flask-SQLAlchemy + Flask-Migrate handle ORM and database migrations
- Flask-JWT-Extended provides access tokens, refresh tokens, and protected route decorators
- Run Flask in production with Gunicorn (multiple workers) behind Nginx
- Flask 2.0+ supports
async defviews natively
1. What Is Flask?
Flask is a lightweight WSGI micro-framework for Python, created by Armin Ronacher in 2010. Unlike Django, which ships with an ORM, admin interface, and authentication system built in, Flask provides only the bare essentials: URL routing, request/response handling, and templating. Everything else — databases, authentication, caching, forms — is added through extensions or custom code.
This philosophy of “do one thing and do it well” makes Flask an excellent choice for REST APIs, microservices, and developers who want full control over their architecture. Flask is built on two core libraries:
- Werkzeug — a comprehensive WSGI utility library that handles HTTP routing, request parsing, and response generation
- Jinja2 — a fast, secure templating engine used for rendering HTML (less relevant for pure API backends)
Flask Architecture Overview
# Flask request lifecycle
#
# HTTP Request
# ↓
# WSGI Server (Gunicorn/uWSGI)
# ↓
# Werkzeug WSGI middleware
# ↓
# Flask app.__call__(environ, start_response)
# ↓
# URL Router (Werkzeug Map)
# ↓
# Before-request hooks (@app.before_request)
# ↓
# View function (@app.route)
# ↓
# After-request hooks (@app.after_request)
# ↓
# Response object
# ↓
# HTTP Response
# Core Flask dependencies
flask # Web framework
werkzeug # WSGI toolkit (auto-installed with Flask)
jinja2 # Templating (auto-installed with Flask)
click # CLI support (auto-installed with Flask)
itsdangerous # Secure signing (sessions, tokens)
Flask follows the WSGI (Web Server Gateway Interface) standard, which defines a simple interface between web servers and Python web applications. This means Flask apps can run on any WSGI-compatible server: Gunicorn, uWSGI, mod_wsgi (Apache), or even the built-in development server.
2. Getting Started
Set up a proper Flask project from the start with a virtual environment, the application factory pattern, and a clean directory structure. The factory pattern is critical for testability and avoiding circular imports.
Installation & Project Structure
# Create and activate virtual environment
python -m venv venv
source venv/bin/activate # Linux/macOS
# venv\Scripts\activate # Windows
# Install Flask and common extensions
pip install flask flask-sqlalchemy flask-migrate \
flask-jwt-extended flask-cors flask-restx
pip freeze > requirements.txt
# Recommended project structure
myapp/
├── app/
│ ├── __init__.py # Application factory (create_app)
│ ├── config.py # Configuration classes
│ ├── models/
│ │ ├── __init__.py
│ │ └── user.py # SQLAlchemy models
│ ├── routes/
│ │ ├── __init__.py
│ │ ├── auth.py # Auth blueprint
│ │ └── users.py # Users blueprint
│ ├── schemas/
│ │ └── user.py # Marshmallow/Pydantic schemas
│ └── extensions.py # Extension instances (db, jwt, etc.)
├── tests/
│ ├── conftest.py
│ └── test_users.py
├── migrations/ # Flask-Migrate files
├── .env
├── wsgi.py # Production entry point
└── requirements.txt
Application Factory Pattern
# app/extensions.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_jwt_extended import JWTManager
from flask_cors import CORS
db = SQLAlchemy()
migrate = Migrate()
jwt = JWTManager()
cors = CORS()
# app/__init__.py — Application Factory
from flask import Flask
from .extensions import db, migrate, jwt, cors
from .config import config_map
def create_app(config_name: str = "development") -> Flask:
app = Flask(__name__)
app.config.from_object(config_map[config_name])
# Initialize extensions with app
db.init_app(app)
migrate.init_app(app, db)
jwt.init_app(app)
cors.init_app(app, resources={r"/api/*": {"origins": "*"}})
# Register blueprints
from .routes.auth import auth_bp
from .routes.users import users_bp
app.register_blueprint(auth_bp, url_prefix="/api/auth")
app.register_blueprint(users_bp, url_prefix="/api/users")
return app
# app/config.py
import os
from dotenv import load_dotenv
load_dotenv()
class BaseConfig:
SECRET_KEY = os.environ.get("SECRET_KEY", "change-me-in-production")
SQLALCHEMY_TRACK_MODIFICATIONS = False
JWT_ACCESS_TOKEN_EXPIRES = 900 # 15 minutes
JWT_REFRESH_TOKEN_EXPIRES = 2592000 # 30 days
class DevelopmentConfig(BaseConfig):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get(
"DATABASE_URL", "sqlite:///dev.db"
)
class ProductionConfig(BaseConfig):
DEBUG = False
SQLALCHEMY_DATABASE_URI = os.environ.get("DATABASE_URL")
SQLALCHEMY_ENGINE_OPTIONS = {
"pool_size": 10,
"pool_recycle": 3600,
"pool_pre_ping": True,
}
class TestingConfig(BaseConfig):
TESTING = True
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
JWT_SECRET_KEY = "test-secret"
config_map = {
"development": DevelopmentConfig,
"production": ProductionConfig,
"testing": TestingConfig,
}
# wsgi.py — Production entry point
import os
from app import create_app
app = create_app(os.environ.get("FLASK_ENV", "production"))
if __name__ == "__main__":
app.run()
3. Routing and Views
Flask routing uses Python decorators to map URL patterns to view functions. The @app.route() decorator registers a URL rule with the underlying Werkzeug URL map. Blueprints let you organize routes into modular components.
Route Decorators & URL Variables
from flask import Flask, jsonify, abort
app = Flask(__name__)
# Basic route
@app.route("/")
def index():
return jsonify({"message": "Hello, Flask!"})
# URL variable with type converter
# Converters: string (default), int, float, path, uuid
@app.route("/users/<int:user_id>")
def get_user(user_id: int):
user = User.query.get_or_404(user_id)
return jsonify(user.to_dict())
# Multiple HTTP methods
@app.route("/users", methods=["GET", "POST"])
def users():
if request.method == "POST":
data = request.get_json(force=True)
# ... create user
users = User.query.all()
return jsonify([u.to_dict() for u in users])
# URL with string and uuid converters
@app.route("/posts/<uuid:post_id>/comments/<int:comment_id>")
def get_comment(post_id, comment_id):
return jsonify({"post_id": str(post_id), "comment_id": comment_id})
# Route with optional trailing slash
@app.route("/about/") # Redirects /about → /about/
@app.route("/contact") # Returns 404 for /contact/
def static_pages():
return jsonify({"page": "static"})
Blueprints for Modular Routes
# app/routes/users.py
from flask import Blueprint, jsonify, request
from ..extensions import db
from ..models.user import User
users_bp = Blueprint("users", __name__)
@users_bp.route("/")
def list_users():
"""List all users with pagination."""
page = request.args.get("page", 1, type=int)
per_page = request.args.get("per_page", 20, type=int)
per_page = min(per_page, 100) # Cap at 100
pagination = User.query.order_by(User.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
return jsonify({
"users": [u.to_dict() for u in pagination.items],
"total": pagination.total,
"pages": pagination.pages,
"current_page": page,
})
@users_bp.route("/<int:user_id>", methods=["GET", "PUT", "DELETE"])
def user_detail(user_id):
user = User.query.get_or_404(user_id)
if request.method == "GET":
return jsonify(user.to_dict())
if request.method == "PUT":
data = request.get_json()
for key, value in data.items():
if hasattr(user, key) and key not in ("id", "password_hash"):
setattr(user, key, value)
db.session.commit()
return jsonify(user.to_dict())
if request.method == "DELETE":
db.session.delete(user)
db.session.commit()
return "", 204
4. Request and Response Handling
Flask's request proxy object gives you access to all incoming data: JSON body, form data, query parameters, headers, cookies, and uploaded files. Response helpers like jsonify and make_response let you control status codes, headers, and content type.
from flask import Flask, request, jsonify, make_response, abort
app = Flask(__name__)
@app.route("/api/data", methods=["POST"])
def handle_data():
# ---- Reading the request ----
# JSON body (Content-Type: application/json)
data = request.get_json() # Returns None if not JSON
data = request.get_json(force=True) # Parse even without Content-Type header
data = request.get_json(silent=True) # Return None instead of raising error
# Query parameters: /api/data?sort=asc&page=2
sort = request.args.get("sort", "desc")
page = request.args.get("page", 1, type=int)
# Form data (Content-Type: multipart/form-data or application/x-www-form-urlencoded)
username = request.form.get("username")
# Headers
auth = request.headers.get("Authorization")
content_type = request.content_type
# Cookies
session_id = request.cookies.get("session_id")
# File uploads
file = request.files.get("upload")
if file:
filename = secure_filename(file.filename)
file.save(f"/uploads/{filename}")
# Request metadata
client_ip = request.remote_addr
method = request.method
path = request.path
# ---- Building responses ----
# Simple JSON response (200 OK by default)
return jsonify({"status": "ok", "page": page})
# With explicit status code
return jsonify({"user": "created"}), 201
# make_response for full control
resp = make_response(jsonify({"data": "..."}), 200)
resp.headers["X-Custom-Header"] = "value"
resp.set_cookie("session", "abc123", httponly=True, secure=True, samesite="Lax")
return resp
# Global error handlers
@app.errorhandler(400)
def bad_request(e):
return jsonify({"error": "Bad request", "message": str(e)}), 400
@app.errorhandler(404)
def not_found(e):
return jsonify({"error": "Not found"}), 404
@app.errorhandler(500)
def server_error(e):
return jsonify({"error": "Internal server error"}), 500
# Custom exception class
class APIError(Exception):
def __init__(self, message: str, status_code: int = 400):
super().__init__(message)
self.message = message
self.status_code = status_code
@app.errorhandler(APIError)
def handle_api_error(e: APIError):
return jsonify({"error": e.message}), e.status_code
# Usage:
# raise APIError("Email already registered", 409)
5. Database Integration with SQLAlchemy
Flask-SQLAlchemy wraps SQLAlchemy with Flask-friendly conveniences: automatic database session management, model base class, and integration with the application factory. Flask-Migrate adds Alembic-powered schema migrations so you never manually alter tables in production.
Defining Models
# app/models/user.py
from datetime import datetime, timezone
from ..extensions import db
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
email = db.Column(db.String(120), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(255), nullable=False)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc))
updated_at = db.Column(db.DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc))
# One-to-many relationship
posts = db.relationship("Post", back_populates="author",
cascade="all, delete-orphan", lazy="dynamic")
def set_password(self, password: str) -> None:
from werkzeug.security import generate_password_hash
self.password_hash = generate_password_hash(password)
def check_password(self, password: str) -> bool:
from werkzeug.security import check_password_hash
return check_password_hash(self.password_hash, password)
def to_dict(self) -> dict:
return {
"id": self.id,
"username": self.username,
"email": self.email,
"is_active": self.is_active,
"created_at": self.created_at.isoformat(),
}
def __repr__(self) -> str:
return f"<User {self.username}>"
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)
created_at = db.Column(db.DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc))
author = db.relationship("User", back_populates="posts")
def to_dict(self) -> dict:
return {
"id": self.id,
"title": self.title,
"body": self.body,
"author": self.author.username,
"created_at": self.created_at.isoformat(),
}
CRUD Operations
from .extensions import db
from .models.user import User
# CREATE
def create_user(username: str, email: str, password: str) -> User:
user = User(username=username, email=email)
user.set_password(password)
db.session.add(user)
db.session.commit()
return user
# READ — single record
user = User.query.get(1) # by primary key
user = User.query.get_or_404(1) # raises 404 if not found
user = User.query.filter_by(email="a@b.com").first()
user = User.query.filter(User.email == "a@b.com").one_or_none()
# READ — multiple records
users = User.query.all()
users = User.query.filter(User.is_active == True).order_by(User.created_at.desc()).limit(10).all()
count = User.query.filter_by(is_active=True).count()
# Pagination
pagination = User.query.paginate(page=2, per_page=20, error_out=False)
users = pagination.items
total = pagination.total
# UPDATE
user = User.query.get_or_404(1)
user.email = "newemail@example.com"
db.session.commit()
# Bulk update (SQLAlchemy 2.x style)
db.session.execute(
db.update(User).where(User.is_active == False).values(is_active=True)
)
db.session.commit()
# DELETE
user = User.query.get_or_404(1)
db.session.delete(user)
db.session.commit()
# Bulk delete
User.query.filter(User.is_active == False).delete()
db.session.commit()
Database Migrations with Flask-Migrate
# 1. Initialize migrations (once per project)
flask db init
# Creates a migrations/ directory with Alembic config
# 2. Generate a migration after changing models
flask db migrate -m "add users and posts tables"
# Creates migrations/versions/abc123_add_users_and_posts_tables.py
# ALWAYS review the generated migration before applying!
# 3. Apply migrations
flask db upgrade
# 4. Roll back the last migration
flask db downgrade
# 5. Check current migration status
flask db current
flask db history
# Typical production deployment script:
# git pull origin main
# pip install -r requirements.txt
# flask db upgrade
# systemctl restart gunicorn
flask db upgrade in production. Alembic cannot detect all schema changes (e.g., column type changes, constraint renames) and may generate incomplete or incorrect migrations.6. REST APIs with Flask-RESTX
Flask-RESTX (the actively maintained fork of Flask-RESTPlus) builds REST API infrastructure on top of Flask: resource-based routing, automatic Swagger UI documentation, request parsing and validation, and response marshalling. It's the quickest way to build a documented, validated REST API with Flask.
from flask import Flask
from flask_restx import Api, Resource, fields, reqparse
from flask_jwt_extended import jwt_required, get_jwt_identity
app = Flask(__name__)
api = Api(
app,
version="1.0",
title="My REST API",
description="A production-ready Flask REST API",
doc="/swagger", # Swagger UI available at /swagger
authorizations={
"Bearer": {
"type": "apiKey",
"in": "header",
"name": "Authorization",
"description": "Add: Bearer <your-token>",
}
},
security="Bearer",
)
# Namespace (equivalent to Blueprint)
users_ns = api.namespace("users", description="User operations")
# Response models (used for Swagger docs + marshalling)
user_model = api.model("User", {
"id": fields.Integer(readonly=True, description="User ID"),
"username": fields.String(required=True, description="Username", min_length=3),
"email": fields.String(required=True, description="Email address"),
"is_active": fields.Boolean(description="Account status"),
"created_at": fields.DateTime(readonly=True),
})
user_create_model = api.model("UserCreate", {
"username": fields.String(required=True, min_length=3, max_length=80),
"email": fields.String(required=True),
"password": fields.String(required=True, min_length=8),
})
# Request parser for query parameters
list_parser = reqparse.RequestParser()
list_parser.add_argument("page", type=int, default=1, location="args")
list_parser.add_argument("per_page", type=int, default=20, location="args")
list_parser.add_argument("search", type=str, location="args")
@users_ns.route("/")
class UserList(Resource):
@users_ns.doc("list_users")
@users_ns.expect(list_parser)
@users_ns.marshal_list_with(user_model)
@jwt_required()
def get(self):
"""List all users (paginated)."""
args = list_parser.parse_args()
query = User.query
if args.search:
query = query.filter(User.username.ilike(f"%{args.search}%"))
return query.paginate(
page=args.page, per_page=args.per_page
).items
@users_ns.doc("create_user")
@users_ns.expect(user_create_model, validate=True)
@users_ns.marshal_with(user_model, code=201)
def post(self):
"""Create a new user."""
data = api.payload
if User.query.filter_by(email=data["email"]).first():
api.abort(409, "Email already registered")
user = User(username=data["username"], email=data["email"])
user.set_password(data["password"])
db.session.add(user)
db.session.commit()
return user, 201
@users_ns.route("/<int:user_id>")
@users_ns.param("user_id", "The user identifier")
class UserDetail(Resource):
@users_ns.marshal_with(user_model)
@jwt_required()
def get(self, user_id):
"""Get a user by ID."""
return User.query.get_or_404(user_id)
@users_ns.expect(user_model)
@users_ns.marshal_with(user_model)
@jwt_required()
def put(self, user_id):
"""Update a user."""
user = User.query.get_or_404(user_id)
current_user_id = get_jwt_identity()
if user.id != current_user_id:
api.abort(403, "Cannot update another user")
data = api.payload
for key in ("username", "email"):
if key in data:
setattr(user, key, data[key])
db.session.commit()
return user
@users_ns.response(204, "User deleted")
@jwt_required()
def delete(self, user_id):
"""Delete a user."""
user = User.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return "", 204
/swagger when using Flask-RESTX. It provides interactive documentation where you can test every endpoint directly in the browser — no Postman needed during development.7. Authentication with Flask-JWT-Extended
Flask-JWT-Extended provides JSON Web Token authentication with access tokens (short-lived), refresh tokens (long-lived), token revocation via a blocklist, and decorators for protecting routes. This is the most production-ready JWT library for Flask.
# app/routes/auth.py
from flask import Blueprint, jsonify, request
from flask_jwt_extended import (
create_access_token,
create_refresh_token,
jwt_required,
get_jwt_identity,
get_jwt,
decode_token,
)
from ..extensions import db, jwt
from ..models.user import User, TokenBlocklist
auth_bp = Blueprint("auth", __name__)
@auth_bp.route("/login", methods=["POST"])
def login():
"""Exchange credentials for JWT tokens."""
data = request.get_json(silent=True) or {}
email = data.get("email", "").strip().lower()
password = data.get("password", "")
if not email or not password:
return jsonify({"error": "Email and password required"}), 400
user = User.query.filter_by(email=email).first()
if not user or not user.check_password(password):
return jsonify({"error": "Invalid credentials"}), 401
if not user.is_active:
return jsonify({"error": "Account disabled"}), 403
# Create tokens — identity can be any JSON-serializable value
access_token = create_access_token(identity=user.id)
refresh_token = create_refresh_token(identity=user.id)
return jsonify({
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "Bearer",
"user": user.to_dict(),
})
@auth_bp.route("/refresh", methods=["POST"])
@jwt_required(refresh=True) # Requires refresh token in Authorization header
def refresh():
"""Issue a new access token using a valid refresh token."""
current_user_id = get_jwt_identity()
new_access_token = create_access_token(identity=current_user_id)
return jsonify({"access_token": new_access_token})
@auth_bp.route("/logout", methods=["DELETE"])
@jwt_required()
def logout():
"""Revoke the current access token (add to blocklist)."""
jti = get_jwt()["jti"] # JWT ID — unique per token
db.session.add(TokenBlocklist(jti=jti))
db.session.commit()
return jsonify({"message": "Token revoked"})
@auth_bp.route("/me")
@jwt_required()
def me():
"""Get the current authenticated user."""
user_id = get_jwt_identity()
user = User.query.get_or_404(user_id)
return jsonify(user.to_dict())
# Token blocklist model
class TokenBlocklist(db.Model):
id = db.Column(db.Integer, primary_key=True)
jti = db.Column(db.String(36), nullable=False, index=True)
created_at = db.Column(db.DateTime, server_default=db.func.now())
# Register blocklist checker in app factory
@jwt.token_in_blocklist_loader
def check_if_token_revoked(jwt_header, jwt_payload):
jti = jwt_payload["jti"]
return db.session.query(
TokenBlocklist.id
).filter_by(jti=jti).scalar() is not None
@jwt.expired_token_loader
def expired_token_callback(jwt_header, jwt_payload):
return jsonify({"error": "Token has expired", "code": "TOKEN_EXPIRED"}), 401
@jwt.invalid_token_loader
def invalid_token_callback(error):
return jsonify({"error": "Invalid token", "code": "INVALID_TOKEN"}), 401
# Using protected routes:
# Authorization: Bearer <access_token>
@auth_bp.route("/admin-only")
@jwt_required()
def admin_only():
user_id = get_jwt_identity()
user = User.query.get_or_404(user_id)
if user.role != "admin":
return jsonify({"error": "Admin access required"}), 403
return jsonify({"data": "secret admin data"})
8. Testing with pytest
Flask has excellent testing support through its built-in test client. Using the application factory pattern, you can create isolated test instances with an in-memory SQLite database. pytest fixtures handle setup and teardown automatically.
# tests/conftest.py
import pytest
from app import create_app
from app.extensions import db as _db
from app.models.user import User
@pytest.fixture(scope="session")
def app():
"""Create application for the whole test session."""
app = create_app("testing")
with app.app_context():
_db.create_all()
yield app
_db.drop_all()
@pytest.fixture(scope="function")
def client(app):
"""Test client that resets database state per test."""
return app.test_client()
@pytest.fixture(scope="function")
def db_session(app):
"""Isolated database session — rolls back after each test."""
with app.app_context():
connection = _db.engine.connect()
transaction = connection.begin()
session = _db.session
yield session
session.close()
transaction.rollback()
connection.close()
@pytest.fixture
def sample_user(db_session):
"""Create a user fixture available to any test."""
user = User(username="testuser", email="test@example.com")
user.set_password("testpassword")
db_session.add(user)
db_session.commit()
return user
@pytest.fixture
def auth_headers(client, sample_user):
"""Return Authorization headers for a logged-in user."""
resp = client.post(
"/api/auth/login",
json={"email": "test@example.com", "password": "testpassword"},
)
token = resp.get_json()["access_token"]
return {"Authorization": f"Bearer {token}"}
# tests/test_users.py
import pytest
class TestAuth:
def test_login_success(self, client, sample_user):
resp = client.post(
"/api/auth/login",
json={"email": "test@example.com", "password": "testpassword"},
)
assert resp.status_code == 200
data = resp.get_json()
assert "access_token" in data
assert "refresh_token" in data
def test_login_wrong_password(self, client, sample_user):
resp = client.post(
"/api/auth/login",
json={"email": "test@example.com", "password": "wrongpassword"},
)
assert resp.status_code == 401
assert resp.get_json()["error"] == "Invalid credentials"
def test_protected_route_without_token(self, client):
resp = client.get("/api/auth/me")
assert resp.status_code == 401
def test_protected_route_with_token(self, client, auth_headers):
resp = client.get("/api/auth/me", headers=auth_headers)
assert resp.status_code == 200
assert resp.get_json()["email"] == "test@example.com"
class TestUsers:
def test_create_user(self, client):
resp = client.post(
"/api/users/",
json={
"username": "newuser",
"email": "new@example.com",
"password": "securePass123",
},
)
assert resp.status_code == 201
data = resp.get_json()
assert data["username"] == "newuser"
assert "password" not in data # Never expose password
def test_duplicate_email(self, client, sample_user):
resp = client.post(
"/api/users/",
json={"username": "other", "email": "test@example.com", "password": "pass"},
)
assert resp.status_code == 409
def test_get_user_list_requires_auth(self, client):
resp = client.get("/api/users/")
assert resp.status_code == 401
def test_get_user_list(self, client, auth_headers, sample_user):
resp = client.get("/api/users/", headers=auth_headers)
assert resp.status_code == 200
data = resp.get_json()
assert isinstance(data, list)
assert len(data) >= 1
# Run tests:
# pytest tests/ -v
# pytest tests/ -v --cov=app --cov-report=html
9. Flask vs FastAPI vs Django vs Bottle
Choosing a Python web framework depends on your project requirements. Here is a detailed comparison of the four most popular options:
| Feature | Flask | FastAPI | Django | Bottle |
|---|---|---|---|---|
| Type | Micro-framework | API framework | Full-stack | Micro-framework |
| Interface | WSGI | ASGI | WSGI / ASGI | WSGI |
| Async Support | Partial (2.0+) | Native | Partial (3.1+) | No |
| Performance | Medium | High | Medium | Medium |
| Auto Docs | Via Flask-RESTX | Built-in (OpenAPI) | Via DRF | No |
| Data Validation | Marshmallow / WTForms | Pydantic (built-in) | Serializers (DRF) | Manual |
| ORM | Any (SQLAlchemy recommended) | Any (SQLAlchemy recommended) | Built-in Django ORM | Any |
| Admin Panel | Flask-Admin (extension) | No (use separate tool) | Built-in | No |
| Auth System | Flask-Login / JWT-Extended | JWT / OAuth2 (manual) | Built-in | Manual |
| Learning Curve | Low | Low–Medium | Medium–High | Very Low |
| Best For | REST APIs, microservices, flexibility | High-perf APIs, type-safe code | Full web apps, content sites | Simple scripts, single-file apps |
| GitHub Stars | ~68K | ~80K | ~80K | ~8K |
| Dependencies | Minimal | Starlette + Pydantic | Heavy (batteries included) | Zero (stdlib only) |
| Community | Large, mature | Fast-growing | Very large | Small |
When to Choose Flask
- You want full control over every component of your stack
- Building a REST API or microservice that doesn't need Django's admin panel or ORM
- You have an existing Flask codebase to maintain or extend
- You need server-rendered HTML templates alongside API routes (Jinja2 integration is seamless)
- Your team is already familiar with Flask and ecosystem extensions
- You prefer gradual adoption of async rather than a fully async codebase
10. Production Deployment
Never run the Flask development server (flask run) in production. Use Gunicorn as the WSGI application server with multiple worker processes, and Nginx as a reverse proxy to handle SSL termination, static files, and connection limiting.
Gunicorn + Nginx Configuration
# Install production dependencies
pip install gunicorn
# Start Flask app with Gunicorn
# Formula: workers = (2 * CPU cores) + 1
gunicorn wsgi:app \
--workers 4 \
--worker-class sync \
--bind 0.0.0.0:8000 \
--timeout 120 \
--keepalive 5 \
--max-requests 1000 \
--max-requests-jitter 50 \
--log-level info \
--access-logfile /var/log/gunicorn/access.log \
--error-logfile /var/log/gunicorn/error.log
# /etc/nginx/sites-available/myapp
server {
listen 80;
server_name myapp.com www.myapp.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name myapp.com www.myapp.com;
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "DENY";
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
}
location /static/ {
root /var/www/myapp;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
Docker Deployment
# Dockerfile
FROM python:3.12-slim
# Security: run as non-root user
RUN addgroup --system appgroup && adduser --system appuser --ingroup appgroup
WORKDIR /app
# Install dependencies first (layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Change ownership
RUN chown -R appuser:appgroup /app
USER appuser
EXPOSE 8000
CMD ["gunicorn", "wsgi:app",
"--workers", "4",
"--bind", "0.0.0.0:8000",
"--timeout", "120"]
# docker-compose.yml
version: "3.9"
services:
app:
build: .
ports:
- "8000:8000"
environment:
- FLASK_ENV=production
- DATABASE_URL=postgresql://user:pass@db:5432/mydb
- SECRET_KEY=${SECRET_KEY}
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "user"]
interval: 10s
timeout: 5s
retries: 5
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
- ./certs:/etc/nginx/certs
depends_on:
- app
volumes:
postgres_data:
11. Flask Best Practices
- Always use the application factory pattern — it enables testing, multiple configs, and avoids circular imports.
- Store all secrets in environment variables — never hardcode
SECRET_KEY, database URLs, or API keys in source code. - Use Flask-Migrate for all schema changes — never manually ALTER TABLE in a production database.
- Validate all input — use Marshmallow, Flask-RESTX models, or Pydantic to validate and deserialize incoming data. Never trust raw
request.get_json()directly. - Use blueprints for everything — even small APIs benefit from separation. Register blueprints in
create_app()to keep the factory clean. - Handle errors globally — register
@app.errorhandlerfor 400, 401, 403, 404, and 500 to ensure consistent JSON error responses. - Set up connection pooling — configure
SQLALCHEMY_ENGINE_OPTIONSwithpool_pre_ping=Trueandpool_recyclein production to avoid stale connections. - Write tests before deploying — use the test client and in-memory SQLite. Aim for at least 80% coverage of your route handlers and service layer.
Frequently Asked Questions
Yes. Flask itself is production-ready, but the built-in development server is not. For production, run Flask behind Gunicorn or uWSGI with Nginx as a reverse proxy. Many large companies including Pinterest, LinkedIn, and Netflix have used Flask in production at scale.
Flask 2.0+ added native async support. You can define async view functions using "async def" and Flask will run them in a thread pool. For full async performance, consider using an ASGI server like Hypercorn instead of Gunicorn. FastAPI is a better choice if async-first is a hard requirement.
Flask is a micro-framework that gives you just the essentials (routing, templating, request handling) and lets you choose your own ORM, auth, and admin tools. Django is a batteries-included framework with a built-in ORM, admin panel, auth system, and more. Flask is better for APIs and microservices; Django is better for full-featured web applications.
Install Flask-CORS (pip install flask-cors) and use CORS(app) to allow all origins, or configure specific origins: CORS(app, origins=["https://yourfrontend.com"]). You can also apply CORS to individual blueprints for fine-grained control.
The application factory pattern creates the Flask app inside a function (usually called create_app). This allows you to create multiple instances of the app with different configurations — great for testing. It also avoids circular imports when using blueprints.
FastAPI is significantly faster than Flask for I/O-bound workloads because it runs on ASGI with native async support. Flask (WSGI) typically handles ~2,000-5,000 requests/sec on a single worker; FastAPI can handle 10,000+ req/sec. However, with multiple Gunicorn workers, Flask performance scales linearly and is sufficient for most applications.
Use Flask-Migrate (built on Alembic). After setting it up: run "flask db init" once, then "flask db migrate -m message" to generate migrations, and "flask db upgrade" to apply them. Always commit migration files to version control.
Yes, with Flask-SocketIO. It provides WebSocket support on top of Flask using the Socket.IO protocol, which also handles fallbacks for older browsers. For a simpler WebSocket-only solution, consider using aiohttp or FastAPI with native WebSocket support.