Skip to content
cccskills
FreePublished on GitHub

flask-conventions

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.

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md13.8 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Flask Conventions

Detection

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

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

Determine the installed extensions:

CheckMeaning
flask-login presentSession-based auth — use @login_required, login_user(), logout_user()
flask-jwt-extended presentToken-based auth — use @jwt_required(), create_access_token()
marshmallow or flask-marshmallow presentJSON API validation — use Schema.load() / Schema.dump()
flask-wtf presentHTML form validation — use FlaskForm with validate_on_submit()
flask-sqlalchemy presentORM — use db.Model, db.session
flask-migrate presentMigrations — 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.

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

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

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

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

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.

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

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.

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

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

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

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

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

# app/extensions.py
from flask_jwt_extended import JWTManager

jwt = JWTManager()

Initialize in the factory:

jwt.init_app(app)

Token creation in the login view:

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:

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:

{# 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>
{# 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:

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

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Angular 18-21 project structure, standalone components vs NgModule, control flow (@if/@for/@switch + *ngIf/*ngFor legacy), decorators, dependency injection (inject() function), lifecycle hooks, pipes, Angular Universal SSR pointer. Use this skill to: - Detect project style (standalone vs NgModule) and apply matching patterns. - Pick correct decorators and DI approach. - Use modern control flow (@if/@for/@switch) in Angular 17+ projects. - Apply `inject()` function over constructor injection where appropriate. - Wire bootstrap correctly (bootstrapApplication for standalone, AppModule for legacy). Do NOT use this skill for: - State management (see angular-state-and-rx). - Routing (see angular-routing). - Forms (see angular-forms). - Testing (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Angular forms: Reactive Forms (preferred — typed FormGroup/FormControl since Angular 14, FormBuilder, custom + async validators, FormArray, multi-step) and Template-driven (`[(ngModel)]` + FormsModule). Validation strategies, server error mapping, accessibility. Use this skill to: - Build Reactive Forms with typed FormGroup/FormControl. - Use FormBuilder для concise syntax. - Implement custom synchronous and async validators. - Wire FormArray for dynamic field lists. - Map server errors back to form fields. - Pick Reactive vs Template-driven (prefer Reactive). Do NOT use this skill for: - General conventions (see angular-conventions). - State management beyond forms (see angular-state-and-rx). - Routing (see angular-routing). - Testing forms (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Angular Router (built-in `@angular/router`) — route configuration for standalone and NgModule projects, functional guards (Angular 14.1+), lazy loading, route resolvers, typed params via signals/observables, programmatic navigation, route data and meta. Use this skill to: - Configure routes (standalone-style or NgModule-style). - Use functional guards (canActivate as function, preferred over class-based in 17+). - Lazy-load components or feature modules. - Implement auth guards via route meta + functional guards. - Read params/queries via `inject(ActivatedRoute)` + signals or RxJS. Do NOT use this skill for: - General conventions (see angular-conventions). - State management (see angular-state-and-rx). - Forms (see angular-forms). - Testing routes (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

State management for Angular 18-21: signals (signal/computed/effect), services-as-state, NgRx Store + Effects + Selectors, NgRx Component Store, NgRx Signals (newer signal-based store). RxJS essentials — operators, async pipe, takeUntilDestroyed, signal/observable interop. Use this skill to: - Pick the right state tool (signals / services / NgRx variant / vue-query equivalent). - Use signals correctly (signal/computed/effect — when each). - Build a Pinia-style service-as-state singleton. - Set up NgRx Store + Effects + Selectors. - Use RxJS without leaking subscriptions (async pipe, takeUntilDestroyed, Subject patterns). - Bridge signals ↔ observables via toSignal / toObservable. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing state (see angular-routing). - Form state (see angular-forms). - Testing state (see angular-testing).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Testing Angular 18-21: TestBed, component harnesses (@angular/cdk/testing), Karma+Jasmine (default historical) vs Jest (jest-preset-angular, modern), Angular Testing Library (RTL-style). HttpClient mocking via HttpTestingController. NgRx Effects testing. Cypress / Playwright e2e. Use this skill to: - Detect runner (Karma+Jasmine vs Jest) and configure correctly. - Write component tests with TestBed. - Use component harnesses for Material / custom UI components. - Mock HttpClient via provideHttpClientTesting + HttpTestingController. - Test signal-based inputs with componentRef.setInput(). - Test NgRx Effects with provideMockActions. Do NOT use this skill for: - General Angular conventions (see angular-conventions). - Routing patterns broadly (see angular-routing — covers testing routes briefly). - Form patterns broadly (see angular-forms).

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

Shared conventions for every SDLC development-phase architect agent: hard rules, code quality bar, workflow steps (superpowers invocation, spec reading, codebase exploration, verification), and the report/compact-summary contract. Architects load this skill first, then apply their stack-specific instructions on top.

The English summary is being prepared. Showing the original description.

AratKruglik/claude-sdlc47Updated Oct 17, 2026

See all skills by AratKruglik

Report a problem with this skill