agentleFS
jmazzahacksjmazzahacks/byteforge-claude-skills/skills/flask-smorest-api/SKILL.md

flask-smorest-api

Skill1 starsChanged 7 days agoMITView on GitHub

Set up Flask REST API with flask-smorest, OpenAPI docs, blueprint architecture, and dataclass models. Use when creating a new Flask API server, building REST endpoints, or setting up a production API.

  • Reads credentials
  • Installs packages

The file

Show its rules and text

We split this skill into the rules it gives an agent

26 rules. About 0 are common, 14 are distinctive, and 12 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈14

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 12

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this skill when:

    line 12
  2. Distinctive:

    IMPORTANT: Before creating files, ask the user these questions:

    line 31
  3. Distinctive:

    Use this to derive:

    line 34
  4. Distinctive:

    Create these directories if they don't exist:

    line 52
  5. Very specific:

    Create {project_name}.py using this template:

    line 69
  6. Very specific:

    If the project uses the database, validate its settings at the very top of create_app().

    line 118
  7. Very specific:

    If the user opted in to a Swagger UI docs endpoint, add these three config lines to create_app() immediately after OPENAPI_VERSION, and add …

    line 146
  8. Very specific:

    If that path 404s, /swagger renders a broken shell.

    line 174
  9. Very specific:

    CRITICAL: Every handler above starts its except chain with except HTTPException: raise.

    line 364
  10. Distinctive:

    If the project needs shared services (database, API clients, etc.), create a singleton manager:

    line 378
  11. Very specific:

    Create or update example.env with required environment variables:

    line 494
  12. Very specific:

    Create requirements.txt with dependencies (no version pinning):

    line 540
  13. Very specific:

    Create dev-requirements.txt with test tooling.

    line 558
  14. Very specific:

    Use pytest.ini, not pyproject.toml: a Flask service usually has no pyproject.toml, and creating one just for pytest config invites …

    line 593
  15. Very specific:

    Keep the route test above and append this configuration test to tests/test_app.py:

    line 650
  16. Distinctive:

    Create or update README.md with:

    line 670
  17. Distinctive:

    If enabled, access Swagger UI at:

    line 727
  18. Distinctive:

    JSON Responses: - All endpoints return JSON via jsonify()

    line 755
  19. Distinctive:

    Environment-Based Config: - All secrets via environment variables

    line 758
  20. Distinctive:

    If database is needed, use postgres-setup skill first:

    line 767
  21. Very specific:

    Important: When running postgres-setup, also follow its Step 7 ("Create Resilient Database Driver") to scaffold …

    line 772
  22. Distinctive:

    If publishing as a package, use python-lib-setup skill:

    line 782
  23. Distinctive:

    If user requests Docker, reference the flask-docker-deployment skill for production-ready containerization with automated versioning and …

    line 821
  24. Distinctive:

    If you find a bug or stale instruction while using this skill:

    line 827
  25. Very specific:

    If HiveMake is connected and byteforge-skills-maintainer-agent is available, file a ticket with that agent.

    line 829
  26. Distinctive:

    If you cannot submit a report, give the human the details and this link.

    line 833

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Flask REST API with flask-smorest Pattern

This skill helps you set up a Flask REST API following a standardized pattern with flask-smorest for OpenAPI documentation, blueprint architecture, and dataclass models for request/response handling.

When to Use This Skill

Use this skill when:

  • Starting a new Flask REST API project
  • You want automatic OpenAPI/Swagger documentation
  • You need a clean, modular blueprint architecture
  • You want type-safe data models using dataclasses with to_dict/from_dict patterns
  • You're building a production-ready API server

What This Skill Creates

  1. Main application file - Flask app initialization with flask-smorest
  2. Blueprint structure - Modular endpoint organization
  3. Data models - Dataclasses with to_dict/from_dict methods and validation
  4. Singleton manager pattern - Centralized service/database initialization
  5. CORS support - Cross-origin request handling
  6. Requirements files - Runtime dependencies (requirements.txt) and dev/test tools (dev-requirements.txt)
  7. pytest.ini + tests/ - pytest scoped to tests/ with the project root importable, plus a DB-free smoke test

Step 1: Gather Project Information

IMPORTANT: Before creating files, ask the user these questions:

  1. "What is your project name?" (e.g., "myapp")
    • Use this to derive:
      • Main module: {project_name}.py (e.g., myapp.py)
      • Port number (suggest based on project, default: 5000)
  2. "What features/endpoints do you need?" (e.g., "users", "tokens", "orders")
    • Each feature will become a blueprint
  3. "Do you need database integration?" (yes/no)
    • If yes, reference the postgres-setup skill for the database layer. Use its Step 7 ("Create Resilient Database Driver") to scaffold src/{project_name}/database.py — the singleton manager below imports Database from that exact path. The resilient pattern (pre-ping + retry + mid-flight death detection) is what keeps the Flask process from wedging when Postgres restarts; the naïve ThreadedConnectionPool pattern returns 500s on every request until the process restarts.
  4. "What port should the server run on?" (default: 5000)
  5. "Do you want a Swagger UI docs endpoint at /swagger?" (yes/no — default no)
    • Default is no. Most internal services (tailnet-only, backend-only, no need for a browsable doc surface) should keep it off — an exposed Swagger UI is attack surface they never asked for, and flask-smorest's Api() / Blueprint / abort machinery all still work without any OPENAPI_URL_PREFIX / OPENAPI_SWAGGER_UI_* config (as long as API_TITLE / API_VERSION / OPENAPI_VERSION are set, which they are in the base template).
    • Say yes only if the service genuinely wants a browsable UI (public API, dev-portal, etc.). If yes, you'll be responsible for vendoring swagger-ui-dist assets under /static/swagger-ui/ (or an internal mirror) — this skill no longer defaults to a third-party CDN (previous versions pointed OPENAPI_SWAGGER_UI_URL at cdn.jsdelivr.net, which is a supply-chain surface and doesn't work in air-gapped/tailnet-only environments).

Step 2: Create Directory Structure

Create these directories if they don't exist:

{project_root}/
├── blueprints/          # Blueprint modules (one per feature)
│   ├── __init__.py
│   └── {feature}.py
├── models/              # Dataclass models with to_dict/from_dict
│   ├── __init__.py
│   └── {feature}.py
├── tests/               # pytest tests (see Step 9b)
│   └── test_app.py
├── pytest.ini           # pytest config (see Step 9b)
└── {project_name}.py    # Main application file

Step 3: Create Main Application File

Create {project_name}.py using this template:

import os
import logging
from flask import Flask
from flask_cors import CORS
from flask_smorest import Api
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def create_app():
    app = Flask(__name__)
    # These three are ALWAYS required by flask-smorest's `Api()`, even
    # when no Swagger UI is served — `Api()` refuses to initialize without
    # them. They govern the OpenAPI spec object flask-smorest builds in
    # memory (used for validation and, if enabled, doc-surface serving).
    app.config['API_TITLE'] = '{Project Name} API'
    app.config['API_VERSION'] = 'v1'
    app.config['OPENAPI_VERSION'] = '3.0.2'

    CORS(app)
    api = Api(app)

    from blueprints.{feature} import blp as {feature}_blp
    api.register_blueprint({feature}_blp)

    logger.info("Flask app initialized")
    return app

if __name__ == '__main__':
    port = int(os.environ.get('PORT', {port_number}))
    app = create_app()
    app.run(host='0.0.0.0', port=port)

CRITICAL: Replace:

  • {Project Name} → Human-readable project name (e.g., "My App")
  • {project_name} → Snake case project name (e.g., "myapp")
  • {port_number} → Actual port number (e.g., 5151)
  • {feature} → Feature name from user's response

Fail fast on missing database config (only if Step 1 Q3 = yes)

If the project uses the database, validate its settings at the very top of create_app(). Add the import with the others at the top of {project_name}.py:

from common import DatabaseConfig

and make this the first statement inside create_app():

    # Fail fast on a misconfigured deploy. Without this, a missing
    # {PROJECT_NAME}_DB_PASSWORD boots "healthy" (/health never touches the DB)
    # and only surfaces as 500s on the first DB request. Under gunicorn a
    # ValueError here fails the worker boot, so the container exits and
    # restart-count alerts fire.
    #
    # Validates env vars ONLY — it deliberately opens no connection. Do NOT
    # replace it with service_manager.get_database(): the driver opens pool
    # connections on construction, so a Postgres outage at boot would
    # crash-loop the service instead of letting the resilient driver
    # (postgres-setup Step 7) reconnect once Postgres is back. It would also
    # make the Step 9b smoke test need a live database.
    DatabaseConfig.from_env()

common.py is created in Step 6.

Swagger UI enrichment (only if the user answered YES to Step 1 Q5)

If the user opted in to a Swagger UI docs endpoint, add these three config lines to create_app() immediately after OPENAPI_VERSION, and add the startup-log line inside the __main__ block:

    # Only present when Swagger UI is opted in (Step 1 Q5).
    # Vendor `swagger-ui-dist` assets under `/static/swagger-ui/` (or an
    # internal mirror you control) and set OPENAPI_SWAGGER_UI_URL to the
    # path you're serving them from. DO NOT point this at cdn.jsdelivr.net
    # or any third-party CDN — prior skill versions did, which was both a
    # supply-chain surface and a hard break in air-gapped / tailnet-only
    # deployments.
    app.config['OPENAPI_URL_PREFIX'] = '/'
    app.config['OPENAPI_SWAGGER_UI_PATH'] = '/swagger'
    app.config['OPENAPI_SWAGGER_UI_URL'] = '/static/swagger-ui/'  # TODO: vendor

And inside the if __name__ == '__main__': block, immediately after app = create_app():

    logger.info(f"Swagger UI: http://localhost:{port}/swagger")

Vendoring reminder: whichever path you point OPENAPI_SWAGGER_UI_URL at, that path must actually serve the swagger-ui-dist JS/CSS bundle. The usual approach is pip install swagger-ui-bundle (or copy the files from the npm package) and configure Flask to serve them from /static/swagger-ui/. If that path 404s, /swagger renders a broken shell.

Step 4: Create Data Models

For each feature, create a models file with dataclasses that include to_dict and from_dict methods:

File: models/{feature}.py

from dataclasses import dataclass
from typing import Optional


@dataclass
class {Feature}:
    """
    {Feature} data model.

    Includes validation in from_dict and serialization via to_dict.
    """
    id: str
    name: str
    created_at: int
    updated_at: Optional[int] = None

    def to_dict(self) -> dict:
        """Serialize to dictionary for JSON response."""
        return {
            "id": self.id,
            "name": self.name,
            "created_at": self.created_at,
            "updated_at": self.updated_at
        }

    @classmethod
    def from_dict(cls, data: dict) -> "{Feature}":
        """
        Create instance from dictionary with validation.

        Args:
            data: Dictionary with {feature} data

        Returns:
            {Feature} instance

        Raises:
            ValueError: If required fields are missing or invalid
        """
        if "id" not in data:
            raise ValueError("id is required")
        if "name" not in data:
            raise ValueError("name is required")
        if "created_at" not in data:
            raise ValueError("created_at is required")

        return cls(
            id=str(data["id"]),
            name=str(data["name"]),
            created_at=int(data["created_at"]),
            updated_at=int(data["updated_at"]) if data.get("updated_at") else None
        )

CRITICAL: Replace:

  • {Feature} → PascalCase feature name (e.g., "TradableToken")
  • {feature} → Snake case feature name (e.g., "tradable_token")

Step 5: Create Blueprint Files

For each feature/endpoint, create a blueprint file:

File: blueprints/{feature}.py

Every blueprint uses url_prefix='/api'. This is a required convention, not a default to customize: nginx and request routing rely on one shared prefix across projects.

import logging
from flask import request, jsonify
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from werkzeug.exceptions import HTTPException

from models.{feature} import {Feature}

logger = logging.getLogger(__name__)

blp = Blueprint('{feature}', __name__, url_prefix='/api', description='{Feature} API')


@blp.route('/{feature}')
class {Feature}ListResource(MethodView):
    def get(self):
        """Get list of {feature}s."""
        try:
            limit = request.args.get('limit', 100, type=int)
            offset = request.args.get('offset', 0, type=int)

            # TODO: Implement logic to fetch {feature}s
            items = []

            return jsonify({
                "data": [item.to_dict() for item in items],
                "limit": limit,
                "offset": offset
            })
        except HTTPException:
            raise
        except ValueError as e:
            logger.warning(f"Bad request: {e}")
            abort(400, message=str(e))
        except Exception as e:
            logger.exception(f"Error fetching {feature}s: {e}")
            abort(500, message="Internal server error")

    def post(self):
        """Create a new {feature}."""
        try:
            data = request.get_json()
            if not data:
                abort(400, message="Request body is required")

            item = {Feature}.from_dict(data)

            # TODO: Implement logic to save {feature}

            return jsonify(item.to_dict()), 201
        except HTTPException:
            raise
        except ValueError as e:
            logger.warning(f"Validation error: {e}")
            abort(400, message=str(e))
        except Exception as e:
            logger.exception(f"Error creating {feature}: {e}")
            abort(500, message="Internal server error")


@blp.route('/{feature}/<string:item_id>')
class {Feature}Resource(MethodView):
    def get(self, item_id: str):
        """Get a single {feature} by ID."""
        try:
            # TODO: Implement logic to fetch {feature} by ID
            item = None

            if not item:
                abort(404, message=f"{Feature} not found: {item_id}")

            return jsonify(item.to_dict())
        except HTTPException:
            raise
        except Exception as e:
            logger.exception(f"Error fetching {feature}: {e}")
            abort(500, message="Internal server error")

    def put(self, item_id: str):
        """Update a {feature}."""
        try:
            data = request.get_json()
            if not data:
                abort(400, message="Request body is required")

            # TODO: Implement logic to update {feature}

            return jsonify({"message": "Updated"})
        except HTTPException:
            raise
        except ValueError as e:
            logger.warning(f"Validation error: {e}")
            abort(400, message=str(e))
        except Exception as e:
            logger.exception(f"Error updating {feature}: {e}")
            abort(500, message="Internal server error")

    def delete(self, item_id: str):
        """Delete a {feature}."""
        try:
            # TODO: Implement logic to delete {feature}

            return jsonify({"message": "Deleted"})
        except HTTPException:
            raise
        except Exception as e:
            logger.exception(f"Error deleting {feature}: {e}")
            abort(500, message="Internal server error")

CRITICAL: Replace:

  • {Feature} → PascalCase feature name (e.g., "TradableToken")
  • {feature} → Snake case feature name (e.g., "tradable_token")

CRITICAL: Every handler above starts its except chain with except HTTPException: raise. This arm is REQUIRED — flask_smorest.abort() raises werkzeug.exceptions.HTTPException, which is an Exception subclass, so any abort(400, ...) / abort(404, ...) fired inside a try block will otherwise be swallowed by the terminal except Exception arm and re-emitted as abort(500, "Internal server error") — clients get 500s for their own bad input, and logs fill with spurious logger.exception stack traces for routine validation. When adding a new handler, keep this arm as the FIRST except clause. Move unconditional aborts (e.g., if not data: abort(400)) outside the try when possible; the arm still catches the inevitable in-try aborts (if not item: abort(404) after a lookup).

Step 6: Create Common Singleton Manager (If Needed)

If the project needs shared services (database, API clients, etc.), create a singleton manager:

File: common.py

"""
Singleton manager for shared service instances.

Provides centralized initialization of database connections, API clients,
and other shared resources.
"""

import os
import logging
from dataclasses import dataclass


logger = logging.getLogger(__name__)


@dataclass
class DatabaseConfig:
    """Database connection settings, read from the same env vars postgres-setup provisions against."""
    host: str
    port: int
    name: str
    user: str
    password: str

    @classmethod
    def from_env(cls) -> "DatabaseConfig":
        """
        Read and validate database settings from the environment.

        Opens NO connection, so it is safe to call at startup even while
        Postgres is down. create_app() calls it to fail fast on a
        misconfigured deploy; get_database() calls it to build the driver.

        Raises:
            ValueError: If the password is missing/empty or the port is not an integer
        """
        # `{PROJECT_NAME}_DB_PORT` MUST be read here — the setup script
        # (postgres-setup Step 4) provisions against it, and skipping it here
        # silently dials 5432 (Docker-mapped 5433 or multi-instance hosts hit
        # this). Ticket 881aa10a fix — five env vars, four surfaces, one invariant.
        password = os.environ.get('{PROJECT_NAME}_DB_PASSWORD')
        if not password:
            raise ValueError("{PROJECT_NAME}_DB_PASSWORD environment variable required")

        return cls(
            host=os.environ.get('{PROJECT_NAME}_DB_HOST', 'localhost'),
            port=int(os.environ.get('{PROJECT_NAME}_DB_PORT', '5432')),
            name=os.environ.get('{PROJECT_NAME}_DB_NAME', '{project_name}'),
            user=os.environ.get('{PROJECT_NAME}_DB_USER', '{project_name}'),
            password=password,
        )


class ServiceManager:
    """
    Singleton manager for shared service instances.

    Ensures only one instance of each service is created and reused
    across all blueprints.
    """

    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(ServiceManager, cls).__new__(cls)
            cls._instance._initialized = False
        return cls._instance

    def __init__(self):
        if self._initialized:
            return

        # Initialize services
        self._db = None
        self._initialized = True
        logger.info("ServiceManager initialized")

    def get_database(self):
        """
        Get database connection instance.

        Returns:
            Database connection instance (lazy initialization)
        """
        if self._db is None:
            # Import database driver — see postgres-setup Step 7 for the
            # resilient implementation (pre-ping + retry; survives PG restart).
            from src.{project_name}.database import Database

            # Connection settings come from DatabaseConfig so startup
            # validation (create_app) and the real driver read identical values.
            config = DatabaseConfig.from_env()
            self._db = Database(config.host, config.name, config.user, config.password, db_port=config.port)
            logger.info("Database connection initialized")

        return self._db


# Global singleton instance
service_manager = ServiceManager()

CRITICAL: Replace:

  • {PROJECT_NAME} → Uppercase project name (e.g., "MYAPP")
  • {project_name} → Snake case project name (e.g., "myapp")

Step 7: Create Environment Configuration

File: example.env

Create or update example.env with required environment variables:

# Server Configuration
PORT={port_number}
DEBUG=False

# Database Configuration (if applicable)
{PROJECT_NAME}_DB_HOST=localhost
{PROJECT_NAME}_DB_PORT=5432
{PROJECT_NAME}_DB_NAME={project_name}
{PROJECT_NAME}_DB_USER={project_name}
{PROJECT_NAME}_DB_PASSWORD=your_password_here

# Optional: CORS Configuration
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080

CRITICAL: Replace:

  • {port_number} → Actual port number (e.g., 5151)
  • {PROJECT_NAME} → Uppercase project name (e.g., "MYAPP")
  • {project_name} → Snake case project name (e.g., "myapp")

File: .env (gitignored)

Instruct the user to copy example.env to .env and fill in actual values:

# Copy example.env to .env and update with actual values
cp example.env .env

Update .gitignore

Merge .env into .gitignore if not already present. When the venv lives at the project root (python3 -m venv .), also add /.lock: uv uses it for environment locking. Preserve the existing ignore rules; uv.lock is a separate dependency lockfile and must remain trackable.

# Environment variables
.env

# uv writes this transient lock into the venv root (= project root here).
/.lock

Step 8: Create Requirements File

Create requirements.txt with dependencies (no version pinning):

# Flask and API framework
Flask
flask-smorest
flask-cors

# Environment variable management
python-dotenv

# Production server (optional but recommended)
gunicorn

# Database (if needed)
psycopg2-binary

Create dev-requirements.txt with test tooling. Keep it separate from requirements.txt: a service's requirements.txt is what gets installed into its Docker image, and test tools don't belong there (same split as the uv-supply-chain-hardening skill uses for apps):

pytest

Step 9: Create Blueprints init.py

Create blueprints/__init__.py:

"""
Blueprint modules for {Project Name} API.

Each blueprint represents a distinct feature or resource endpoint.
"""

Step 9b: Create pytest.ini and a Smoke Test

File: pytest.ini

[pytest]
# REQUIRED. The venv lives at the project root (bin/, lib/, include/), so a
# bare `pytest` otherwise collects the venv's own site-packages tests and dies
# on third-party collection errors before running a single project test.
testpaths = tests
# REQUIRED. The app modules ({project_name}.py, blueprints/, models/, common.py)
# live at the project root, not in an installed package. pytest's default
# import mode puts tests/ on sys.path, not the root, so without this line
# `import {project_name}` fails with ModuleNotFoundError.
pythonpath = .

Use pytest.ini, not pyproject.toml: a Flask service usually has no pyproject.toml, and creating one just for pytest config invites confusion with the library skills.

File: tests/conftest.py

Build one app per test session. This also works when byteforge-prometheus-metrics is added: repeated successful factory calls would register app_info twice in the global Prometheus registry. Keep PROMETHEUS_MULTIPROC_DIR unset before starting these single-process tests. The app module must not also instantiate the factory at import time; if it intentionally exports an already-created app, return that instance from the fixture instead.

import pytest
from flask import Flask

from {project_name} import create_app


@pytest.fixture(scope='session')
def app() -> Flask:
    app = create_app()
    app.config['TESTING'] = True
    return app

File: tests/test_app.py

from flask import Flask


def test_create_app_registers_routes(app: Flask) -> None:
    # Checks wiring only — no requests, so TODO handlers cannot touch Postgres.
    # Keep create_app() itself free of live-DB requirements.
    rules = [rule.rule for rule in app.url_map.iter_rules()]
    assert '/api/{feature}' in rules

CRITICAL: Replace:

  • {project_name} → Snake case project name (e.g., "myapp")
  • {feature} → the first feature's snake case name (e.g., "tradable_token")

If the project uses the database (Step 3's fail-fast check is present), use this version of tests/conftest.py instead. Settings are validated without opening a connection, so a dummy password is enough — no Postgres needed. MonkeyPatch.context() works at session scope and restores the environment at teardown; the built-in monkeypatch fixture is function-scoped and cannot be injected into this fixture.

from collections.abc import Iterator

import pytest
from flask import Flask

from {project_name} import create_app


@pytest.fixture(scope='session')
def app() -> Iterator[Flask]:
    with pytest.MonkeyPatch.context() as env:
        env.setenv('{PROJECT_NAME}_DB_PASSWORD', 'test-not-a-real-password')
        app = create_app()
        app.config['TESTING'] = True
        yield app

Keep the route test above and append this configuration test to tests/test_app.py:

import pytest

from {project_name} import create_app


def test_create_app_fails_fast_without_db_password(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.delenv('{PROJECT_NAME}_DB_PASSWORD', raising=False)
    with pytest.raises(ValueError, match='{PROJECT_NAME}_DB_PASSWORD'):
        create_app()

This direct factory call is safe because Step 3 validates settings before metrics registration. Match the configuration error so a duplicate-metric error cannot make the test pass accidentally. All tests needing a successfully initialized app should request the shared fixture. Share the app, but create a fresh test client per test to keep cookies/session state isolated.

Also replace {PROJECT_NAME} → uppercase project name (e.g., "MYAPP").

Step 10: Document Usage

Create or update README.md with:

Setup

# Copy example environment file
cp example.env .env

# Edit .env and fill in actual values
# Create and activate the virtual environment (at the project root)
python3 -m venv .
source bin/activate

# Then install dependencies
pip install --upgrade -r requirements.txt
pip install --upgrade -r dev-requirements.txt

Running Tests

source bin/activate && pytest

pytest.ini scopes collection to tests/ and puts the project root on the import path, so a bare pytest from the project root works.

Running the Server

# Development mode
python {project_name}.py

# Production mode with Gunicorn
gunicorn -w 4 -b 0.0.0.0:{port_number} '{project_name}:create_app()'

Environment Variables

Copy example.env to .env and configure:

Server Configuration:

  • PORT - Server port (default: {port_number})
  • DEBUG - Enable debug mode (default: False)

Database (if applicable):

  • {PROJECT_NAME}_DB_HOST - Database host (default: localhost)
  • {PROJECT_NAME}_DB_PORT - Database port (default: 5432)
  • {PROJECT_NAME}_DB_NAME - Database name (default: {project_name})
  • {PROJECT_NAME}_DB_USER - Database user (default: {project_name})
  • {PROJECT_NAME}_DB_PASSWORD - Database password (REQUIRED — the app refuses to start without it)

The app checks that these are set when it starts, but does not connect to Postgres until the first request that needs it — so it starts even if Postgres is temporarily down, and fails immediately if the password is missing.

API Documentation

(Only applicable if Swagger UI was opted in during Step 1 Q5.)

If enabled, access Swagger UI at:

http://localhost:{port_number}/swagger

Requires swagger-ui-dist assets to be served under OPENAPI_SWAGGER_UI_URL; see the Swagger UI enrichment note in Step 3.

Design Principles

This pattern follows these principles:

Architecture:

  1. Blueprint Organization - Modular endpoint organization, one blueprint per feature
  2. MethodView Classes - Class-based views for HTTP methods (get, post, put, delete)
  3. Separation of Concerns - Routes, models, and business logic separated
  4. Singleton Manager - Centralized service initialization prevents duplicate connections
  5. Application Factory - create_app() pattern for testing and flexibility

Data Models:

  1. Dataclasses - Type-safe data models using Python dataclasses
  2. to_dict/from_dict - Consistent serialization and deserialization pattern
  3. Validation in from_dict - Input validation with clear error messages
  4. Self-Contained - Each model handles its own validation and serialization

API Design:

  1. OpenAPI/Swagger - Automatic documentation via flask-smorest
  2. Error Handling - Consistent error responses with proper HTTP status codes
  3. CORS Support - Cross-origin requests for frontend consumption
  4. JSON Responses - All endpoints return JSON via jsonify()

Best Practices:

  1. Environment-Based Config - All secrets via environment variables
  2. Logging - Structured logging throughout
  3. Idempotent Operations - Safe to call multiple times
  4. Production Ready - Gunicorn support out of the box
  5. Testing Friendly - Application factory enables easy testing

Integration with Other Skills

Database Integration

If database is needed, use postgres-setup skill first:

User: "Set up postgres database for my project"

Important: When running postgres-setup, also follow its Step 7 ("Create Resilient Database Driver") to scaffold src/{project_name}/database.py. The singleton manager above imports Database from that path. Skipping Step 7 and using a naïve ThreadedConnectionPool will wedge the Flask process every time Postgres restarts.

Then reference the database in your blueprints via the singleton manager:

from common import service_manager

db = service_manager.get_database()

Package Structure

If publishing as a package, use python-lib-setup skill:

User: "Set up Python package for PyPI"

Example Usage in Claude Code

User: "Set up Flask API server for my project"

Claude: "What is your project name?"

User: "crypto-tracker"

Claude: "What features/endpoints do you need?"

User: "prices, tokens, portfolio"

Claude: "Do you need database integration?"

User: "yes"

Claude: "What port should the server run on?"

User: "8080"

Claude:

  1. Creates crypto_tracker.py with Flask app
  2. Creates models/ directory with dataclass models:
    • prices.py, tokens.py, portfolio.py
  3. Creates blueprints/ directory with endpoint handlers:
    • prices.py, tokens.py, portfolio.py
  4. Creates common.py with ServiceManager singleton
  5. Creates requirements.txt with dependencies and dev-requirements.txt with pytest
  6. Creates pytest.ini and tests/test_app.py (DB-free smoke test)
  7. Documents environment variables needed
  8. Provides startup and test instructions

Optional: Docker Support

If user requests Docker, reference the flask-docker-deployment skill for production-ready containerization with automated versioning and health checks.


Reporting problems with this skill

If you find a bug or stale instruction while using this skill:

  • If HiveMake is connected and byteforge-skills-maintainer-agent is available, file a ticket with that agent. Use discover_agents to find its project ID.
  • Otherwise, open an issue at GitHub Issues. If you cannot submit a report, give the human the details and this link.

Include the skill name, plugin version if known, the instruction followed, expected and actual results, relevant errors/runtime versions, and any fix or workaround that worked. Omit credentials and private data.

Address the user's immediate problem first; report the skill defect afterward.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.