FastAPIのフォルダ構成をChatGPTに教えてもらった

## はじめに
FastAPIを使ってみて便利だったのですが、フォルダ・ファイルはどう構成したらいいの？と困りました。チュートリアルを熟読したらいいのですがページ数が多く億劫になり、まずはChatGPT（GPT-3.5）に教わりました。
コードや説明は、ほとんどそのまま転記しています。
コードの動作検証はしておりません。あくまでファイルの役割を把握するためのものです。

## ファイル構成
(ChatGPTによると、)FastAPIのフォルダ構成には特定の規定はありませんが、一般的に推奨されるフォルダ構成は以下のようになります。


``` md
myproject/
├── app/
│   ├── __init__.py
│   ├── api/
│   │   ├── __init__.py
│   │   └── endpoints/
│   │       ├── __init__.py
│   │       └── some_endpoint.py
│   ├── models/
│   │   ├── __init__.py
│   │   └── some_model.py
│   ├── services/
│   │   ├── __init__.py
│   │   └── some_service.py
│   ├── database/
│   │   ├── __init__.py
│   │   └── some_database.py
│   ├── schemas/
│   │   ├── __init__.py
│   │   └── some_schema.py
│   ├── config.py
│   └── main.py
├── tests/
│   ├── __init__.py
│   └── test_some_endpoint.py
├── requirements.txt
└── README.md
```


## ファイルのサンプル

ChatGPTに書いてもらったサンプルコードとその説明を貼り付けていきます。

### requirements.txt
以下は、このファイル構成で必要となるパッケージの例です。ただし、使用するパッケージによって異なります。
``` requirements.txt
fastapi[all]
SQLAlchemy
psycopg2-binary
pytest
```



### README.md
~~~ README.md
# My Project

This is a sample project created with FastAPI.

## Getting Started

### Prerequisites

- Python 3.8 or higher
- pip

### Installation

1. Clone the repository:

```
git clone https://github.com/username/my-project.git
```

2. Install the dependencies:

```
pip install -r requirements.txt
```

### Usage

1. Start the FastAPI application:

```
uvicorn app.main:app --reload
```

2. Open the API documentation in your browser:

```
http://localhost:8000/docs
```

## Running the tests

```
pytest
```

## Contributing

1. Fork the repository
2. Create a feature branch (`git checkout -b feature/my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin feature/my-new-feature`)
5. Create a new pull request

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
~~~ 

### app/

``` app/__init__.py
# app/__init__.py
# 空のファイル
``` 

``` app/config.py
from pydantic import BaseSettings


class Settings(BaseSettings):
    APP_NAME: str = "My FastAPI App"
    DATABASE_URL: str

    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

settings = Settings()
```
このファイルは、FastAPIアプリケーションの設定を管理するためのクラス`Settings`を定義しています。

`Settings`クラスのプロパティには、アプリケーション名(`APP_NAME`)、、データベースのURL(`DATABASE_URL`)が含まれます。これらの値は、アプリケーションで使用されます。

`Settings`クラスには、`Config`という内部クラスが含まれています。`Config`クラスには、`.env`という名前の環境変数ファイルを使用することを指示する`env_file`プロパティと、ファイルのエンコーディングをUTF-8に設定する`env_file_encoding`プロパティが含まれています。`.env`ファイルは、アプリケーションが実行される前に、環境変数として読み込まれます。これにより、アプリケーションが使用する設定を変更することができます。

最後の行では、`Settings`クラスをインスタンス化し、`settings`変数に代入しています。アプリケーションの異なる部分でこの変数を使用することができます。


``` .env
DB_USER=myuser
DB_PASSWORD=mypassword
DB_HOST=localhost
DB_PORT=3306
DB_NAME=mydatabase
DATABASE_URL=mysql+pymysql://$DB_USER:$DB_PASSWORD@$DB_HOST:$DB_PORT/$DB_NAME
```
注意点：
- `.env` ファイルは、アプリケーションのルートディレクトリに配置することが多いです。
- `=` の前後にスペースを含めないようにしてください。
- `DATABASE_URL` には、使用するデータベースの種類や接続情報が含まれます。この例では MySQL を使用しています。ユーザー名とパスワード、ホスト名、データベース名を指定してください。


``` app/main.py
from fastapi import FastAPI
from .api import router as api_router
from .database import database, engine
from .config import settings

app = FastAPI(title=settings.APP_NAME)

@app.on_event("startup")
async def startup():
    await database.connect()

@app.on_event("shutdown")
async def shutdown():
    await database.disconnect()

app.include_router(api_router)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
```
上記のコードでは、FastAPIのインスタンスを作成し、`myproject/app/api/endpoints/some_endpoint.py`で定義されたエンドポイントのルーターをインクルードしています。エンドポイントにアクセスするには、FastAPIを起動して、エンドポイントが公開されているURLにアクセスする必要があります。

### app/api/
`app/api/__init__.py` は `app/api` モジュールの初期化ファイルであり、FastAPIアプリケーションのAPIルートエンドポイントのURLパスを定義します。通常、このファイルは空のままで、このディレクトリの他のファイルからインポートされるモジュールを提供する役割があります。

例えば、`app/api/endpoints/some_endpoint.py` ファイルを作成した場合、そのエンドポイントを `app/api/__init__.py` からインポートし、APIルーターに追加します。

以下は `app/api/__init__.py` の例です：

``` app/api/__init__.py
from fastapi import APIRouter
from .endpoints import some_endpoint

router = APIRouter()
router.include_router(some_endpoint.router, prefix="/somepath", tags=["some"])
```
この例では、FastAPIの `APIRouter` をインスタンス化し、`some_endpoint` モジュールからインポートした `router` を含め、URLのプレフィックスとタグを定義し、ルーターに追加しています。

`APIRouter` は、`FastAPI`アプリケーションに対して、複数のAPIエンドポイントを管理し、それらを一元的に扱うことができるようにします。また、`prefix`パラメータを指定することで、APIエンドポイントのURLを特定のパス以下にグループ化することもできます。`tags`パラメータは、SwaggerUIやReDocなどのAPIドキュメント生成ツールで使用される、APIエンドポイントのグループ化に役立ちます。


### app/api/endpoints/
``` app/api/endpoints/__init__.py
# app/api/endpoints/__init__.py
# 空のファイル
```
``` app/api/endpoints/some_endpoint.py
from fastapi import APIRouter, Depends, HTTPException
from typing import List
from sqlalchemy.orm import Session

from app.database.some_database import SessionLocal
from app.schemas.some_schema import UserCreate, UserUpdate, User
from app.services.some_service import UserService


router = APIRouter()


@router.post("/users/", response_model=User)
def create_user(user: UserCreate, db: Session = Depends(SessionLocal)):
    db_user = UserService(db).create_user(user)
    return db_user


@router.get("/users/", response_model=List[User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(SessionLocal)):
    users = UserService(db).get_users(skip=skip, limit=limit)
    return users


@router.get("/users/{user_id}", response_model=User)
def read_user(user_id: int, db: Session = Depends(SessionLocal)):
    db_user = UserService(db).get_user_by_id(user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return db_user


@router.put("/users/{user_id}", response_model=User)
def update_user(user_id: int, user: UserUpdate, db: Session = Depends(SessionLocal)):
    db_user = UserService(db).get_user_by_id(user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    db_user = UserService(db).update_user(db_user, user.dict(exclude_unset=True))
    return db_user


@router.delete("/users/{user_id}", response_model=User)
def delete_user(user_id: int, db: Session = Depends(SessionLocal)):
    db_user = UserService(db).get_user_by_id(user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    UserService(db).delete_user(db_user)
    return db_user


```
このエンドポイントは、POSTメソッドを使って、`/users/`エンドポイントにユーザーを作成するAPIを提供します。`UserCreate`クラスは、ユーザーのデータを取得するために使用され、`User`クラスは、作成されたユーザーのデータを返すために使用されます。`get_db`関数はデータベースセッションを作成し、`Depends`を使って関数内で使用できます。ユーザーが作成されたら、POST要求によって作成されたユーザー情報が返されます。


### app/models/
``` app/models/__init__.py
# app/models/__init__.py
# 空のファイル
```
``` app/models/some_model.py
from sqlalchemy import Column, Integer, String
from app.database import Base

class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String(50), index=True)
    email = Column(String(50), unique=True, index=True)
    password = Column(String(50))
    is_active = Column(Boolean(), default=True)

```

このコードでは、SQLAlchemyの`Column`クラスを使って、データベースに保存する`User`モデルを定義しています。`Base`クラスは、SQLAlchemyの`declarative_base`関数で生成されたベースクラスで、すべてのモデルクラスはこのベースクラスを継承します。

`User`モデルには、`id`、`name`、`email`、`password`、`is_active`の5つの属性があります。`id`属性は主キーであり、自動的にインクリメントされます。`name`、`email`、`password`属性はそれぞれ文字列で、`email`属性はユニークである必要があります。`is_active`属性はブール値で、デフォルト値はTrueです。

##### What is SQLAlchemy?
**SQLAlchemy**（エスキューエル・オーエル・ケミストリー）は、Pythonで書かれたオープンソースの**SQLツールキット**で、**リレーショナルデータベースとPythonアプリケーションの相互作用を容易**にします。 SQLAlchemyを使用することで、Python開発者はPythonコードでSQLクエリを書いたり、Pythonオブジェクトをデータベースに保存したりできます。 SQLAlchemyにはORM（Object-Relational Mapping）と呼ばれる機能もあり、Pythonオブジェクトとデータベーステーブルをマッピングすることで、Pythonアプリケーションのデータベースへのアクセスをより簡単にすることができます。 SQLAlchemyは、Pythonの様々なORMライブラリの中でも最も人気のあるものの1つで、多数のアプリケーションで使用されています。


##### What is Pydantic?
**Pydantic**は、**Pythonの型注釈（Type hints）を使用して、データ検証とシリアル化/逆シリアル化の機能を提供するライブラリ**です。 Pydanticは、FastAPIなどのフレームワークで使用されることが多く、APIのパラメータやレスポンス、データモデルの検証に役立ちます。また、**PythonオブジェクトをJSONやYAMLなどのシリアル化可能な形式に変換する機能**を提供し、APIなどでのデータの受け渡しに役立ちます。Pydanticは、簡単に使えるという特徴があります。また、非常に高速であり、ユーザーフレンドリーなエラーメッセージを提供することで開発者の生産性を向上させることができます。



### app/services/

``` app/services/__init__.py
# app/services/__init__.py
# 空のファイル
```
`app/services/some_service.py`では、サービスのロジックを実装します。以下は、単純なユーザーサービスの例です。ユーザーモデルがあり、ユーザー情報を検証、作成、更新する関数を提供します。
``` app/services/some_service.py
from typing import List, Optional
from sqlalchemy.orm import Session
from app.models.some_model import User
from app.database.some_database import SessionLocal

class UserService:
    def __init__(self):
        self.db = SessionLocal()

    def get_user_by_id(self, user_id: int) -> Optional[User]:
        return self.db.query(User).filter(User.id == user_id).first()

    def get_users(self, skip: int = 0, limit: int = 100) -> List[User]:
        return self.db.query(User).offset(skip).limit(limit).all()

    def create_user(self, user: User) -> User:
        self.db.add(user)
        self.db.commit()
        self.db.refresh(user)
        return user

    def update_user(self, user: User, update_data: dict) -> User:
        for field, value in update_data.items():
            setattr(user, field, value)
        self.db.commit()
        self.db.refresh(user)
        return user

```
ここでは、データベースのセッションがサービスに渡され、データベースへのクエリと操作が行われます。

### app/database/
``` app/database/__init__.py
# app/database/__init__.py
# 空のファイル
```
``` app/database/some_database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.config import settings

engine = create_engine(settings.DATABASE_URL, echo=True)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

```

### app/schemas/

``` app/schemas/__init__.py
# app/schemas/__init__.py
# 空のファイル
```
`someschema.py`では、Pydanticを使用して、APIエンドポイントから受け取るデータを検証するためのスキーマを定義しています。

``` app/schemas/some_schema.py
from pydantic import BaseModel

class UserBase(BaseModel):
    email: str
    username: str

class UserCreate(UserBase):
    password: str

class UserUpdate(UserBase):
    password: str = None

class User(UserBase):
    id: int

    class Config:
        orm_mode = True
```
`someschema.py`ファイルで定義されている各クラスは、Pydanticの`BaseModel`クラスを継承しています。各クラスは、APIエンドポイントで必要なデータを検証するために使用されます。

`UserBase`：`email`と`username`を必須のフィールドとして持ち、他のクラスで継承されます。
`UserCreate`：`UserBase`を継承し、`password`を必須のフィールドとして追加します。これは、新しいユーザーを作成するときに使用されます。
`UserUpdate`：`UserBase`を継承し、`password`をオプションのフィールドとして追加します。これは、ユーザー情報を更新するときに使用されます。
`User`：`UserBase`を継承し、`id`を必須のフィールドとして追加します。`orm_mode`を有効にすることで、SQLAlchemyからのモデルインスタンスを直接Pydanticモデルに変換することができます。これは、データベースから取得されたユーザー情報をAPIエンドポイントのレスポンスに使用する場合に使用されます。

### app/tests/
``` app/tests/__init__.py
# app/tests/__init__.py
```
``` app/tests/test_some_endpoint.py
# app/tests/test_some_endpoint.py
from fastapi.testclient import TestClient

from app.main import app


client = TestClient(app)


def test_create_user():
    data = {"email": "test@example.com", "username": "testuser", "password": "password"}
    response = client.post("/users/", json=data)
    assert response.status_code == 200
    assert response.json()["email"] == data["email"]
    assert response.json()["username"] == data["username"]
    assert "id" in response.json()

def test_read_user():
    data = {"email": "test@example.com", "username": "testuser", "password": "password"}
    response = client.post("/users/", json=data)
    user_id = response.json()["id"]
    response = client.get(f"/users/{user_id}")
    assert response.status_code == 200
    assert response.json()["email"] == data["email"]
    assert response.json()["username"] == data["username"]
    assert response.json()["id"] == user_id

def test_update_user():
    data = {"email": "test@example.com", "username": "testuser", "password": "password"}
    response = client.post("/users/", json=data)
    user_id = response.json()["id"]
    data["username"] = "newusername"
    response = client.put(f"/users/{user_id}", json=data)
    assert response.status_code == 200
    assert response.json()["email"] == data["email"]
    assert response.json()["username"] == data["username"]
    assert response.json()["id"] == user_id

def test_delete_user():
    data = {"email": "test@example.com", "username": "testuser", "password": "password"}
    response = client.post("/users/", json=data)
    user_id = response.json()["id"]
    response = client.delete(f"/users/{user_id}")
    assert response.status_code == 200
    assert response.json() == {"id": user_id} 

```
この例では、`pytest`と`fastapi.testclient.TestClient`を使用しています。`pytest.fixture`で`TestClient`を作成し、その`fixture`を`test_some_endpoint`関数に注入しています。`test_some_endpoint`関数では、`client.get`メソッドを使用して、エンドポイントへのGETリクエストを行い、レスポンスの状態コードとボディをテストしています。

## まとめ

| ファイル名                  | 説明                                              |
| ------------------------ | ----------------------------------------------- |
| app/\_\_init\_\_.py          | appパッケージの初期化                                |
| app/api/\_\_init\_\_.py      | apiパッケージの初期化                                |
| app/api/endpoints/\_\_init\_\_.py | endpointsパッケージの初期化                          |
| app/api/endpoints/some_endpoint.py | FastAPIエンドポイント定義ファイル                     |
| app/models/\_\_init\_\_.py   | モデルパッケージの初期化                             |
| app/models/some_model.py | SQLAlchemyモデル定義ファイル                          |
| app/services/\_\_init\_\_.py | サービスパッケージの初期化                             |
| app/services/some_service.py | サービス定義ファイル（主にDB処理を記述）                  |
| app/database/\_\_init\_\_.py | データベースパッケージの初期化                         |
| app/database/some_database.py | SQLAlchemyデータベース接続処理定義ファイル               |
| app/schemas/\_\_init\_\_.py  | スキーマパッケージの初期化                             |
| app/schemas/some_schema.py | Pydanticモデル定義ファイル                            |
| app/config.py            | アプリケーションの設定を管理するファイル                   |
| app/main.py              | アプリケーションのエントリーポイント                     |
| tests/\_\_init\_\_.py        | テストパッケージの初期化                              |
| tests/test_some_endpoint.py | some_endpoint.pyのテストコード                       |
| requirements.txt         | アプリケーションが依存するPythonパッケージの一覧を記述するファイル |
| README.md                | プロジェクトのドキュメンテーションファイル                 |


# さいごに

今回のように情報量が多い場合、ChatGPTと複数回やりとりする必要がある。
すると、この程度でもChatGPTが前後で矛盾した回答をすることがままある。
ChatGPT3.5を利用すればプログラマの仕事がなくなる！とはとても言える状況にないし
ChatGPT4はどうかな？将来のバージョンでの精度向上を期待したい。