Scaffold a production-ready Flask 3.x application with application factory, Blueprints, SQLAlchemy, JWT auth, and Gunicorn deployment.
84
80%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Optimize this skill with Tessl
npx tessl skill review --optimize ./backend-python/flask-project-starter/SKILL.mdScaffold a production-ready Flask 3.x application with application factory, Blueprints, SQLAlchemy, JWT auth, and Gunicorn deployment.
uv or pip for dependency managementmkdir -p src/app/{auth,users,extensions,errors} tests
touch src/app/__init__.py src/app/auth/__init__.py src/app/users/__init__.py \
src/app/extensions/__init__.py src/app/errors/__init__.py tests/__init__.py
cat > pyproject.toml << 'PYPROJECT'
[project]
name = "app"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"flask>=3.1",
"flask-sqlalchemy>=3.1",
"flask-migrate>=4.0",
"flask-jwt-extended>=4.7",
"flask-cors>=5.0",
"marshmallow>=3.23",
"psycopg2-binary>=2.9",
"gunicorn>=23.0",
"python-dotenv>=1.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.3",
"pytest-flask>=1.3",
"ruff>=0.8",
]
[tool.ruff]
target-version = "py312"
line-length = 99
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "B", "SIM"]
[tool.pytest.ini_options]
testpaths = ["tests"]
PYPROJECT
flask --app src/app db init -d src/app/migrations
# Initialize database
flask --app src/app db upgrade -d src/app/migrationsproject-root/
├── pyproject.toml
├── .env
├── .env.example # Required env vars template
├── src/
│ └── app/
│ ├── __init__.py # create_app() factory
│ ├── config.py # Config classes
│ ├── extensions/
│ │ └── __init__.py # db, migrate, jwt instances
│ ├── errors/
│ │ └── __init__.py # Global error handlers
│ ├── auth/
│ │ ├── __init__.py # Blueprint
│ │ └── routes.py
│ ├── users/
│ │ ├── __init__.py # Blueprint
│ │ ├── models.py
│ │ ├── routes.py
│ │ └── schemas.py
│ ├── migrations/ # Flask-Migrate (Alembic)
│ │ ├── env.py
│ │ └── versions/
│ └── cli.py # Custom CLI commands
└── tests/
├── __init__.py
├── conftest.py
└── test_users.pyapp object.extensions/__init__.py, then initialized in create_app().BaseConfig, DevConfig, ProdConfig, TestConfig.marshmallow for request/response serialization (not Pydantic -- Flask ecosystem convention).flask-jwt-extended. Store secrets in environment variables.import os
class BaseConfig:
SECRET_KEY = os.environ.get("SECRET_KEY", "change-me")
SQLALCHEMY_DATABASE_URI = os.environ.get(
"DATABASE_URL", "postgresql://postgres:postgres@localhost:5432/app"
)
SQLALCHEMY_TRACK_MODIFICATIONS = False
JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "jwt-change-me")
JWT_ACCESS_TOKEN_EXPIRES = 3600 # seconds
class DevConfig(BaseConfig):
DEBUG = True
class ProdConfig(BaseConfig):
DEBUG = False
class TestConfig(BaseConfig):
TESTING = True
SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
configs = {
"dev": DevConfig,
"prod": ProdConfig,
"test": TestConfig,
}from flask_cors import CORS
from flask_jwt_extended import JWTManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
migrate = Migrate()
jwt = JWTManager()
cors = CORS()import os
from dotenv import load_dotenv
from flask import Flask
from app.config import configs
# Load .env before reading any config — python-dotenv auto-loads when FLASK_APP
# is set, but explicit is better than implicit for non-CLI entrypoints (e.g. gunicorn).
load_dotenv()
def create_app(config_name: str | None = None) -> Flask:
app = Flask(__name__)
config_name = config_name or os.environ.get("FLASK_CONFIG", "dev")
app.config.from_object(configs[config_name])
_init_extensions(app)
_register_blueprints(app)
_register_error_handlers(app)
_register_cli(app)
return app
def _init_extensions(app: Flask) -> None:
from app.extensions import cors, db, jwt, migrate
db.init_app(app)
migrate.init_app(app, db)
jwt.init_app(app)
cors.init_app(app)
def _register_blueprints(app: Flask) -> None:
from app.auth import auth_bp
from app.users import users_bp
app.register_blueprint(auth_bp, url_prefix="/api/v1/auth")
app.register_blueprint(users_bp, url_prefix="/api/v1/users")
def _register_error_handlers(app: Flask) -> None:
from app.errors import register_error_handlers
register_error_handlers(app)
def _register_cli(app: Flask) -> None:
from app.cli import register_cli
register_cli(app)from flask import Flask, jsonify
from marshmallow import ValidationError
from werkzeug.exceptions import HTTPException
def register_error_handlers(app: Flask) -> None:
@app.errorhandler(HTTPException)
def handle_http_error(exc: HTTPException):
return jsonify({"error": exc.name, "message": exc.description}), exc.code
@app.errorhandler(ValidationError)
def handle_validation_error(exc: ValidationError):
return jsonify({"error": "Validation Error", "messages": exc.messages}), 422
@app.errorhandler(Exception)
def handle_unexpected_error(exc: Exception):
app.logger.exception("Unhandled exception")
return jsonify({"error": "Internal Server Error"}), 500from datetime import datetime, timezone
from werkzeug.security import check_password_hash, generate_password_hash
from app.extensions import db
class User(db.Model):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
username = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(256), nullable=False)
is_active = db.Column(db.Boolean, default=True)
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
def set_password(self, password: str) -> None:
self.password_hash = generate_password_hash(password)
def check_password(self, password: str) -> bool:
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f"<User {self.email}>"from marshmallow import Schema, fields, validate
class UserSchema(Schema):
id = fields.Int(dump_only=True)
email = fields.Email(required=True)
username = fields.Str(required=True, validate=validate.Length(min=3, max=80))
is_active = fields.Bool(dump_only=True)
created_at = fields.DateTime(dump_only=True)
class UserCreateSchema(UserSchema):
password = fields.Str(required=True, load_only=True, validate=validate.Length(min=8))# users/__init__.py
from flask import Blueprint
users_bp = Blueprint("users", __name__)
from app.users import routes # noqa: E402, F401# users/routes.py
from flask import jsonify, request
from flask_jwt_extended import jwt_required
from app.extensions import db
from app.users import users_bp
from app.users.models import User
from app.users.schemas import UserCreateSchema, UserSchema
user_schema = UserSchema()
users_schema = UserSchema(many=True)
create_schema = UserCreateSchema()
@users_bp.get("/")
@jwt_required()
def list_users():
page = request.args.get("page", 1, type=int)
per_page = request.args.get("per_page", 25, type=int)
pagination = User.query.filter_by(is_active=True).paginate(
page=page, per_page=per_page, error_out=False
)
return jsonify({
"items": users_schema.dump(pagination.items),
"total": pagination.total,
"page": pagination.page,
"pages": pagination.pages,
})
@users_bp.post("/")
def create_user():
data = create_schema.load(request.get_json())
user = User(email=data["email"], username=data["username"])
user.set_password(data["password"])
db.session.add(user)
db.session.commit()
return jsonify(user_schema.dump(user)), 201
@users_bp.get("/<int:user_id>")
@jwt_required()
def get_user(user_id: int):
user = db.get_or_404(User, user_id)
return jsonify(user_schema.dump(user))# auth/__init__.py
from flask import Blueprint
auth_bp = Blueprint("auth", __name__)
from app.auth import routes # noqa: E402, F401# auth/routes.py
from flask import jsonify, request
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
from app.auth import auth_bp
from app.users.models import User
@auth_bp.post("/login")
def login():
data = request.get_json()
user = User.query.filter_by(email=data.get("email")).first()
if not user or not user.check_password(data.get("password", "")):
return jsonify({"error": "Invalid credentials"}), 401
token = create_access_token(identity=str(user.id))
return jsonify({"access_token": token})
@auth_bp.get("/me")
@jwt_required()
def me():
user = User.query.get_or_404(int(get_jwt_identity()))
from app.users.schemas import UserSchema
return jsonify(UserSchema().dump(user))import click
from flask import Flask
def register_cli(app: Flask) -> None:
@app.cli.command("seed")
@click.option("--count", default=10, help="Number of users to create")
def seed_db(count: int):
"""Seed the database with sample data."""
from app.extensions import db
from app.users.models import User
for i in range(count):
user = User(email=f"user{i}@example.com", username=f"user{i}")
user.set_password("password123")
db.session.add(user)
db.session.commit()
click.echo(f"Seeded {count} users.")
@app.cli.command("create-admin")
@click.option("--email", prompt=True)
@click.option("--password", prompt=True, hide_input=True)
def create_admin(email: str, password: str):
"""Create an admin user."""
from app.extensions import db
from app.users.models import User
user = User(email=email, username=email.split("@")[0])
user.set_password(password)
db.session.add(user)
db.session.commit()
click.echo(f"Admin {email} created.")import pytest
from app import create_app
from app.extensions import db as _db
@pytest.fixture
def app():
app = create_app("test")
with app.app_context():
_db.create_all()
yield app
_db.session.rollback()
_db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
@pytest.fixture
def db(app):
return _dbdef test_create_user(client):
response = client.post("/api/v1/users/", json={
"email": "new@example.com",
"username": "newuser",
"password": "securepass123",
})
assert response.status_code == 201
assert response.json["email"] == "new@example.com"
def test_login_invalid_credentials(client):
response = client.post("/api/v1/auth/login", json={
"email": "nobody@example.com",
"password": "wrong",
})
assert response.status_code == 401.env.example to .env and fill in valuespython -m venv .venv && source .venv/bin/activatepip install -e ".[dev]" (or uv sync)flask --app src/app db upgrade -d src/app/migrationsflask --app src/app run --debug --port 8000curl http://localhost:8000/api/v1/users/# Install dependencies
uv sync # or: pip install -e ".[dev]"
# Run development server
flask --app src/app run --debug --port 8000
# Initialize migrations (first time only)
flask --app src/app db init -d src/app/migrations
# Create migration
flask --app src/app db migrate -d src/app/migrations -m "add users table"
# Apply migrations
flask --app src/app db upgrade -d src/app/migrations
# Run custom CLI commands
flask --app src/app seed --count 50
flask --app src/app create-admin
# Run tests
pytest
# Lint and format
ruff check src tests
ruff format src tests
# Production (Gunicorn)
gunicorn "app:create_app()" --bind 0.0.0.0:8000 --workers 4 --chdir srcflask-cors.gunicorn as the entrypoint. Run flask db upgrade as a pre-start hook.flask db upgrade against a test DB, then pytest. Lint with ruff check.celery[redis] and create a celery_app.py that calls create_app() to get the Flask context. Use celery -A celery_app worker to start.flask-socketio with eventlet or gevent as the async driver.181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.