FastAPIが覇権を取れるかもという話

こんにちは、こんばんは、おはようございます。
[K.S.ロジャース](https://www.wantedly.com/companies/ks-rogers) の島袋です。

[以前PythonのWebフレームワークの次期覇権はコレ](https://qiita.com/shuto-S/items/433707f26b9efd927869) みたいな記事を書きましたが、それからしばらくPythonから離れたエンジニア生活を過ごしていたところ、なんと [FastAPI](https://fastapi.tiangolo.com/) なるフレームワークが登場しているではないですか！

正直、何番煎じかわからない遅れ方してますが、ぜひとも紹介させてください。

## FastAPIの特徴
[公式トップページ](https://fastapi.tiangolo.com/) に書いてあるには、

1. 高速 - NodeJSやGoなみに早い
2. コード書くの早い - 2倍3倍で書ける！
3. バグが減る - 40％くらい減る！
4. 直感的 - デバッグが簡単
5. 簡単 - 使いやすい。ドキュメント読むのも簡単！
6. 短い - 重複が少ない。パラメータ宣言で複数の機能を呼べる
7. 堅牢 - 自動生成あるからほんと楽
8. スタンダード - OpenAPI標準だから互換性あるよ（Swaggerと互換性あるよ）

とありますが一番下の `OpenAPI標準だから互換性あるよ（Swaggerと互換性あるよ）` これが素晴らしすぎる。
FastAPI、実はSwagger（ドキュメント）の自動生成ができるのです！

## ドキュメント自動生成
pipで `fastapi` と `uvicorn` をインストールしたら早速触ってみます。
ちなみに標準のpipには環境切り分けなどがないので、[pipenv](https://github.com/pypa/pipenv) を利用することをおすすめします。


```py
from fastapi import FastAPI

app = FastAPI()


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

上記を `main.py` で作成後に `uvicorn main:app --reload` で起動。
`http://127.0.0.1:8000/docs` を確認すると…

<img width="1412" alt="Fast API - Swagger UI 2020-02-05 00-29-56.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103383/3d15af08-8e19-e425-8ef4-e9d6fba521e7.jpeg">

はい！素敵！！
試しに `path parameter` を設定すると…

### Path parameters

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

<img width="1412" alt="Fast API - Swagger UI 2020-02-05 13-09-21.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103383/07a4639d-ab31-6f86-f023-4c85d5263cac.jpeg">

自動で反映されます。
ドキュメントが自動で作られていくとか最強です。
とくにドキュメントを重要視している弊社では便利すぎる機能です。

### Query Parameters

```py
@app.get("/")
async def read_item(name: str = ""):
    return {"message": "Hello " + name}
```

<img width="1408" alt="Fast API - Swagger UI 2020-02-05 23-04-27.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103383/a9303f72-c23d-3b54-566c-966ac4d296f3.jpeg">

### Requests
Modelを使います。

```py
from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: str = None

app = FastAPI()


@app.post("/items/")
async def create_item(item: Item):
    return item
```

<img width="1404" alt="Fast API - Swagger UI 2020-02-05 23-12-03.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103383/34d5aafb-a12b-1711-20ec-24c2f550b0f8.jpeg">

さらにModelクラス内でのプロパティにバリデーションを指定できるので、取り回しが凄く良いです。
ここらへんは `Django` のModelと似たような構成ですね。

```py
class Item(BaseModel):
    name: str = Field(None, min_length=2, max_length=5)
    description: str = Field(None, max_length=100)
```

## CRUD (Crate, Read, Update, Delete)
基本のCRUDをざっくり書いてみます。
一部抜粋して書いているので、詳しくは書きリンクをご参考ください。

https://github.com/shuto-S/sample-fast-api

### DB Connection & Migration
DBへの接続とマイグレーションは `databases` と `sqlalchemy` を使います。
今回は取り回しのよい `SQLite` をDBに使います。

さくっとコネクション周りを書いて、

```py
import databases
import sqlalchemy

DATABASE_URL = "sqlite:///./db.sqlite3"

database = databases.Database(DATABASE_URL)
engine = sqlalchemy.create_engine(DATABASE_URL, echo=False)
metadata = sqlalchemy.MetaData()
```

### Schemas
テーブル定義を記述して、同ファイルを実行しておきます。

```py
import sqlalchemy
from db import metadata, engine


items = sqlalchemy.Table(
    "items",
    metadata,
    sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
    sqlalchemy.Column("name", sqlalchemy.String),
    sqlalchemy.Column("description", sqlalchemy.String),
)

metadata.create_all(engine)
```

### Main
`main.py` も改修

```py
from fastapi import FastAPI
from db import database
from starlette.requests import Request
from routers import items

app = FastAPI()

@app.on_event("startup")
async def startup():
    # DBコネクション開始
    await database.connect()

@app.on_event("shutdown")
async def shutdown():
    # DBコネクション切断
    await database.disconnect()

# routersを登録
app.include_router(items.router)

# ミドルウェアでDBコネクション埋め込んでおく（routerで取得できるように）
@app.middleware("http")
async def db_session_middleware(request: Request, call_next):
    request.state.connection = database
    response = await call_next(request)
    return response
```

### Routers
`router` もファイル分割してエンドポイント毎に記述できるようにします。

```py
from fastapi import APIRouter, Depends, HTTPException
from typing import List
from databases import Database
from starlette.status import HTTP_204_NO_CONTENT

from utils import get_db_connection
from schemas import items
from models.item import ItemModel


router = APIRouter()

@router.get("/items", tags=["items"], response_model=List[ItemModel])
async def list_item(database: Database = Depends(get_db_connection)):
    query = items.select()
    return await database.fetch_all(query)

@router.post("/items", tags=["items"], response_model=ItemModel)
async def create_item(data: ItemModel, database: Database = Depends(get_db_connection)):
    query = items.insert()
    await database.execute(query, data.dict())
    return {**data.dict()}

@router.patch("/items/{item_id}", tags=["items"], response_model=ItemModel)
async def update_item(item_id: int, data: ItemModel, database: Database = Depends(get_db_connection)):
    query = items.update().where(items.columns.id==item_id)
    ret = await database.execute(query, data.dict())
    if not ret:
        raise HTTPException(status_code=404, detail="Not Found")
    return {**data.dict()}

@router.delete("/items/{item_id}", tags=["items"], status_code=HTTP_204_NO_CONTENT)
async def delete_item(item_id: int, database: Database = Depends(get_db_connection)):
    query = items.delete().where(items.columns.id==item_id)
    ret = await database.execute(query)
    if not ret:
        raise HTTPException(status_code=404, detail="Not Found")
```

## まとめ
どうでしょう？ドキュメント自動生成はかなり魅力的な機能ではないでしょうか？
[Responder](https://responder.kennethreitz.org/en/latest/) もかなり良いフレームワークだと思いますが、FastAPI のほうが癖が少なく書きやすく感じました。

## あとづけ
ちなみに弊社、Tech系以外にも会社ブログも掲載してますので、気になった方は是非どうぞ。
https://www.wantedly.com/companies/ks-rogers