Python初心者がFastAPIでTodo(API)を作ってみた

# Python初心者がFastAPIでTodo(API)を作ってみた

## 前提
Python 3.9
FastAPI 0.88
sqlmodel 
sqlite3

***開発環境***
MacBook + VScode

## FastAPIについて
### 特徴

公式より引用

* 高速: NodeJS や Go 並みのとても高いパフォーマンス (Starlette と Pydantic のおかげです)。 最も高速な Python フレームワークの一つです.
* 高速なコーディング: 開発速度を約 200%~300%向上させます。
* 少ないバグ: 開発者起因のヒューマンエラーを約 40％削減します。
* 直感的: 素晴らしいエディタのサポートや オートコンプリート。 デバッグ時間を削減します。
* 簡単: 簡単に利用、習得できるようにデザインされています。ドキュメントを読む時間を削減します。
* 短い: コードの重複を最小限にしています。各パラメータからの複数の機能。少ないバグ。
* 堅牢性: 自動対話ドキュメントを使用して、本番環境で使用できるコードを取得します。
* Standards-based: API のオープンスタンダードに基づいており、完全に互換性があります: OpenAPI (以前は Swagger として知られていました) 
* 

## インストール
pythonは3.9がインストール済みの前提

### FastAPIのインストール
```pip3 install fastapi```

### uvicornサーバ
* httpdライブサーバ
```pip3 install "uvicorn[standard]"```

### sqlmodelのインストール
* sqlite3のO/Rマッパー
```pip3 install sqlmodel```


### 開発

### 初期DBの作成とコネクションクラスの作成

#### 初期DBの作成
models/init.py
```
from datetime import datetime
from typing import Optional

from sqlmodel import Field, SQLModel, create_engine



class Todo(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    content: str

    created_at: datetime = Field(default=datetime.utcnow(), nullable=False)
    updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)


# DBへの接続を定義
sqlite_file_name = "todos.sql"
sqlite_url = f"sqlite:///{sqlite_file_name}" # エンジンデータベースのURL

engine = create_engine(sqlite_url, echo=True) # engineを作成
SQLModel.metadata.create_all(engine)  # モデルからテーブルを生成
```
#### DB接続クラス
models/db.py
```
from sqlmodel import create_engine

# DBへの接続を定義
sqlite_file_name = "todos.sql"
sqlite_url = f"sqlite:///{sqlite_file_name}" # エンジンデータベースのURL
engine = create_engine(sqlite_url, echo=True) # engineを作成
```
#### メイン処理
main.py
```
import json
from fastapi import FastAPI
from sqlmodel import Field, SQLModel, create_engine, Session, select
from pydantic import BaseModel
from datetime import datetime
from typing import Optional
import models.db as Db


#SQL MODEL
class Todo(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    content: str
    created_at: datetime = Field(default=datetime.utcnow(), nullable=False)
    updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
    
#FORM MODELS
class TodoCreate(BaseModel):
    content: str

class TodoUpdate(BaseModel):
    id: int
    content: str

class TodoDelete(BaseModel):
    id: int


# main
app = FastAPI()

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

# select list
@app.get("/todos/")
async def todo_list():
    with Session(Db.engine) as session:
        stmt = select(Todo)
        results = session.exec(stmt)
        todos = []
        for todo in results:
            todos.append(todo)


    return {"todos": todos}

# select one
@app.get("/todos/{_id}")
async def todo_find(_id:int):
    with Session(Db.engine) as session:
        stmt = select(Todo).where(Todo.id == _id)
        todo = session.exec(stmt).first()

    return {"todo": todo}


# create
@app.post("/todo")
async def todo_create(todo :TodoCreate):
    with Session(Db.engine) as session:        
        todo = Todo(content=todo.content)
        session.add(todo)
        session.commit()

    return True


# update
@app.patch("/todo")
async def todo_update(todoUpdate :TodoUpdate):
    with Session(Db.engine) as session:
        stmt = select(Todo).where(Todo.id == todoUpdate.id)
        todo = session.exec(stmt).first()
        todo.content = todoUpdate.content
        session.add(todo)
        session.commit()
        #更新したレコードの取得
        stmt = select(Todo).where(Todo.id == todoUpdate.id)
        todo = session.exec(stmt).first()

    return todo


# delete
@app.delete("/todo")
async def todo_delete(todoDelete :TodoDelete):
    with Session(Db.engine) as session:
        stmt = select(Todo).where(Todo.id == todoDelete.id)
        todo = session.exec(stmt).first()
        session.delete(todo)
        session.commit()

    return True
```

#### 動かしてみる

ライブサーバ(uvicorn)を起動
```uvicorn main:app --reload```

ソースを変更するとホットリロードされます。

#### Postmanで動作テスト

##### リストを取得する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99447/7141d2b9-cb33-2053-e2e4-5fc5e8015e48.png" width="600">

##### todoを追加する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99447/856776b6-9523-a0bd-6a49-d5a11878036f.png" width="600">

##### todoを更新する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99447/9569ec9a-2eb5-7cbd-0dee-a13dada2f5e5.png" width="600">

##### todoを削除する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99447/a9009a8b-d988-0a01-62b7-bd3e2cea76c7.png" width="600">


### OpenAPIのドキュメントも自動生成される
ブラウザでドキュメントのURLにアクセスする
```http://127.0.0.1:8000/docs```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99447/8338f1cb-f006-a7af-d49f-3a61ef5f52b8.png" width="600">


## 終わりに
FastAPIは軽くて小さなフレームワークだと思います。
プロトタイプなど、一時的にAPIを開発するには、簡単で軽い反面、いろいろやりたいことがある場合は、ライブラリをインストール必要があるかと思います。なので、依存関係などを管理するのは大変かもしれません。
ドキュメントやエコシステムが未発達なので、やりたいことを検索してもうまく情報が見つからない場合がある気がします。

プロダクションとかはDjangoとか実績のあるフレームワークのほうがいい気がします。

### 参考ページ
https://fastapi.tiangolo.com/ja/
https://zenn.dev/nameless_sn/articles/why_fastapi_development