backend-python/flask-project-starter/SKILL.md
Scaffold a production-ready Flask 3.x application with application factory, Blueprints, SQLAlchemy, JWT auth, and Gunicorn deployment.
npx skillsauth add achreftlili/deep-dev-skills flask-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scaffold 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/migrations
project-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.py
app 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"}), 500
from 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 _db
def test_create_user(client):
response = client.post("/api/v1/users/", json={
"email": "[email protected]",
"username": "newuser",
"password": "securepass123",
})
assert response.status_code == 201
assert response.json["email"] == "[email protected]"
def test_login_invalid_credentials(client):
response = client.post("/api/v1/auth/login", json={
"email": "[email protected]",
"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 src
flask-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.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.