とりあえずFlaskでAPI開発ハンズオン

# 環境
- macOS:13.6.7
- Docker:27.3.1
- VScode

# 開発環境構築
VScodeを開いて、ターミナルを起動
自分の好きなディレクトリにて開発をしましょう

```bash
$ cd /Users/{username}/Desktop/
$ mkdir API_test
```
:::note info
{username}は自分のPCのユーザー名を入力
例: `/Users/{username}/Desktop/API_test` が `/Users/alice/Desktop/API_test` になる
:::

今回は軽量のDockerイメージのalpineを使ってみたいと思いますので、イメージをローカル環境に持ってきます

```bash
$ docker pull python:3.8-alpine
```

# API構築
そもそもAPIとは？GPTに聞いてみた
```
異なるソフトウェアアプリケーションが相互にデータや機能をやりとりできるようにするための「インターフェース（接点）」です
```
連携の際に使えるデータの取り出し口のようなもの

また、APIを実装していく上でいくつかの用語が出てきますのでざっくり理解

- **エンドポイント**
APIを利用するためのURLパスで、特定のデータや機能を提供するための接続先です。
例えば、/userエンドポイントはユーザー情報を扱うための接続先になります。

- **メソッド（HTTPメソッド）**
APIがどのようなアクションを実行するかを指定するもので、代表的なものにGET（データ取得）、POST（データ送信）、PUT（データ更新）、DELETE（データ削除）があります。

- **パラメータ**
エンドポイントへのリクエストに付随して送る情報で、必要なデータや条件を指定します。

## ステップ1 コピペ実装
では実際にソースコードを書いてみます。
とりあえずコピペしてみて、動くものを見てみましょう！

**ディレクトリ構成**
```bash
API_test/
├── src
    └── app.py
    └── user.json
├── Dockerfile
├── requirements.txt
└── docker-compose.yml
```

**`app.py`ファイル**
```app.py
# app.py
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify(message="Hello!")

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

```

**`Dockerfile`ファイル**
```Dockerfile
# Dockerfile
# ベースイメージにAlpineのPythonを指定
FROM python:3.8-alpine

# 作業ディレクトリを設定
WORKDIR /app

# 必要なパッケージをインストール
RUN apk add --no-cache gcc musl-dev curl

# 依存関係をインストール
COPY requirements.txt .
RUN pip install -r requirements.txt

# Flaskアプリのソースコードをコピー
COPY . .

# Flaskアプリの実行
CMD ["python", "./src/app.py"]

```

**`docker-compose.yml`ファイル**
```docker-compose.yml
# docker-compose.yml
version: '3'
services:
  web:
    container_name: api_test
    build: .
    ports:
      - "5000:5000"
    volumes:
      - /Users/{username}/Desktop/API_test:/apps
```

### 実行
`/Users/{username}/Desktop/API_test`配下にて以下を実行
```bash
$ docker compose up --build
$ curl localhost:5000 -v
```
以下のように出力されるはず
```bash
*   Trying [::1]:5000...
* Connected to localhost (::1) port 5000
> GET / HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/8.4.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: Werkzeug/3.0.6 Python/3.8.20
< Date: Fri, 08 Nov 2024 17:18:52 GMT
< Content-Type: application/json
< Content-Length: 49
< Connection: close
< 
{"message":"Hello!"}
* Closing connection
```

`app.py`にて指定した通り、`{"message":"Hello!"}`とJSONが返ってきます
今回`curl`を`-v`オプションで詳細まで表示しています
確認してみるとレスポンスが200でGETが返ってきていることがわかります

## ステップ2 GETメソッドのパスを追加してみる
パスを新しく追加してみます
`/heartbeat`のエンドポイントにリクエストされた場合は「元気です！」とテキストでレスポンスするようにしてみましょう

```app.py
...

# 新しいエンドポイント /heartbeat を追加
@app.route('/heartbeat')
def heartbeat():
    return "元気です！"
...

```
ソースコードを更新したので、コンテナも更新する必要があります

```bash
$ docker compose up --build
$ curl localhost:5000/heartbeat
```

このようにGETメソッドは追加することができます

## ステップ3 クエリを指定してGETメソッドしてみよう
`/hello`エンドポイントで、クエリパラメータからusernameを受け取り、「[username]さん、こんにちは！」と返すように修正してみましょう
```app.py
from flask import Flask, jsonify,request

...

# /helloエンドポイントでusernameを受け取り、挨拶を返す
@app.route('/hello')
def hello():
    username = request.args.get('username', 'ゲスト')
    return f"{username}さん、こんにちは！"

...
```

```bash
$ docker compose up --build
$ curl localhost:5000/hello?username=raha
```
どうですか？出力されましたか？

# ステップ3 POST&PUTを試す
ユーザーとその年齢を登録するようにしてみましょう
少し難しくなります...

まず、ユーザー情報を管理するjsonファイルを用意しましょう

```bash
$ touch ./src/user.json
```

それではソースコードを変更していきます

```app.py
# app.py
import json
from flask import Flask, request, jsonify

app = Flask(__name__)

# ユーザー情報を保存するファイルパス
USER_FILE = 'user.json'

# user.jsonファイルを読み込んで辞書として返す
def load_users():
    try:
        with open(USER_FILE, 'r') as file:
            return json.load(file)
    except (FileNotFoundError, json.JSONDecodeError):
        return {}

# usersをuser.jsonファイルに保存する
def save_users(users):
    with open(USER_FILE, 'w') as file:
        json.dump(users, file, indent=4)

# POST /user エンドポイント：新しいユーザーを追加
@app.route('/user', methods=['POST'])
def add_user():
    users = load_users()
    data = request.get_json()
    username = data.get('username')
    age = data.get('age')

    if not username or not age:
        return "usernameとageを指定してください", 400

    # ユーザーが既に存在するかチェック
    if username in users:
        return f"{username}は既に存在します", 400

    # 新しいユーザーを追加してファイルに保存
    users[username] = age
    save_users(users)
    return jsonify(message=f"{username}が追加されました", user={username: age}), 201

# PUT /user/<username> エンドポイント：ユーザーの年齢を更新
@app.route('/user/<username>', methods=['PUT'])
def update_user(username):
    users = load_users()

    if username not in users:
        return f"{username}は存在しません", 404

    data = request.get_json()
    age = data.get('age')

    if not age:
        return "ageを指定してください", 400

    # ユーザーの年齢を更新してファイルに保存
    users[username] = age
    save_users(users)
    return jsonify(message=f"{username}の年齢が更新されました", user={username: age}), 200

# GET /user/<username> エンドポイント：ユーザー情報を取得
@app.route('/user/<username>', methods=['GET'])
def get_user(username):
    users = load_users()

    if username not in users:
        return f"{username}は存在しません", 404

    # ユーザー情報を返す
    return jsonify(username=username, age=users[username])

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

```

ではリクエストしてみましょう
POSTはGETと違うcurlの叩き方をします

```bash
$ docker compose up --build
$ curl -X POST -H "Content-Type: application/json" -d '{"username": "raha", "age": 15}' http://localhost:5000/user
```

では、情報が登録されたかGETリクエストしてみましょう


次に、年齢が嘘なので、更新をしてみます

```bash
$ curl -X PUT -H "Content-Type: application/json" -d '{"age": 20}' http://localhost:5000/user/raha
```

こちらもGETリクエストで確認してみましょう

## ステップ4 ユーザー削除してみる

```bash
...

# DELETE /user エンドポイント：クエリパラメータで指定されたユーザー情報を削除
@app.route('/user', methods=['DELETE'])
def delete_user():
    username = request.args.get('username')
    if not username:
        return "usernameをクエリパラメータで指定してください", 400

    users = load_users()

    if username not in users:
        return f"{username}は存在しません", 404

    # ユーザー情報を削除してファイルに保存
    del users[username]
    save_users(users)
    return jsonify(message=f"{username}が削除されました"), 200

...
```
削除の際のリクエストは以下です

```bash
$ curl -X DELETE "http://localhost:5000/user?username=taro"
```

では、ユーザー情報を取得してみましょう
適当な存在しないユーザー情報を取得しようとするとどうなるでしょう

# おわり
APIサーバーの開発ハンズオンは以上です