Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Flask Conventions

ASecurity

Flask web framework conventions: app factory pattern with create_app(), Blueprint registration with url_prefix, MethodView for class-based API views, Marshmallow schema validation, WTForms for HTML forms, Flask-Login for session auth, flask-jwt-extended for stateless API auth, Jinja2 template conventions, error handlers, and extension initialization. Activated automatically by flask-plugin/stack.md. Use this skill to: - Structure Flask applications with the app factory and per-feature Bluepr...

35 stars
0 votes
0 copies
0 views
Added 9/22/2026
developmentpythongobashsqlflasktestingapidatabasefrontend

Works with

cliapi

Security Analysis

A100/100

Scanned 9/22/2026

Install to Claude Code

$npx -y skills add AratKruglik/claude-sdlc --skill flask-conventions --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Flask Conventions?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Flask Conventions
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/aratkruglik-flask-conventions/badge)](https://www.skillsdirectory.com/skills/aratkruglik-flask-conventions)

More formats (shields.io, HTML) on the badges page.

Download with Pro
Files
SKILL.md
---
name: flask-conventions
description: |
  Flask web framework conventions: app factory pattern with create_app(), Blueprint registration with url_prefix, MethodView for class-based API views, Marshmallow schema validation, WTForms for HTML forms, Flask-Login for session auth, flask-jwt-extended for stateless API auth, Jinja2 template conventions, error handlers, and extension initialization. Activated automatically by flask-plugin/stack.md.

  Use this skill to:
  - Structure Flask applications with the app factory and per-feature Blueprints.
  - Validate JSON request data with Marshmallow and HTML forms with WTForms.
  - Implement session-based auth with Flask-Login or token auth with flask-jwt-extended.
  - Render Jinja2 templates safely or return JSON responses for API mode.
  - Register global error handlers for consistent error responses.

  Do NOT use this skill for:
  - SQLAlchemy ORM model patterns and Flask-Migrate — see flask-plugin:sqlalchemy-patterns.
  - Python language idioms — see python-foundation:python-conventions.
  - Testing patterns — see python-foundation:pytest-testing.
user-invocable: false
paths: ["**/*.py", "**/templates/**"]
---

# Flask Conventions

## Detection

Read `pyproject.toml` or `requirements.txt` before writing any Flask code:

```bash
grep -E "flask|Flask" requirements.txt pyproject.toml
```

Determine the installed extensions:

| Check | Meaning |
|---|---|
| `flask-login` present | Session-based auth — use `@login_required`, `login_user()`, `logout_user()` |
| `flask-jwt-extended` present | Token-based auth — use `@jwt_required()`, `create_access_token()` |
| `marshmallow` or `flask-marshmallow` present | JSON API validation — use `Schema.load()` / `Schema.dump()` |
| `flask-wtf` present | HTML form validation — use `FlaskForm` with `validate_on_submit()` |
| `flask-sqlalchemy` present | ORM — use `db.Model`, `db.session` |
| `flask-migrate` present | Migrations — `flask db migrate`, `flask db upgrade` |

---

## App factory

Define `create_app()` in `app/__init__.py`. Never instantiate `Flask` at module level in a way that creates side effects — the factory pattern allows multiple app instances for testing.

```python
# app/__init__.py
from flask import Flask

from app.config import config_by_name
from app.extensions import db, login_manager, migrate


def create_app(config_name: str = "development") -> Flask:
    app = Flask(__name__)
    app.config.from_object(config_by_name[config_name])

    _init_extensions(app)
    _register_blueprints(app)
    _register_error_handlers(app)

    return app


def _init_extensions(app: Flask) -> None:
    db.init_app(app)
    migrate.init_app(app, db)
    login_manager.init_app(app)


def _register_blueprints(app: Flask) -> None:
    from app.auth.views import auth_bp
    from app.users.views import users_bp
    from app.orders.views import orders_bp

    app.register_blueprint(auth_bp)
    app.register_blueprint(users_bp)
    app.register_blueprint(orders_bp)


def _register_error_handlers(app: Flask) -> None:
    from app.errors import register_error_handlers
    register_error_handlers(app)
```

Initialize extensions at **module level** in `app/extensions.py`, then call `.init_app(app)` in the factory. This avoids circular imports.

```python
# app/extensions.py
from flask_login import LoginManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
login_manager.login_view = "auth.login"
```

Config classes split per environment:

```python
# app/config.py
import os


class BaseConfig:
    SECRET_KEY = os.environ["SECRET_KEY"]
    SQLALCHEMY_TRACK_MODIFICATIONS = False


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["DATABASE_URL"]


class TestingConfig(BaseConfig):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
    WTF_CSRF_ENABLED = False


config_by_name = {
    "development": DevelopmentConfig,
    "production": ProductionConfig,
    "testing": TestingConfig,
}
```

Never hardcode `SECRET_KEY`. If the env var is missing, `os.environ["SECRET_KEY"]` raises `KeyError` at startup — fail fast rather than silently using a weak default.

---

## Blueprints

One Blueprint per feature with a `url_prefix`:

```python
# app/orders/views.py
from flask import Blueprint, jsonify, request
from flask_login import login_required

from app.orders.schemas import OrderSchema, OrderCreateSchema
from app.orders.service import get_order, create_order

orders_bp = Blueprint("orders", __name__, url_prefix="/orders")
order_schema = OrderSchema()
order_create_schema = OrderCreateSchema()


@orders_bp.route("/", methods=["GET"])
@login_required
def list_orders():
    orders = get_orders_for_current_user()
    return jsonify(order_schema.dump(orders, many=True))


@orders_bp.route("/", methods=["POST"])
@login_required
def create_order_view():
    data = order_create_schema.load(request.get_json())
    order = create_order(data)
    return jsonify(order_schema.dump(order)), 201
```

For class-based API views, use `MethodView`:

```python
from flask.views import MethodView


class OrderResource(MethodView):
    decorators = [login_required]

    def get(self, order_id: int):
        order = get_order(order_id)
        return jsonify(order_schema.dump(order))

    def put(self, order_id: int):
        data = order_schema.load(request.get_json(), partial=True)
        order = update_order(order_id, data)
        return jsonify(order_schema.dump(order))

    def delete(self, order_id: int):
        delete_order(order_id)
        return "", 204


orders_bp.add_url_rule(
    "/<int:order_id>",
    view_func=OrderResource.as_view("order_resource"),
)
```

---

## Marshmallow validation

Use Marshmallow for JSON API request validation and response serialization. Separate input and output schemas when field sets differ.

```python
# app/users/schemas.py
from marshmallow import Schema, ValidationError, fields, post_load, validates


class UserCreateSchema(Schema):
    email = fields.Email(required=True)
    password = fields.String(required=True, load_only=True)
    display_name = fields.String(required=True)

    @validates("password")
    def validate_password(self, value: str) -> None:
        if len(value) < 8:
            raise ValidationError("Password must be at least 8 characters.")

    @post_load
    def make_user_data(self, data: dict, **kwargs) -> dict:
        return data


class UserReadSchema(Schema):
    id = fields.Int(dump_only=True)
    email = fields.Email(dump_only=True)
    display_name = fields.String(dump_only=True)
    created_at = fields.DateTime(dump_only=True)
```

Usage in a view:

```python
user_create_schema = UserCreateSchema()
user_read_schema = UserReadSchema()


@users_bp.route("/", methods=["POST"])
def register():
    try:
        data = user_create_schema.load(request.get_json())
    except ValidationError as err:
        return jsonify({"errors": err.messages}), 422
    user = create_user(data)
    return jsonify(user_read_schema.dump(user)), 201
```

---

## WTForms

Use WTForms for **HTML form** validation (server-rendered Jinja2 mode). WTForms provides CSRF protection via Flask-WTF. Use `validate_on_submit()` which checks both `POST` and CSRF validity.

```python
# app/auth/forms.py
from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired, Email, Length


class LoginForm(FlaskForm):
    email = StringField("Email", validators=[DataRequired(), Email()])
    password = PasswordField("Password", validators=[DataRequired(), Length(min=8)])
    submit = SubmitField("Log In")
```

Usage in a view:

```python
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = authenticate_user(form.email.data, form.password.data)
        if user:
            login_user(user)
            return redirect(url_for("users.profile"))
        form.email.errors.append("Invalid credentials.")
    return render_template("auth/login.html", form=form)
```

**When to use WTForms vs Marshmallow:**
- WTForms: HTML form submissions (`Content-Type: application/x-www-form-urlencoded`), Jinja2 templates, CSRF protection needed.
- Marshmallow: JSON request/response (`Content-Type: application/json`), API mode, SPA frontends.

---

## Flask-Login

Use Flask-Login for **session-based auth** (browser clients, Jinja2 mode).

```python
# app/extensions.py
from flask_login import LoginManager

login_manager = LoginManager()
login_manager.login_view = "auth.login"  # redirect for @login_required


@login_manager.user_loader
def load_user(user_id: str):
    from app.users.models import User
    return User.query.get(int(user_id))
```

The `User` model must implement `UserMixin`:

```python
# app/users/models.py
from flask_login import UserMixin
from app.extensions import db


class User(UserMixin, db.Model):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True, nullable=False)
    hashed_password = db.Column(db.String(255), nullable=False)

    def get_id(self) -> str:
        return str(self.id)
```

Auth view functions:

```python
from flask_login import login_required, login_user, logout_user


@auth_bp.route("/login", methods=["POST"])
def login():
    user = authenticate_user(email, password)
    if user:
        login_user(user, remember=form.remember.data)
        return redirect(url_for("users.profile"))
    return jsonify({"error": "Invalid credentials"}), 401


@auth_bp.route("/logout", methods=["POST"])
@login_required
def logout():
    logout_user()
    return redirect(url_for("auth.login"))
```

---

## flask-jwt-extended

Use flask-jwt-extended for **token-based auth** (JSON API mode, SPA frontends).

```python
# app/extensions.py
from flask_jwt_extended import JWTManager

jwt = JWTManager()
```

Initialize in the factory:

```python
jwt.init_app(app)
```

Token creation in the login view:

```python
from flask_jwt_extended import create_access_token, create_refresh_token


@auth_bp.route("/login", methods=["POST"])
def login():
    data = login_schema.load(request.get_json())
    user = authenticate_user(data["email"], data["password"])
    if not user:
        return jsonify({"error": "Invalid credentials"}), 401
    access_token = create_access_token(identity=str(user.id))
    refresh_token = create_refresh_token(identity=str(user.id))
    return jsonify({"access_token": access_token, "refresh_token": refresh_token})
```

Protecting routes and getting the current identity:

```python
from flask_jwt_extended import get_jwt_identity, jwt_required


@users_bp.route("/me", methods=["GET"])
@jwt_required()
def get_current_user():
    user_id = get_jwt_identity()
    user = User.query.get(int(user_id))
    return jsonify(user_read_schema.dump(user))


@auth_bp.route("/refresh", methods=["POST"])
@jwt_required(refresh=True)
def refresh_token():
    user_id = get_jwt_identity()
    new_access_token = create_access_token(identity=user_id)
    return jsonify({"access_token": new_access_token})
```

---

## Jinja2 conventions

Organize templates under `app/templates/`. Use a base template with blocks:

```html
{# app/templates/base.html #}
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My App{% endblock %}</title>
</head>
<body>
    <nav>
        {% if current_user.is_authenticated %}
            <a href="{{ url_for('auth.logout') }}">Log out</a>
        {% else %}
            <a href="{{ url_for('auth.login') }}">Log in</a>
        {% endif %}
    </nav>
    {% block content %}{% endblock %}
</body>
</html>
```

```html
{# app/templates/users/profile.html #}
{% extends "base.html" %}
{% block title %}Profile — {{ user.display_name }}{% endblock %}
{% block content %}
<h1>{{ user.display_name }}</h1>
<p>{{ user.email }}</p>
{% endblock %}
```

Key Jinja2 rules:
- `{{ user.name }}` — auto-escaped. Safe for all user-controlled strings.
- `{{ content|safe }}` — bypasses auto-escaping. Only use for content sanitized with `bleach.clean()`.
- `{{ url_for('orders.list_orders') }}` — always use `url_for()` for URLs, never hardcode paths.
- Never use `Markup(user_input)` — only use `Markup()` for static, developer-controlled HTML fragments.

---

## Error handlers

Register error handlers in the factory. Detect the request's preferred response format from `request.accept_mimetypes`:

```python
# app/errors.py
from flask import Flask, jsonify, render_template, request


def register_error_handlers(app: Flask) -> None:
    @app.errorhandler(404)
    def not_found(error):
        if request.accept_mimetypes.best == "application/json":
            return jsonify({"error": "not found", "status": 404}), 404
        return render_template("errors/404.html"), 404

    @app.errorhandler(422)
    def unprocessable(error):
        return jsonify({"error": "unprocessable entity", "status": 422}), 422

    @app.errorhandler(500)
    def internal_error(error):
        if request.accept_mimetypes.best == "application/json":
            return jsonify({"error": "internal server error", "status": 500}), 500
        return render_template("errors/500.html"), 500
```

---

## Checklist

Before handing off to flask-migrate-specialist:

- [ ] All Blueprints registered via `app.register_blueprint()` in the factory?
- [ ] All extensions initialized with `init_app(app)`?
- [ ] `SECRET_KEY` read from `os.environ`, never hardcoded?
- [ ] No `flask db migrate` or `flask db upgrade` called?
- [ ] No `{{ var|safe }}` on user-controlled data in templates?
- [ ] No `Markup(user_input)` calls?
- [ ] Every state-changing route protected by `@login_required` or `@jwt_required()` (or explicitly BA-approved for anonymous access)?
- [ ] `ruff format .` run and output is clean?

Attribution

AratKruglikAratKruglik
View sourceMore from AratKruglik →
SSkills DirectorySkills Directory

Know which skills are safe — weekly.

Best new skills + every skill we flagged as malicious. From the team that scanned 103,619.

Join free

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Know which skills are safe — weekly.

Best new skills + every skill we flagged as malicious. From the team that scanned 103,619.

Join free

Related Skills

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

284722 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2192 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Tanstack Start

Build a full-stack TanStack Start app on Cloudflare Workers from scratch — SSR, file-based routing, server functions, D1+Drizzle, better-auth, Tailwind v4+shadcn/ui. Use whenever the user mentions TanStack Start, asks to scaffold a full-stack Cloudflare app with SSR, wants an SSR dashboard, or asks for a React 19 + Cloudflare Workers app with file-based routing and server functions — even if they don't name TanStack Start specifically. No template repo — Claude generates every file fresh per ...

9881 votes

Pentest

PTES-aligned adversarial security audit for backend, frontend, and mobile applications. Produces a CVSS-scored Hacker Report with verified PoCs and phased remediation.

5491 votes
View all in development →