DevToolBoxฟรี
บล็อก

Flask Guide: Routing, SQLAlchemy, REST APIs, JWT Auth, and Deployment

13 min readโดย DevToolBox
TL;DR — Flask is a lightweight WSGI micro-framework for Python. Install with pip, define routes with decorators, integrate SQLAlchemy for databases, Flask-JWT-Extended for auth, and deploy with Gunicorn + Nginx or Docker. Choose Flask when you want full control over your stack.
Key Takeaways
  • 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 def views 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
Warning: Always review auto-generated migrations before running 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
Tip: Access the auto-generated Swagger UI at /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:

FeatureFlaskFastAPIDjangoBottle
TypeMicro-frameworkAPI frameworkFull-stackMicro-framework
InterfaceWSGIASGIWSGI / ASGIWSGI
Async SupportPartial (2.0+)NativePartial (3.1+)No
PerformanceMediumHighMediumMedium
Auto DocsVia Flask-RESTXBuilt-in (OpenAPI)Via DRFNo
Data ValidationMarshmallow / WTFormsPydantic (built-in)Serializers (DRF)Manual
ORMAny (SQLAlchemy recommended)Any (SQLAlchemy recommended)Built-in Django ORMAny
Admin PanelFlask-Admin (extension)No (use separate tool)Built-inNo
Auth SystemFlask-Login / JWT-ExtendedJWT / OAuth2 (manual)Built-inManual
Learning CurveLowLow–MediumMedium–HighVery Low
Best ForREST APIs, microservices, flexibilityHigh-perf APIs, type-safe codeFull web apps, content sitesSimple scripts, single-file apps
GitHub Stars~68K~80K~80K~8K
DependenciesMinimalStarlette + PydanticHeavy (batteries included)Zero (stdlib only)
CommunityLarge, matureFast-growingVery largeSmall

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.errorhandler for 400, 401, 403, 404, and 500 to ensure consistent JSON error responses.
  • Set up connection pooling — configure SQLALCHEMY_ENGINE_OPTIONS with pool_pre_ping=True and pool_recycle in 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

Is Flask production-ready?

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.

Does Flask support async/await?

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.

What is the difference between Flask and Django?

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.

How do I handle CORS in Flask?

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.

What is the Flask application factory pattern?

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.

How does Flask compare to FastAPI in performance?

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.

How do I run database migrations in Flask?

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.

Can Flask be used for WebSockets?

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.

𝕏 Twitterin LinkedIn
บทความนี้มีประโยชน์ไหม?

อัปเดตข่าวสาร

รับเคล็ดลับการพัฒนาและเครื่องมือใหม่ทุกสัปดาห์

ไม่มีสแปม ยกเลิกได้ตลอดเวลา

ลองเครื่องมือที่เกี่ยวข้อง

{ }JSON Formatter#Hash GeneratorB→Base64 Encoder

บทความที่เกี่ยวข้อง

Django Guide: Models, Views, REST API with DRF, and Deployment

Master Django Python web framework. Covers MTV pattern, ORM and models, views and URL routing, Django REST Framework, JWT authentication, deployment with Docker and Nginx, and Django vs Flask vs FastAPI comparison.

Python Async/Await Guide: asyncio, aiohttp, FastAPI, and Testing

Master Python async programming with asyncio. Complete guide with async/await basics, Tasks, aiohttp client/server, FastAPI integration, asyncpg, concurrent patterns, sync/async bridge, and pytest-asyncio.

PostgreSQL Complete Guide: SQL, Indexes, JSONB, and Performance

Master PostgreSQL with this complete guide. Covers core SQL, indexes, Node.js pg, Prisma ORM, Python asyncpg, JSONB, full-text search, window functions, and performance tuning.