{"id":50979354,"url":"https://github.com/tkc/fastapi-sandbox","last_synced_at":"2026-06-19T12:33:54.917Z","repository":{"id":339170966,"uuid":"1160765038","full_name":"tkc/fastapi-sandbox","owner":"tkc","description":null,"archived":false,"fork":false,"pushed_at":"2026-02-18T13:56:57.000Z","size":86,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-18T15:25:15.185Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tkc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-18T10:47:43.000Z","updated_at":"2026-02-18T13:56:58.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/tkc/fastapi-sandbox","commit_stats":null,"previous_names":["tkc/fastapi-sandbox"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/tkc/fastapi-sandbox","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tkc%2Ffastapi-sandbox","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tkc%2Ffastapi-sandbox/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tkc%2Ffastapi-sandbox/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tkc%2Ffastapi-sandbox/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tkc","download_url":"https://codeload.github.com/tkc/fastapi-sandbox/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tkc%2Ffastapi-sandbox/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34532255,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-19T02:00:06.005Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-19T12:33:52.751Z","updated_at":"2026-06-19T12:33:54.896Z","avatar_url":"https://github.com/tkc.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# fastapi-sandbox\n\nFastAPI + DynamoDB で構築した User API のサンドボックスプロジェクト。\nクリーンアーキテクチャの実践と、構造化ログ・型安全性・定数管理などの横断的関心事を検証する。\n\n## Tech Stack\n\n| Category | Tool |\n|---|---|\n| Web Framework | FastAPI |\n| Database | Amazon DynamoDB (Local) |\n| DI Container | injector |\n| Logging | structlog |\n| Validation | Pydantic / pydantic-settings |\n| Linter / Formatter | Ruff |\n| Type Checker | ty |\n| Test | pytest |\n| Runtime | Python 3.13, uv |\n\n## Libraries\n\n### FastAPI\n\nASGI ベースの Web フレームワーク。`Depends()` による DI、Pydantic モデルによるリクエスト/レスポンスの自動バリデーション、`exception_handler` によるグローバル例外処理を活用。Starlette の `BaseHTTPMiddleware` を継承したログミドルウェアもここに乗る。\n\n### boto3\n\nAWS SDK for Python。DynamoDB テーブルの CRUD 操作に `dynamodb.Table` リソース API を使用。ローカル開発では DynamoDB Local (`localhost:8000`) に接続する。\n\n### injector\n\nPython の DI コンテナライブラリ。`IUserRepository` インターフェースを `UserDynamoDBRepository` にバインドし、`UserService` のコンストラクタへ自動注入する。バインディング定義は `app/container/container.py` に集約。\n\n### Pydantic / pydantic-settings\n\n- **Pydantic**: ドメインエンティティ (`User`) と API スキーマ (`UserCreate`, `UserResponse`) の定義に使用。`Field` による入力バリデーション（文字列長、数値範囲）、`EmailStr` によるメールアドレス検証を提供。\n- **pydantic-settings**: 環境変数ベースのアプリ設定 (`Settings`) を管理。DynamoDB 接続先やログレベルを外部から注入可能にする。\n\n### structlog\n\n構造化ログライブラリ。stdlib logging と統合し、JSON / コンソール出力を切替可能。`contextvars` を利用してリクエストスコープの `trace_id` を全ログ行に自動付与する。`@log_action()` デコレータと組み合わせ、メソッド単位の開始・成功・エラーログを自動記録。\n\n### Ruff\n\nLinter と Formatter を兼ねるツール。`pycodestyle`, `pyflakes`, `isort`, `flake8-bugbear`, `pyupgrade`, `flake8-annotations` 相当のルールを有効化。テストコードでは `ANN` (型アノテーション) ルールを除外。\n\n### ty\n\nPython の型チェッカー。`app/` 配下のコードに対して静的型検査を実施し、`NewType` (`UserId`) の誤用を検出する。\n\n### pytest\n\nテストフレームワーク。単体テスト（usecase, domain, core, infrastructure）と結合テスト（API エンドポイント, ログ検証）の2レベルで構成。`MagicMock(spec=...)` によるリポジトリのモック、`structlog.testing.capture_logs()` によるログ出力の検証を行う。\n\n## Architecture\n\nクリーンアーキテクチャに基づき、依存の方向を **外側 → 内側** に統一している。\n\n```\n┌─────────────────────────────────────────────────────┐\n│  API Layer (app/api/)                               │\n│  FastAPI endpoints, dependency injection             │\n├─────────────────────────────────────────────────────┤\n│  Usecase Layer (app/usecase/)                       │\n│  Application business logic (UserService)            │\n├─────────────────────────────────────────────────────┤\n│  Domain Layer (app/domain/)                         │\n│  Entities, repository interfaces                     │\n├─────────────────────────────────────────────────────┤\n│  Infrastructure Layer (app/infrastructure/)          │\n│  DynamoDB repository implementation, datasource      │\n└─────────────────────────────────────────────────────┘\n```\n\n### 依存関係\n\n```\nAPI → Usecase → Domain ← Infrastructure\n```\n\n- **Domain** は他のどの層にも依存しない（純粋なエンティティとインターフェース）\n- **Infrastructure** は Domain のインターフェース (`IUserRepository`) を実装する（依存性逆転）\n- **Usecase** は Domain のインターフェースにのみ依存し、具体実装を知らない\n- **API** は Usecase を呼び出すだけの薄いレイヤー\n\n### ディレクトリ構成\n\n```\napp/\n├── api/                          # API Layer\n│   ├── dependencies.py           #   FastAPI Depends() でサービスを解決\n│   └── v1/endpoints/users.py     #   REST エンドポイント定義\n├── usecase/                      # Usecase Layer\n│   └── user/user_service.py      #   ビジネスロジック (CRUD + 検索)\n├── domain/                       # Domain Layer\n│   └── user/\n│       ├── entity.py             #   User エンティティ (Pydantic BaseModel)\n│       └── i_user_repository.py  #   リポジトリインターフェース (ABC)\n├── infrastructure/               # Infrastructure Layer\n│   ├── datasource/dynamodb.py    #   DynamoDB リソース/テーブル生成\n│   ├── repository/               #   IUserRepository の DynamoDB 実装\n│   └── constants.py              #   DynamoDB 固有の定数\n├── core/                         # Cross-cutting concerns\n│   ├── types.py                  #   NewType 定義 (UserId)\n│   ├── constants.py              #   アプリ共通定数\n│   ├── config.py                 #   pydantic-settings による設定管理\n│   ├── decorators.py             #   @log_action 構造化ログデコレータ\n│   ├── exceptions.py             #   例外階層 (AppError → UserNotFoundError, RepositoryError)\n│   ├── exception_handlers.py     #   FastAPI グローバル例外ハンドラー\n│   └── logger.py                 #   structlog セットアップ\n├── container/                    # DI Container\n│   └── container.py              #   injector によるバインディング定義\n├── middleware/                   # Middleware\n│   └── logging_middleware.py     #   リクエスト単位の trace_id 付与・ログ出力\n├── schemas/                      # API Schemas\n│   └── user.py                   #   リクエスト/レスポンス用 Pydantic モデル\n└── main.py                       # アプリケーションエントリーポイント\n```\n\n### 横断的関心事\n\n#### 型安全性\n\n`typing.NewType` で `UserId` 型を定義し、全層で `user_id: str` の代わりに `user_id: UserId` を使用。\n素の `str` との混同を型チェッカーレベルで防止する。\n\n#### 構造化ログ\n\n- **リクエストレベル**: `LoggingMiddleware` が `X-Request-ID` ヘッダーから `trace_id` を取得し、structlog の contextvars にバインド。全ログ行にリクエスト単位のトレースIDが自動付与される。\n- **メソッドレベル**: `@log_action()` デコレータが開始・成功・エラーの3イベントを自動記録。引数のサニタイズ（`exclude_args` でリダクション）、実行時間の計測を含む。\n\n#### 例外ハンドリング\n\n詳細は後述の [Error Design](#error-design) を参照。\n\n#### 定数管理\n\n文字列・数値リテラルを `app/core/constants.py`（アプリ共通）と `app/infrastructure/constants.py`（DynamoDB 固有）に集約。変更時の影響範囲を明確化。\n\n## Error Design\n\n### 例外階層\n\n```\nException\n └── AppError                    # アプリケーション基底例外 (message: str)\n      ├── UserNotFoundError      # ユーザー未検出 (user_id: UserId)\n      └── RepositoryError        # データアクセス失敗 (operation: str)\n```\n\n- **`AppError`** — 全てのアプリケーション例外の基底クラス。`message` 属性を持つ。\n- **`UserNotFoundError`** — リポジトリが `None` を返した場合に Usecase 層で送出。`user_id` を保持し、ログとレスポンスに含める。\n- **`RepositoryError`** — boto3 の `ClientError` / `BotoCoreError` を Infrastructure 層でキャッチし、`from err` で原因チェーンを保持したまま送出。`operation` 属性で失敗した操作名 (`save`, `find_by_id` 等) を記録する。\n\n### 例外の発生箇所と伝播\n\n```\nInfrastructure Layer                    Usecase Layer\n──────────────────                      ─────────────\nboto3 ClientError                       repo.find_by_id() == None\n      │                                        │\n      ▼                                        ▼\nRepositoryError (from err)              UserNotFoundError(user_id)\n      │                                        │\n      └──────────────┐  ┌─────────────────────┘\n                     ▼  ▼\n              FastAPI exception_handler\n              (app/core/exception_handlers.py)\n```\n\n- エンドポイントは `try/except` を持たない。全ての例外はグローバルハンドラーで処理される。\n- Infrastructure 層は boto3 例外を `RepositoryError` にラップし、内部実装の詳細を上位層に漏らさない。\n\n### HTTP レスポンスマッピング\n\n| Exception | Status Code | Response Body | Log Level |\n|---|---|---|---|\n| `UserNotFoundError` | `404 Not Found` | `{\"detail\": \"User not found\"}` | `WARNING` |\n| `RepositoryError` | `500 Internal Server Error` | `{\"detail\": \"Internal server error\"}` | `ERROR` |\n| `AppError` | `500 Internal Server Error` | `{\"detail\": \"Internal server error\"}` | `ERROR` |\n\n- クライアントに内部エラーの詳細（スタックトレース、DB エラーメッセージ）は返さない。\n- 全てのハンドラーは構造化ログに `user_id`, `operation`, `message`, `path` 等のコンテキストを出力する。\n- HTTP ステータスコードは `starlette.status` の定数を使用。\n\n### ログイベント\n\n例外ハンドラーが出力するログイベント名は `app/core/constants.py` で一元管理される。\n\n| Constant | Event Name | Trigger |\n|---|---|---|\n| `LOG_USER_NOT_FOUND` | `user_not_found` | `UserNotFoundError` 捕捉時 |\n| `LOG_REPOSITORY_ERROR` | `repository_error` | `RepositoryError` 捕捉時 |\n| `LOG_APP_ERROR` | `app_error` | `AppError` 捕捉時 |\n\n### バリデーションエラー\n\nリクエストボディのバリデーションは Pydantic / FastAPI が自動処理する。\n\n| Validation | Rule | Response |\n|---|---|---|\n| `name` | 1〜100文字 | `422 Unprocessable Entity` |\n| `email` | RFC 5322 準拠 | `422 Unprocessable Entity` |\n| `age` | 0〜150 | `422 Unprocessable Entity` |\n| `address` | 1〜500文字 | `422 Unprocessable Entity` |\n\nこれらは FastAPI のデフォルトハンドラーが処理し、フィールド単位のエラー詳細を JSON で返却する。\n\n## Setup\n\n```bash\n# 依存インストール\nuv sync\n\n# DynamoDB Local 起動\ndocker run -d -p 8000:8000 amazon/dynamodb-local\n\n# サーバー起動\nuv run uvicorn app.main:app --reload\n```\n\n## Development\n\n```bash\n# テスト\nuv run pytest tests/ -v\n\n# Lint \u0026 Format\nuv run ruff format app/ tests/\nuv run ruff check . --fix\n\n# 型チェック\nuv run ty check app/\n```\n\n## API Endpoints\n\n| Method | Path | Description |\n|---|---|---|\n| `POST` | `/users` | ユーザー作成 |\n| `GET` | `/users` | ユーザー一覧取得 |\n| `GET` | `/users/search?name=\u0026email=` | ユーザー検索 |\n| `GET` | `/users/{user_id}` | ユーザー取得 |\n| `GET` | `/health` | ヘルスチェック |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftkc%2Ffastapi-sandbox","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftkc%2Ffastapi-sandbox","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftkc%2Ffastapi-sandbox/lists"}