CtrlK
BlogDocsLog inGet started
Tessl Logo

flask-project-starter

Scaffold a production-ready Flask 3.x application with application factory, Blueprints, SQLAlchemy, JWT auth, and Gunicorn deployment.

84

Quality

80%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

Optimize this skill with Tessl

npx tessl skill review --optimize ./backend-python/flask-project-starter/SKILL.md
SKILL.md
Quality
Evals
Security

Flask Project Starter

Scaffold a production-ready Flask 3.x application with application factory, Blueprints, SQLAlchemy, JWT auth, and Gunicorn deployment.

Prerequisites

  • Python 3.12+
  • PostgreSQL 15+ (recommended for production)
  • uv or pip for dependency management

Scaffold Command

mkdir -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 Structure

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

Key Conventions

  • Application factory pattern is mandatory. No global app object.
  • Extensions are instantiated without an app in extensions/__init__.py, then initialized in create_app().
  • One Blueprint per feature domain. Blueprints register their own routes and error handlers.
  • Config is class-based: BaseConfig, DevConfig, ProdConfig, TestConfig.
  • Models are imported into a central location so Flask-Migrate can detect them.
  • Use marshmallow for request/response serialization (not Pydantic -- Flask ecosystem convention).
  • JWT tokens via flask-jwt-extended. Store secrets in environment variables.

Essential Patterns

Config (config.py)

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,
}

Extensions (extensions/init.py)

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()

Application Factory (init.py)

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)

Error Handlers (errors/init.py)

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

Model (users/models.py)

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}>"

Schema (users/schemas.py)

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))

Blueprint + Routes (users/init.py and users/routes.py)

# 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 Blueprint (auth/init.py and auth/routes.py)

# 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))

CLI Commands (cli.py)

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.")

Test Conftest (tests/conftest.py)

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

Test Example (tests/test_users.py)

def 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

First Steps After Scaffold

  1. Copy .env.example to .env and fill in values
  2. Create virtual environment: python -m venv .venv && source .venv/bin/activate
  3. Install dependencies: pip install -e ".[dev]" (or uv sync)
  4. Run database migrations: flask --app src/app db upgrade -d src/app/migrations
  5. Start dev server: flask --app src/app run --debug --port 8000
  6. Test the API: curl http://localhost:8000/api/v1/users/

Common Commands

# 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

Integration Notes

  • Frontend: Flask serves JSON APIs. For SPA deployments, serve the frontend from a CDN and configure CORS with flask-cors.
  • Docker: Multi-stage build. Use gunicorn as the entrypoint. Run flask db upgrade as a pre-start hook.
  • CI: Run flask db upgrade against a test DB, then pytest. Lint with ruff check.
  • Celery: Add 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.
  • WebSockets: Use flask-socketio with eventlet or gevent as the async driver.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

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.