# Flaskの大規模アプリ構成：アプリケーションファクトリ（create_app）とBlueprintで循環importを避けて拡張する

> Flask 3.1系の大規模アプリ構成を本番品質で設計する実践ガイド。グローバルappと循環importの破綻、create_appアプリケーションファクトリ、extensions.pyの裸の拡張→init_app、Blueprintのurl_prefix・endpoint命名・入れ子・テンプレート/静的ファイル・エラーハンドラ・CLI、from_object+from_prefixed_envの環境別Config、src/レイアウトとYAGNIの分割規律を公式ドキュメントに忠実な実コードで解説します。

- 公開日: 2026-06-25
- 著者: 友田 陽大
- タグ: Python, Flask, Blueprint, アーキテクチャ設計, 本番運用, バックエンド
- URL: https://tomodahinata.com/blog/flask-application-factory-blueprints-large-app-structure-guide

## 要点

- グローバルなappをモジュールトップに置くと、テストで設定を差し替えられず、複数環境を持てず、ビューとモデルが循環importする。これが大規模Flaskの三大破綻
- create_app(test_config=None)のアプリケーションファクトリが背骨。instance_relative_config=Trueとtest_config分岐で、テスト容易性と複数環境を同時に解決する
- 拡張はextensions.pyで裸（db = SQLAlchemy()）に生成し、ファクトリ内でinit_app(app)して束縛する。1つの拡張オブジェクトが複数アプリに再利用でき、循環importが消える
- Blueprintのnameはendpointの前綴りでありURLではない。url_prefix・入れ子・blueprint-localなテンプレート/静的ファイル・errorhandlerの404捕捉不可・CLIグループには公式仕様上の落とし穴がある
- 環境別ConfigはBase→Production/Testの継承＋from_object→from_prefixed_envの二段構え。Blueprintへの分割は『3つ目で抽出』のYAGNI規律で判断する

---

## **導入：Flask アプリは「機能不足」ではなく「構造不足」で壊れる**

Flask の案件が技術的負債に沈むとき、原因はほとんど Flask の機能不足ではありません。**構造の欠如**です。`app = Flask(__name__)` を 1 ファイルのトップに書いて始めたアプリが、ルートとモデルとフォームを足していくうちに、ある日 `ImportError: cannot import name 'app' from partially initialized module` で起動しなくなる——これは Flask を触ったことのある人なら一度は見た光景でしょう。

この破綻は偶然ではなく、構造的な必然です。グローバルな `app` を中心に据えた瞬間、ビューもモデルも拡張も「`app` を import する」ことを強いられ、その `app` 側もビューを import する——依存が円を描き、import 順序という最も脆い前提にアプリ全体がぶら下がります。

本記事は、この破綻を **Flask 3.1 系（現行安定版、本稿執筆時点で 3.1.3）の公式ドキュメントに忠実**に解消する設計——**アプリケーションファクトリ（`create_app`）**と **Blueprint**、そして両者をつなぐ **`extensions.py` の `init_app` パターン**——を、本番品質で組み上げる深掘りガイドです。これは [Flask 本番運用ガイド](/blog/flask-production-guide) の §1（アプリ構成）と §3（ファクトリ）を、循環 import の回避と Blueprint の落とし穴という観点から徹底的に展開したスポークにあたります。全体像はまずピラーを参照してください。

筆者は、**経済産業大臣賞を受賞した B2B SaaS のバックエンドを Python / Flask / SQLAlchemy / PostgreSQL で設計・実装し、API Gateway → ALB → ECS(Fargate) 上で本番運用**してきました。エンドポイントが増え、認証・課金・管理画面が別々の関心事として育っていく中で、ここで示すファクトリと Blueprint の規律だけが、コードベースを「変更しやすい状態（ETC）」に保ち続けました。本記事はその実戦の設計判断を、公式仕様に照らして言語化したものです。

> 💡 **この記事で扱うバージョン**：**Flask 3.1 系**（最新安定版 3.1.3）を前提とします。Flask 3.1 は **Python 3.9 以上**が必要です。なお Flask 3.0 で `Flask.__version__` は廃止されたため、バージョンを取得したいときは `importlib.metadata.version("flask")` を使ってください（後述）。本稿のコードは公式ドキュメントのパターンに基づきます。

---

## **1. なぜ単一ファイルを超えるのか：グローバル `app` と循環 import の破綻**

設計を語る前に、**何が壊れるのか**を正確に理解します。Flask の最小形はこうでした。

```python
# hello.py
from flask import Flask

app = Flask(__name__)


@app.route("/")
def index():
    return "Hello, World!"
```

学習にはこれで十分です。問題は、機能が増えてビューやモデルを別ファイルに切り出した瞬間に起きます。典型的な「壊れ方」を再現します。

```python
# models.py — モデルが app を欲しがる
from app import app   # ← app.py を import

db = ...  # app.config を読んで初期化したい


# app.py — app がビュー（モデルを使う）を欲しがる
from flask import Flask

app = Flask(__name__)

import models   # ← models.py を import（models は app を import する）
```

`app.py` を起動すると、`import models` の行で `models.py` が `from app import app` を実行します。しかしこの時点で `app.py` はまだ `import models` の行を実行中で、**モジュールの初期化が完了していません**。結果、`app` という名前がまだ束縛されておらず、`ImportError: cannot import name 'app' from partially initialized module 'app'`（循環 import）で落ちます。

この破綻には、3 つの独立した症状があります。

| 症状 | 何が起きるか | 根本原因 |
|---|---|---|
| **テストで設定を差し替えられない** | `app` は import 時に確定するので、テスト用 DB やテスト設定に切り替える隙がない | `app` がモジュールトップで即時生成される |
| **複数構成を同時に持てない** | 本番・ステージング・テストで異なる設定のアプリを並行して作れない | `app` がモジュール内でシングルトン化している |
| **循環 import を誘発する** | ビュー/モデルが `app` を import し、`app` 側もそれらを import する | 依存方向が双方向になっている |

> ⚠️ **アンチパターン**：「とりあえず動くから」とグローバル `app` のまま機能を足し続け、循環 import が出たら `import` 文を関数の中に押し込んで（遅延 import で）その場しのぎをする。これは負債を隠すだけで、依存が円を描いている事実は消えません。次節のアプリケーションファクトリは、**依存方向を「上から下」に一方向化**することで、この円を構造から断ち切ります。

---

## **2. アプリケーションファクトリ：`create_app` を深掘りする**

解決策は **アプリケーションファクトリ**です。`app` をモジュールトップで生成するのをやめ、**関数の中で組み立てて返す**。「いつ・どの設定で・どの部品を載せて」アプリを作るかを、呼び出し側が完全に制御できるようになります。

### 2.1 公式チュートリアルの正準形

Flask 公式チュートリアル（Flaskr）が示すファクトリの正準形がこれです。すべての要素に意味があるので、一行ずつ読み解きます。

```python
# src/myapp/__init__.py
import os

from flask import Flask


def create_app(test_config: dict | None = None) -> Flask:
    # ① instance_relative_config=True で、設定をリポジトリ外の instance/ から読む
    app = Flask(__name__, instance_relative_config=True)

    # ② コード内の安全な既定値（本番では必ず上書きする）
    app.config.from_mapping(
        SECRET_KEY="dev",
        DATABASE=os.path.join(app.instance_path, "myapp.sqlite"),
    )

    if test_config is None:
        # ③ 本番/開発：instance/config.py があれば上書き（無ければ黙ってスキップ）
        app.config.from_pyfile("config.py", silent=True)
    else:
        # ④ テスト：呼び出し側が渡した設定で上書き
        app.config.from_mapping(test_config)

    # ⑤ instance フォルダは Flask が自動生成しない。明示的に作る
    os.makedirs(app.instance_path, exist_ok=True)

    @app.route("/health")
    def health():
        return {"status": "ok"}

    return app
```

- **① `instance_relative_config=True`**：設定ファイルの解決基点を、パッケージ内ではなく**インスタンスフォルダ**（`app.instance_path`、既定で `<project>/instance/`）に切り替えます。これにより秘密情報を含む設定をリポジトリ外に置けます（§6.2）。
- **② `from_mapping(...)`**：コードに直書きできる**安全な既定値**だけをここに置きます。`SECRET_KEY="dev"` は開発用のダミーで、本番では必ず上書きされる前提です。
- **③④ `test_config` 分岐**：**この分岐がファクトリの心臓**です。引数が `None`（通常起動）なら `instance/config.py` から本番設定を読み、引数が渡されれば（テスト）その設定で上書きします。テストは「実ファイルを置かずに」設定を注入できます。
- **⑤ `os.makedirs(app.instance_path, exist_ok=True)`**：**Flask はインスタンスフォルダを自動生成しません**。SQLite やアップロードをそこに置くなら、ファクトリで明示的に作る必要があります。公式チュートリアルもこの一行を入れています。

起動は `flask` CLI がファクトリを自動検出します。

```bash
flask --app myapp run --debug
```

### 2.2 ファクトリの自動検出と引数渡し

`flask --app` は、対象モジュールに **`create_app` または `make_app`** という名前の関数があれば、それをファクトリとして自動的に呼び出します。引数を渡したいときは、括弧内を **Python リテラル**として書けます。

```bash
# 引数なしのファクトリを自動検出
flask --app myapp run --debug

# ファクトリに文字列リテラル "dev" を渡す（括弧内は Python リテラルとして解釈）
flask --app 'hello:create_app("dev")' run
```

この書式は本番の Gunicorn でも同じです。`gunicorn 'myapp:create_app()'` のようにファクトリを直接指せます。Gunicorn のワーカー数・`ProxyFix`・Docker 化といった本番デプロイの詳細は [本番デプロイガイド](/blog/flask-deployment-gunicorn-docker-production-wsgi-guide) に集約しています。

### 2.3 なぜファクトリがテスト容易性と複数環境を「同時に」解決するのか

ファクトリの本質的な価値は、**アプリの生成を「遅延」させ「パラメータ化」する**ことにあります。

- **テスト容易性**：`create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:"})` のように、テストごとにクリーンな設定でアプリを生成できます。pytest の fixture でアプリを作り直せば、テスト間の状態汚染も起きません（テストの詳細は [テスト実践ガイド](/blog/flask-testing-pytest-test-client-fixtures-guide) へ）。
- **複数環境**：同じ `create_app` を、本番は環境変数の設定で、テストは `test_config` 引数で呼び分けるだけです。環境ごとにコードを分岐させる必要がありません。

> 💡 **ファクトリは「将来の拡張」ではなく「現在の要件」**：アプリケーションファクトリを「規模が大きくなったら導入する」ものと捉えるのは誤りです。**テスト容易性は最初から必要な要件**であり、ファクトリはそれを満たす最小の構造です。小さなアプリでも `create_app` と `extensions.py` の分離だけは最初から入れる——これが負債を生まない出発点です（§7 の YAGNI 規律と矛盾しません。分割すべきは Blueprint であって、ファクトリの導入は別問題です）。

---

## **3. `extensions.py` パターン：裸の拡張 → `init_app` で束縛する**

ファクトリが循環 import を断ち切る鍵は、**拡張オブジェクトの扱い方**にあります。Flask-SQLAlchemy や Flask-Migrate のような拡張を、`app` と一緒に生成してしまうと、再び循環 import の罠に戻ります。公式が示す解法は明快です——**拡張を「アプリに束縛されていない裸の状態」で生成し、ファクトリ内で `init_app(app)` を呼んで初めて束縛する**。

### 3.1 二段階初期化の構造

```python
# src/myapp/extensions.py — どのアプリにも束縛されていない「裸」の拡張
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()      # この時点では app を知らない
migrate = Migrate()    # この時点では app を知らない
```

```python
# src/myapp/__init__.py（抜粋）
from flask import Flask

from .extensions import db, migrate


def create_app(test_config: dict | None = None) -> Flask:
    app = Flask(__name__, instance_relative_config=True)
    # ...設定読み込み（§2 / §6）...

    # ここで初めて app に束縛する（二段階初期化の第二段）
    db.init_app(app)
    migrate.init_app(app, db)

    # Blueprint の登録は import を「関数の中」で行う（§4）
    from .blueprints.api import bp as api_bp
    app.register_blueprint(api_bp, url_prefix="/api")

    return app
```

ポイントは、`db = SQLAlchemy()` が `extensions.py` で**アプリと無関係に**生成されることです。モデル定義は `from myapp.extensions import db` で `db` を import しますが、この `db` は `app` を一切知らないため、`app` を import する必要がありません。**依存方向が `models → extensions` の一方向**になり、円が消えます。

### 3.2 なぜこれが循環 import を構造的に解消するのか

依存グラフで見ると、二段階初期化の威力が明確になります。

| アプローチ | 依存方向 | 結果 |
|---|---|---|
| グローバル `app` ＋ `db = SQLAlchemy(app)` | `models → app → models` | 循環（起動不能） |
| `extensions.py` ＋ `db.init_app(app)` | `models → extensions`、`__init__ → extensions`、`__init__ → models`（関数内 import） | 一方向（健全） |

`extensions.py` が「アプリにも、モデルにも、ビューにも依存しない最下層」になることで、すべてのモジュールが安心して `extensions` を import できます。これは依存性逆転の素朴な実装——**具体（特定のアプリ）ではなく抽象（裸の拡張オブジェクト）に依存する**——でもあります。

### 3.3 1 つの拡張オブジェクトが複数アプリに再利用できる

この設計には、循環 import の回避以外にもう 1 つ重要な帰結があります。**`db` という単一の拡張オブジェクトが、`init_app` を通じて複数のアプリに束縛できる**ことです。本番アプリとテストアプリが、それぞれ別の DB 設定で同じ `db` オブジェクトを使えます。これがファクトリ（§2）と拡張パターンが噛み合う理由です——ファクトリが複数のアプリを作り、各アプリに同じ拡張を `init_app` で結びつける。

> 💡 **`current_app` がこの設計を完成させる**：ファクトリでアプリを作ると「`app` をモジュールトップから import できない」状況が生まれます（import 時には存在しないため）。拡張やビューが現在のアプリの設定へアクセスするには、`app` を引き回す代わりに `current_app` プロキシを使います。`current_app.config["..."]` は「いま処理中のアプリ」へ動的に解決されるため、ファクトリ構成と完全に整合します。`current_app` / `g` / アプリケーションコンテキストの仕組みは [コンテキスト徹底解説](/blog/flask-application-request-context-g-current-app-guide) で深掘りしています。永続化層（SQLAlchemy 2.0 の型付き ORM）の設計は [SQLAlchemy 2.0 実践ガイド](/blog/sqlalchemy-2-typed-orm-production-guide) を併読してください。

---

## **4. Blueprint 徹底解説：分割の単位と公式仕様の落とし穴**

アプリケーションファクトリが「アプリをどう作るか」を解決したら、次は「機能をどう分割するか」です。それが **Blueprint** です。Blueprint は「ルート・テンプレート・静的ファイル・エラーハンドラ・CLI コマンドをひとまとめにした、登録可能な機能ユニット」です。Blueprint API は Flask 0.7 で追加されました。

### 4.1 定義と登録：`name` は URL ではなく endpoint の前綴り

最も重要な誤解から潰します。**Blueprint の `name`（第一引数）は URL を決めません。endpoint（`url_for` で使う名前）を前綴りします。**

```python
# src/myapp/blueprints/simple_page.py
from flask import Blueprint, render_template
from werkzeug.exceptions import abort

# 第1引数 = name（endpoint の前綴り）、第2引数 = import_name
simple_page = Blueprint("simple_page", __name__, template_folder="templates")


@simple_page.route("/<page>")
def show(page):
    return render_template(f"pages/{page}.html")
```

```python
# create_app 内で登録
from .blueprints.simple_page import simple_page

app.register_blueprint(simple_page)
# URL を前綴りしたいなら url_prefix を登録時に指定する
app.register_blueprint(simple_page, url_prefix="/pages")
```

ここで `url_for` がどう変わるかが核心です。

| 指定 | endpoint | 生成される URL（`url_prefix="/pages"`） |
|---|---|---|
| `url_for("simple_page.show", page="about")` | `simple_page.show` | `/pages/about` |

**`name`（`simple_page`）は endpoint に `.` 区切りで前綴りされ、URL には `url_prefix` が前綴りされる**——この 2 つは別物です。`name` を変えても URL は変わらず、`url_prefix` を変えても endpoint は変わりません。この分離を理解していないと、`url_for` で `BuildError` を出して延々ハマります。

> ⚠️ **アンチパターン**：`Blueprint("/admin", __name__)` のように、`name` に URL パスを書く。`name` は endpoint の名前空間であって URL ではありません。スラッシュを含む `name` は不正です。URL を付けたいなら `app.register_blueprint(admin, url_prefix="/admin")` を使ってください。同様に、`url_for("show")` のように Blueprint 名を省くと、ビューが別 Blueprint にいるとき解決に失敗します。**同一 Blueprint 内なら `url_for(".show")`（先頭ドット）で相対参照**できます。

### 4.2 Blueprint の入れ子（nesting）

Blueprint は入れ子にできます。親 Blueprint に子を登録し、親をアプリに登録すると、**子は親の `name` と `url_prefix` の両方を継承**します。

```python
parent = Blueprint("parent", __name__, url_prefix="/parent")
child = Blueprint("child", __name__, url_prefix="/child")


@child.route("/create")
def create():
    return "created"


parent.register_blueprint(child)   # 親に子を登録
app.register_blueprint(parent)     # アプリに親を登録
```

この結果、endpoint は `parent.child.create`、URL は `/parent/child/create` になります。

```python
url_for("parent.child.create")   # → "/parent/child/create"
```

`name` も `url_prefix` も連結される点に注意してください。`subdomain=` を使ったサブドメインでの入れ子も同様に機能します。入れ子は「管理画面の中にユーザー管理と請求管理がある」といった階層を素直に表現できますが、深くしすぎると endpoint 名が長大になります。**入れ子は 2 段までを目安**にし、それ以上は別アプリ（マイクロサービス）への分割を検討するのが現実的です。

### 4.3 Blueprint ローカルのテンプレートと静的ファイル：落とし穴の宝庫

Blueprint は自前のテンプレート・静的ファイルを持てますが、**ここに公式仕様上の落とし穴が集中**しています。

**テンプレート**：`Blueprint("x", __name__, template_folder="templates")` で Blueprint 固有のテンプレートフォルダを追加できます。ただし優先順位に注意が必要です。

- Blueprint のテンプレートフォルダは、**アプリのテンプレートフォルダより低い優先度**で追加されます。つまり同じ相対パスのテンプレートがあれば、**アプリ側が Blueprint 側を上書きできます**（カスタマイズの余地を残す設計）。
- 複数の Blueprint が同じ相対パスのテンプレートを提供した場合、**最初に登録された Blueprint が勝ちます**。

> ⚠️ **テンプレート衝突の罠**：複数の Blueprint がそれぞれ `templates/index.html` を持つと、後から登録した Blueprint の `index.html` は**永久に解決されません**（最初に登録されたものが勝つため）。これを避けるには、Blueprint ごとにサブフォルダで名前空間を切ります——`templates/admin/index.html`、`templates/auth/index.html` のように。テンプレートの相対パスを Blueprint 名で名前空間化するのが、衝突を防ぐ唯一確実な方法です。

**静的ファイル**：ここがさらに罠です。`Blueprint("admin", __name__, static_folder="static")` で静的フォルダを持てますが、**`url_prefix` を持たない Blueprint は、自分の静的フォルダを配信できません**。

- Blueprint の静的ファイルは `url_prefix + /static` で配信され、endpoint は `admin.static` になります。
- `url_prefix` がない Blueprint では、アプリの `/static` が勝ち、**Blueprint の静的フォルダはフォールバックとして検索すらされません**。

```python
# url_prefix があれば静的フォルダが配信される
admin = Blueprint("admin", __name__, static_folder="static")
app.register_blueprint(admin, url_prefix="/admin")
# → /admin/static/... で配信、endpoint は "admin.static"
# → url_for("admin.static", filename="style.css") → "/admin/static/style.css"
```

実務では、Blueprint ごとに静的ファイルを散らすより、**アプリ単一の `/static` に集約してビルドツール（Vite 等）でバンドルする**ほうが運用が単純です。Blueprint ローカル静的は「プラグイン的に完全独立した機能ユニットを配布する」ような特殊な場合に限るのが無難です。

### 4.4 Blueprint のエラーハンドラと「404 を捕捉できない」例外

Blueprint は `@bp.errorhandler(404)` でエラーハンドラを登録できますが、**ここに最も誤解されやすい落とし穴**があります。

```python
@simple_page.errorhandler(404)
def page_not_found(e):
    # これは「無効な URL アクセス」では呼ばれない！
    return render_template("pages/404.html"), 404
```

**Blueprint の `errorhandler(404)` は、その Blueprint の*自身のビュー関数の中で* `abort(404)` や `raise` が起きたときにだけ呼ばれます。** 存在しない URL へのアクセス（ルーティング失敗）では呼ばれません。理由は明快で——**Blueprint は URL 空間を「所有」していない**からです。`/nonexistent` というリクエストがどの Blueprint に属するべきかを Flask は判断できないため、無効 URL の 404 は**アプリレベルのハンドラ**でしか捕捉できません。

アプリ全体に効くハンドラを Blueprint から登録したいときは、`app_errorhandler` を使います。

```python
# 自分の Blueprint のビュー内の abort(403) だけを捕捉
@simple_page.errorhandler(403)
def handle_403(e):
    return render_template("pages/403.html"), 403


# アプリ全体の 404（無効 URL を含む）を、この Blueprint から登録する
@simple_page.app_errorhandler(404)
def handle_app_404(e):
    return render_template("404.html"), 404
```

| デコレータ | スコープ | 無効 URL の 404 を捕捉？ |
|---|---|---|
| `@bp.errorhandler(404)` | その Blueprint 自身のビュー内の `abort`/`raise` のみ | ❌ しない |
| `@bp.app_errorhandler(404)` | アプリ全体（無効 URL を含む） | ✅ する |
| `@app.errorhandler(404)` | アプリ全体 | ✅ する |

エラーハンドラの解決順序、`HTTPException` の一括ハンドリング、API での JSON エラー設計の詳細は [エラー処理・可観測性ガイド](/blog/flask-error-handling-logging-observability-guide) に分けています。

### 4.5 Blueprint の CLI コマンド

Blueprint は `@bp.cli.command(...)` で、その Blueprint に紐づく `flask` CLI コマンドを定義できます。既定では、Blueprint 名を冠したグループの下に入ります。

```python
# blueprints/admin/__init__.py
import click
from flask import Blueprint

admin = Blueprint("admin", __name__)


@admin.cli.command("create")
@click.argument("name")
def create(name):
    """管理ユーザーを作成する。"""
    click.echo(f"created admin: {name}")
```

```bash
# 既定では Blueprint 名（admin）のグループ配下に入る
flask admin create alice
```

グループ名は変更できます。`Blueprint("admin", __name__, cli_group="other")` で別名のグループに、`cli_group=None` でトップレベルに昇格させて `flask create alice` にできます。`db migrate` のような独立した運用コマンド群を Blueprint 単位で持たせると、CLI が機能ごとに整理されて運用しやすくなります。

---

## **5. 推奨パッケージ構成：`src/` レイアウトの具体例**

ここまでの設計要素——ファクトリ・`extensions.py`・Blueprint・Config——を、1 つのディレクトリ構成に落とします。これが本番 Flask の「型」です。`src/` レイアウト（パッケージを `src/` 配下に置く）を採用すると、テスト時に「インストール済みのパッケージ」として import され、「カレントディレクトリのファイルをうっかり import する」事故を防げます。

```text
myapp/
├── pyproject.toml
├── src/
│   └── myapp/
│       ├── __init__.py          # create_app（アプリケーションファクトリ）§2
│       ├── config.py            # 環境別 Config クラス（Base/Production/Test）§6
│       ├── extensions.py        # db = SQLAlchemy() など「裸」の拡張 §3
│       ├── errors.py            # app_errorhandler でアプリ全体のエラー整形 §4.4
│       ├── models/
│       │   ├── __init__.py
│       │   ├── user.py          # from myapp.extensions import db
│       │   └── invoice.py
│       └── blueprints/
│           ├── __init__.py
│           ├── auth/            # 認証 Blueprint（login/logout/signup）
│           │   ├── __init__.py  # auth = Blueprint("auth", __name__)
│           │   ├── routes.py
│           │   └── templates/auth/   # 名前空間化したテンプレート §4.3
│           └── api/             # API Blueprint（REST エンドポイント）
│               ├── __init__.py  # api = Blueprint("api", __name__)
│               ├── routes.py
│               └── schemas.py   # marshmallow スキーマ（境界バリデーション）
├── instance/                    # .gitignore（秘密・SQLite・アップロード）§6.2
│   └── config.py
├── migrations/                  # Flask-Migrate（Alembic）
└── tests/
    ├── conftest.py              # app / client / runner fixtures
    └── test_*.py
```

この構成での依存方向を明示します——**すべて「上から下」の一方向**です。

```text
__init__.py (create_app)
    ├──→ config.py        （Config クラスを読む）
    ├──→ extensions.py    （db, migrate を init_app）
    ├──→ blueprints/*     （register_blueprint）
    └──→ errors.py        （app_errorhandler を登録）

blueprints/api/routes.py
    ├──→ extensions.py    （db を使う）
    ├──→ models/*         （モデルを使う）
    └──→ blueprints/api/schemas.py

models/*  ──→ extensions.py   （db.Model を継承するためだけに db を import）
```

`extensions.py` が最下層にいて、誰も `__init__.py`（`create_app`）を import しない——この形を保つ限り、循環 import は構造的に発生しません。API Blueprint の `schemas.py` における「`Router → Schema（境界）→ Model` の層分離」は、[marshmallow × Flask × SQLAlchemy で本番 REST API を設計する](/blog/marshmallow-flask-sqlalchemy-rest-api-production-guide) で詳説しています。本記事の Blueprint が「どこに」を、その記事が「中で何を」を担います。

> 💡 **`models/` と `blueprints/` をパッケージ（ディレクトリ＋ `__init__.py`）にする**：単一ファイルの `models.py` で始めても問題ありませんが、モデルが増えたら `models/` パッケージに昇格させ、`models/__init__.py` で各モデルを再エクスポートします。こうすると `from myapp.models import User` という import パスを安定させたまま、内部のファイル分割を自由に変えられます。import パスの安定性は、リファクタの自由度に直結します。

---

## **6. 環境別 Config：`from_object` + `from_prefixed_env` の二段構え**

ファクトリの `test_config` 分岐（§2.1）と組み合わせて、**環境ごとに設定を切り替える**仕組みを整えます。本番で揺るがしてはならない原則は 1 つ——**秘密情報（`SECRET_KEY`・DB パスワード・API キー）をコードに書かない**。これは 12-factor の核であり、セキュリティの最低ラインです。

### 6.1 Config クラスの継承階層

`app.config.from_object(...)` は、**大文字の属性だけ**を設定として読み込みます。これを使い、`Base → Production / Test` の継承で環境差分を表現します。

```python
# src/myapp/config.py
class BaseConfig:
    """全環境共通の安全な既定値。"""
    JSON_SORT_KEYS = False
    SESSION_COOKIE_HTTPONLY = True       # 既定 True だが意図を明示
    SESSION_COOKIE_SAMESITE = "Lax"
    SQLALCHEMY_TRACK_MODIFICATIONS = False


class ProductionConfig(BaseConfig):
    SESSION_COOKIE_SECURE = True         # HTTPS 限定 Cookie


class TestConfig(BaseConfig):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:"
```

```python
# create_app 内（§2 の設定読み込み部分を置き換える）
def create_app(test_config: dict | None = None) -> Flask:
    app = Flask(__name__, instance_relative_config=True)

    # ① Config クラスで安全な既定値を与える
    app.config.from_object("myapp.config.ProductionConfig")
    # ② 環境変数で本番値を上書きする（FLASK_ 前綴り）
    app.config.from_prefixed_env()

    if test_config is not None:
        app.config.from_mapping(test_config)   # テストは最後に上書き

    db.init_app(app)
    # ...Blueprint 登録...
    return app
```

> ⚠️ **`from_object` の罠：クラス参照は「インスタンス化されない」**。`from_object("myapp.config.ProductionConfig")` は、クラスの**大文字属性をそのまま読む**だけで、クラスを**インスタンス化しません**。つまり `@property` で計算した設定値は読み込まれません（`property` オブジェクトのまま無視される）。動的に計算した設定が必要なら、**クラスではなくインスタンスを渡す**（`from_object(ProductionConfig())`）か、`from_prefixed_env()` で環境変数から流し込んでください。

### 6.2 `from_prefixed_env` とインスタンスフォルダ

`from_prefixed_env()` は **Flask 3.0 で追加**された、コンテナ／12-factor 時代に最適なメソッドです。`FLASK_` で始まる環境変数を自動で読み込み、値は **`json.loads` で型付け**します。

```bash
# 本番の環境変数（コンテナの Secrets から注入）
export FLASK_SECRET_KEY='...'                       # python -c 'import secrets; print(secrets.token_hex())'
export FLASK_SQLALCHEMY_DATABASE_URI='postgresql+psycopg://...'
export FLASK_MAX_CONTENT_LENGTH=10485760            # json.loads されて int になる
export FLASK_SQLALCHEMY_ENGINE_OPTIONS__pool_size=10  # __ でネストしたキーに代入
```

`FLASK_MAX_CONTENT_LENGTH=10485760` は文字列ではなく `int 10485760` として読み込まれ、`__`（二重アンダースコア）でネストした辞書キーにも代入できます。「環境変数は全部文字列で扱いづらい」という典型的な悩みを Flask 側が解消します。

設定の読み込み口は他にもあります。本記事で使う `from_object` / `from_prefixed_env` / `from_pyfile` のほか、JSON/TOML を読む `from_file(path, load=tomllib.load, text=False)`、単一の環境変数からパスを読む `from_envvar` があります。設定管理の全体像は [Flask 本番運用ガイド](/blog/flask-production-guide) の §4 にまとまっています。

**インスタンスフォルダ**（`instance/`、Flask 0.8 で追加）は、`instance_relative_config=True`（§2.1）のときに `from_pyfile("config.py")` の解決基点になる「リポジトリにコミットしない設定置き場」です。`instance/` は**必ず `.gitignore` に入れて**ください。Cookie・CSRF・`SECRET_KEY` 周りのセキュリティ設定の深掘りは [セキュリティ実装ガイド](/blog/flask-security-sessions-csrf-secure-cookies-guide) を参照してください。

### 6.3 Flask のバージョンを取得する

設定やヘルスチェックでフレームワークのバージョンを出したくなることがあります。**Flask 3.0 で `Flask.__version__` は廃止**されました。代わりに標準ライブラリの `importlib.metadata` を使います。

```python
from importlib.metadata import version

flask_version = version("flask")   # "3.1.3" のような文字列
```

---

## **7. いつ分割するか：「3 つ目で抽出」の YAGNI 規律**

ここまで Blueprint と src レイアウトを説明してきましたが、**最も重要なのは「いつ分割するか」の判断**です。設計力とは、構造を増やす力ではなく、**構造を増やすべき瞬間を見極める力**です。

最初から `blueprints/auth/`、`blueprints/api/`、`blueprints/admin/` と細かく割る必要はありません。これは典型的な早すぎる抽象化で、まだ存在しない要件のために認知負荷を先払いする行為です。筆者が本番で守ってきた規律はシンプルです。

| 段階 | 構造 | 判断 |
|---|---|---|
| 1 機能目 | `create_app` 内に直接ルートを書く | Blueprint はまだ不要 |
| 2 機能目 | 1 つの Blueprint にまとめる | 「分けたくなる」が、まだ我慢 |
| 3 機能目 | 関心事ごとに Blueprint へ抽出する | **ここで初めて分割** |

この「**3 つ目で抽出**」は、DRY の「2 回は偶然、3 回はパターン」と同じ哲学です。2 つの似たルートは偶然の一致かもしれませんが、3 つ揃えば「これは独立した関心事だ」という確信が持てます。確信が持てるまで分割を遅らせることで、**間違った境界線で割ってしまう**（後で割り直す羽目になる）リスクを避けられます。

> 💡 **分割の単位は「変更理由（SRP）」で決める**：Blueprint を「URL のプレフィックス」で割るのではなく、「**何が変わったら一緒に変わるか**」で割ってください。認証ロジックが変わるとき一緒に変わるものは `auth/` に、API のスキーマが変わるとき一緒に変わるものは `api/` に。同じ理由で変わるコードを 1 つの Blueprint に集め、違う理由で変わるコードを分ける——これが単一責任原則の、Blueprint 粒度での適用です。URL は結果であって、分割の基準ではありません。

ただし例外があります。**`create_app` と `extensions.py` の分離だけは、機能が 1 つでも最初から入れてください**（§2.3）。これは「将来の拡張のため」ではなく「テスト容易性という現在の要件」を満たすための分離だからです。YAGNI が適用されるのは Blueprint の細分化であって、ファクトリの導入ではありません。この区別を取り違えないことが、過小設計（テストできない）と過剰設計（割りすぎ）の両方を避ける鍵です。

---

## **まとめ：循環 import は「設計」で構造的に消す**

大規模 Flask アプリの破綻は、Flask の機能不足ではなく構造不足から来ます。本記事の規律を再掲します。

1. **グローバル `app` を捨てる**。モジュールトップの `app = Flask(__name__)` は、テスト不能・単一構成・循環 import という三大破綻の根源。
2. **アプリケーションファクトリ（`create_app(test_config=None)`）**を背骨にする。`instance_relative_config=True` と `test_config` 分岐で、テスト容易性と複数環境を同時に解決する。
3. **拡張は `extensions.py` で裸に生成し、`init_app(app)` で束縛する**。`extensions` を最下層に置くことで依存を一方向化し、循環 import を構造から消す。1 つの拡張が複数アプリに再利用できる。
4. **Blueprint で機能を分割する**。`name` は endpoint の前綴りで URL ではない、無効 URL の 404 は Blueprint の `errorhandler` では捕捉できず `app_errorhandler` が要る、`url_prefix` のない Blueprint は静的フォルダを配信できない、テンプレートは最初に登録したものが勝つ——この公式仕様の落とし穴を踏まない。
5. **環境別 Config は `from_object`（既定値）→ `from_prefixed_env`（環境上書き）**の二段構え。`from_object` はクラスをインスタンス化しない点に注意し、秘密はインスタンスフォルダと環境変数でコードから追い出す。
6. **分割は「3 つ目で抽出」の YAGNI 規律**で。Blueprint の細分化は遅らせてよいが、`create_app` と `extensions.py` の分離だけは最初から入れる。

これらは「規模が大きくなったら導入する」ものではなく、**最初の数百行から効く設計**です。Flask は「核だけ」を提供し、構造はあなたに委ねます。その自由に責任を持つ最小の規律が、アプリケーションファクトリと Blueprint です。全体像と各論（コンテキスト・テスト・デプロイ・セキュリティ・可観測性）の地図は [Flask 本番運用ガイド](/blog/flask-production-guide) に戻って俯瞰してください。
