PythonでAPIを爆速で構築してみた

# 目次
[1.はじめに](#1-はじめに)
[2.コーディング](#2-コーディング)
[3.コンテナ化](#3-コンテナ化)


# 1. はじめに

友人に「PythonでAPIをサクッと作ってよ」と言われたのでシンプルなREST APIを作ってみた。

作ったものを渡すだけでなく作り方も教えて欲しいとのことなので、ここに記事として掲載する。少し手順書のような記載なため、初学者向けかもしれない。

Pythonと聞いて「Djangoでも使うか？」と思いつつも、より*サクッと*感のあるフレームワークを探してみたところ
FastAPIなるものがあり、今回はこれを採用してみた。

*[公式](https://fastapi.tiangolo.com/ja/ "FastAPI")より引用*
>FastAPI は、Pythonの標準である型ヒントに基づいてPython 3.6 以降でAPI を構築するための、モダンで、高速(高パフォーマンス)な、Web フレームワークです。
>FastAPI には Swagger UI と ReDoc の両スタイルのドキュメントを自動で生成してくれる機能があります。

コーディング負荷が軽量でありドキュメントも自動生成とのことで、*サクッと*度合い高めな印象。

DB接続を伴うAPI構築については下記記事に記載している。

https://qiita.com/yota_dev/items/8309dd294975b15c22d5


# 2. コーディング

まずは構成の説明。

```
.
├── app/
│   └── main.py
└── requirements.txt
```


パッケージ一括管理用に`requirements.txt`を(使用する内容は少数だが)作成しておく。

```text:requirements.txt
fastapi==0.70.0
uvicorn==0.15.0
```

早速installしてみる。

```sh
pip3 install -r requirements.txt
```

FastAPI のインストール完了。

早速コーディングしてみる。

```python:main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}
```

起動してみる。

```sh
cd app
uvicorn main:app --reload 
```

するとterminal上でサーバが動いている様が見える。

```
INFO:     Will watch for changes in these directories: ['xxxx/app']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [5129] using statreload
INFO:     Started server process [5131]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
```

では早速curlコマンドを打ってみる。

```sh
~ % curl localhost:8000
{"message":"Hello World"}
```

無事に世界に挨拶することが出来た。

サーバーログにもしっかりと出力されていて安心。

```
INFO:     127.0.0.1:52258 - "GET / HTTP/1.1" 200 OK
```

CTRL+C でサーバーを止めることが出来るが、起動時に指定したOption`--reload `はソースコードの変更を検知して自動で反映してくれるらしい。止めずにこのまま構築を続けてみる。


次はPathParameterやQueryParameterを受け付けてみる。

```python:main.py
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
    if q:
        return {"item_id": item_id, "q": q}
    return {"item_id": item_id}
```

`item_id`はPathParameterで受け取りつつ、`q`はQueryで受け取る。

queryは省略できるように`= None`を指定しておき、存在の有無でresponse bodyを切り替えてみた。

curlコマンドで動きを見る。

```sh
curl "localhost:8000/items/123"
{"item_id":123}
```

```sh
curl "localhost:8000/items/123?q=hoge"
{"item_id":123,"q":"hoge"}
```

いい感じ。


次にPOSTを受け付けたい。

データやオブジェクトをいい感じに取り扱たのでBaseModelを使用する。

下記を追加してみる。

```python:main.py
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float

@app.post("/items")
def update_item(item: Item):
    return {"item_name": item.name, "twice price": item.price * 2}

```

curlで動きをみる。今回はjsonを投げるため headerとbodyを指定する。

```sh
curl -X POST "localhost:8000/items" -H 'Content-Type: application/json' -d '{"name":"おなまえ","price":123}' 
{"item_name":"おなまえ","twice price":246.0}
```

いい感じ。


ここまでで、リクエストのパラメーターをPath/Queryで受け付けつつJsonBodyで受け付ける内容が実装されたためOKとする。あとはコピペを繰り返せば一旦はI/Fはできると思う。RDBとの接続などは別の記事で記述予定。


ソースコードの全量は下記。

```python:main.py
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float

@app.get("/")
async def root():
    return {"message": "Hello World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
    if q:
        return {"item_id": item_id, "q": q}
    return {"item_id": item_id}

@app.post("/items")
def update_item(item: Item):
    return {"item_name": item.name, "twice price": item.price * 2}
```

ちなみに[http://localhost:8000/docs](http://localhost:8000/docs "http://localhost:8000/docs")にブラウザでアクセスするとSwaggerのドキュメントがみれるらしいので確認してみる。

![FastAPI](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527084/74b6b1bd-85d8-b136-cbab-daa838d5bf59.png "FastAPI")


すごく便利。`openapi.json`を取得できるのは嬉しい。合わせてgitにでもあげておきたい。

次にコンテナ化に移る。`CTRL+C`でサーバーは止めておこう。

# 3. コンテナ化

コーディングとしては以上だが、*流行り的に* DockerImageも作っておく。

まずは構成の説明。

```
.
├── app/
│   └── main.py
├── Dockerfile
└── requirements.txt
```


次にDockerfileを記述。

```Dockerfile
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9-slim

COPY ./app /app
```


公式のDockerImageが利用できるようで、記述が非常に楽。

詳しくはFastAPIの[公式](https://fastapi.tiangolo.com/ja/deployment/docker/ "FastAPI/Docker")を参照。


では早速imageをbuild。

```sh
docker build -t sample-api:1.0.0 .
```

確認。

```sh
docker images | grep sample-api
sample-api                       1.0.0                f91600e19608   About an hour ago   991MB
```

いた。

次にコンテナも立ち上げてみる。

どうやらportは80で空いているようだ。特に変える必要もないため80のまま解放。

```sh
docker run -d --name sample-api-container -p 80:80 sample-api:1.0.0
```

コンテナログを見てみる。

```sh
docker ps -q -f "name=sample-api-container" | xargs docker logs -f
```

timestampが付与されていて公式のDokcerImageの優秀さを感じる。

```
Checking for script in /app/prestart.sh
Running script /app/prestart.sh
Running inside /app/prestart.sh, you could add migrations to this file, e.g.:

#! /usr/bin/env bash

# Let the DB start
sleep 10;
# Run migrations
alembic upgrade head

[2022-02-12 09:44:50 +0000] [1] [INFO] Starting gunicorn 20.1.0
[2022-02-12 09:44:50 +0000] [1] [INFO] Listening at: http://0.0.0.0:80 (1)
[2022-02-12 09:44:50 +0000] [1] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2022-02-12 09:44:50 +0000] [7] [INFO] Booting worker with pid: 7
[2022-02-12 09:44:50 +0000] [8] [INFO] Booting worker with pid: 8
[2022-02-12 09:44:50 +0000] [7] [INFO] Started server process [7]
[2022-02-12 09:44:50 +0000] [7] [INFO] Waiting for application startup.
[2022-02-12 09:44:50 +0000] [7] [INFO] Application startup complete.
[2022-02-12 09:44:50 +0000] [9] [INFO] Booting worker with pid: 9
[2022-02-12 09:44:50 +0000] [8] [INFO] Started server process [8]
[2022-02-12 09:44:50 +0000] [8] [INFO] Waiting for application startup.
[2022-02-12 09:44:50 +0000] [8] [INFO] Application startup complete.
[2022-02-12 09:44:50 +0000] [9] [INFO] Started server process [9]
[2022-02-12 09:44:50 +0000] [9] [INFO] Waiting for application startup.
[2022-02-12 09:44:50 +0000] [9] [INFO] Application startup complete.
[2022-02-12 09:44:50 +0000] [10] [INFO] Booting worker with pid: 10
[2022-02-12 09:44:50 +0000] [10] [INFO] Started server process [10]
[2022-02-12 09:44:50 +0000] [10] [INFO] Waiting for application startup.
[2022-02-12 09:44:50 +0000] [10] [INFO] Application startup complete.
[2022-02-12 09:44:50 +0000] [11] [INFO] Booting worker with pid: 11
[2022-02-12 09:44:50 +0000] [12] [INFO] Booting worker with pid: 12
[2022-02-12 09:44:50 +0000] [11] [INFO] Started server process [11]
[2022-02-12 09:44:50 +0000] [11] [INFO] Waiting for application startup.
[2022-02-12 09:44:50 +0000] [11] [INFO] Application startup complete.
[2022-02-12 09:44:50 +0000] [12] [INFO] Started server process [12]
[2022-02-12 09:44:50 +0000] [12] [INFO] Waiting for application startup.
[2022-02-12 09:44:50 +0000] [12] [INFO] Application startup complete.
```

コンテナの外からcurlを打ってみる。portが異なることに注意。

```sh
curl localhost:80  
{"Hello":"World"}
```

再び世界とこんにちは。

ログもしっかりと確認する。

```
{"loglevel": "info", "workers": 6, "bind": "0.0.0.0:80", "graceful_timeout": 120, "timeout": 120, "keepalive": 5, "errorlog": "-", "accesslog": "-", "workers_per_core": 1.0, "use_max_workers": null, "host": "0.0.0.0", "port": "80"}
172.17.0.1:56684 - "GET / HTTP/1.1" 200
```

良い。再び`CTRL+C`で閉じておく。

containerとimageの消し方などは別途記事にする予定。

以上。