Fast API を使って、改めて RESTful API と GraphQL を学び直してみた

## はじめに

エンジニアとしてそろそろ初心者と言えない年数となりましたが、未だに API についてふわっとした理解しかできていないなと思い、この記事を書くに至りました。

そんな自分が API について学び直し、最近よく使っている [FastAPI](https://fastapi.tiangolo.com/) を用いて簡単な API を作ってみました。

- API (Application Programming Interface) って結局なに
- RESTful、GraphQL、言葉は知っているけどよくわからない 
- FastAPI で API はどうかけるのか

という方々の助力に少しでもなれば幸いです。
また、ご指摘などあればぜひコメントのほどよろしくお願いいたします！

## API について学び直してみた

今まで、API といえばデータのやりとり（作成、取得、更新、削除など）ができるインターフェースという漠然とした理解を持っていました。[ちゃんと調べた](https://www.sbbit.jp/article/cont1/62752)ら、データのやりとりにかかわらずプログラムやサービスなどの連携をするものだと理解しました。サービスを作る上で、システム間の疎結合を実現するための仕組みとして利用できるなと思いました。

API の種類としてよく知られているのは、以下のものと思われます。

- RESTful API
- SOAP
- GraphQL
- gRPC
- cURL

その中で RESTful API と GraphQL について深掘りしてみました。

### RESTful API

RESTful API とは、REST の設計思想に則って作られた API を指します。

REST（Representational State Transfer）は Web サービスのアーキテクチャスタイルの一つで、データの送受信を HTTP メソッドで行うモデルです。REST に必要な要素は以下の 4 つです（原則と言われているものですが、あえて要素と述べました）。

-  Stateless
    サーバはクライアントの状態を持たないので、クライアントへ必要な情報は全て含める必要がある

- Uniform Interface
    統一されたインターフェース。REST を用いるクライアントはすべて同じ方法で呼び出せる

- Addressability
    アドレスを見ただけでどんなものかがわかる。また、リソース同士が相互にリンクする

- Connectability
    データのやりとりをする情報以外に、別の情報や State のリンクを含められる


この要素を満たせば、RESTful API を作ることができます。

また、最近では次の要素も考慮する必要がありそうです。

- HTTP メソッドの使い分け
    以下のように、用途に応じてどのメソッドとして利用すればいいかを厳密に決める必要があります。例えば、特定のデータを取得するのに POST メソッドを使わないようにします

    | メソッド | HTTP | CRUD |
    | --- | --- | :-: |
    | 登録 | POST | Create |
    | 取得 | GET | Read |
    | 更新 | PUT | Update |
    | 削除 | DELETE | Delete |


- API のバージョン管理
    疎結合なシステムにはバージョン管理が必要です。バージョン管理により、クライアント側のシステムやプログラム更新などを考慮せずに、API を改修することができます

- パフォーマンスを意識する
    単に API を実行すると、リソースの取得をはじめとしたコストがかかります。以下の点を意識すると、パフォーマンスを落とさずに作成することができます

    - キャッシュの活用
    - データ圧縮
    - パラメータは最小限（簡潔なパラメータ設定）
    - 非同期処理
    - データ分割

    また、日々パフォーマンスを監視して、重い処理がないかを視ることも重要です

### GraphQL とは

[GraphQL](https://graphql.org/) は、API を開発するためのクエリ言語、ランタイムの一種です。Meta（旧 Facebook）社によって 2015 年頃に公開されました。

GraphQL は、REST の課題を解決するために生まれたもので、以下の要素によって構成されています。

- Query
    クライアントがサーバーからデータを取得するためのリクエスト

- Mutation
    データの作成、更新、削除を行うためのリクエスト

- Schema
    どのようなデータを提供するか、どのような操作が可能かを定義する

- Resolver
    Query や Mutation の実際のデータ取得や操作を行う


GraphQL には以下のような特徴があります。

- データ取得の効率性
    クライアント側がデータの構造を指定することで、サーバ側がその構造に沿ったデータ**のみ**を処理してくれる。取得したいデータを変えたければ、このデータ構造を変えるだけで API 側の改修は不要になる

- 単一エンドポイント
    必要なエンドポイントは 1 つなので、API 設計がシンプルになる

- 型システム
    データに関する強い整合性があるので、データ不整合が起きない
    
- 高速
    指定したデータだけを正確に取得するので、オーバーフェッチ、アンダーフェッチを避けられ、その結果高速なやりとりが可能になる

## FastAPI で実装してみた

より深い理解のために、作ってみたほうが早いと思い、[FastAPI](https://fastapi.tiangolo.com/) で実装してみました。FastAPI は、Python ベースで記述するモダンで高速な Web フレームワークです。型安全性、高速、Swagger UI 自動生成という特徴があります。

今回は、超基本ですが item というテーブルに対して CRUD 処理をするものを書いてみました。サーバは [uvicorn](https://www.uvicorn.org/) を導入しただけ、DB はインストールだけですぐに使える [SQLite](https://www.sqlite.org/) です。

### RESTful API 例

#### 事前準備

```Python
from fastapi import FastAPI, HTTPException, Body
import sqlite3
from typing import List

app = FastAPI()

# DB 接続
def get_db_connection():
    conn = sqlite3.connect('a-s-sample.sqlite3')
    conn.row_factory = sqlite3.Row
    return conn
```

#### データ取得

```Python
@app.get("/items/", response_model=List[dict])
def read_items():   # 全件取得
    conn = get_db_connection()
    items = conn.execute('select * from items').fetchall()
    conn.close()
    return [dict(item) for item in items]

@app.get("/items/{item_id}", response_model=dict)
def read_item(item_id: int):   # 特定の id のデータ取得
    conn = get_db_connection()
    item = conn.execute('select * from items WHERE id = ?', (item_id,)).fetchone()
    conn.close()

    if item is None:
        raise HTTPException(status_code=404, detail="Item not found")

    return dict(item)
```

#### データ登録

```Python
@app.post("/items/", response_model=dict)
def create_item(title: str = Body(), description: str = Body(default=None),):
    conn = get_db_connection()
    try:
        conn.execute('insert into items (title, description) values (?, ?)', (title, description,))
        conn.commit()
    except Exception as e:
        conn.rollback()
        raise HTTPException(status_code=400, detail=str(e))
    finally:
        conn.close()
    
    return {"title": title, "description": description,}
```

#### データ更新

　なんとなく、パラメータを指定したものだけ更新するように作ってみました。

```Python
@app.put("/items/{item_id}", response_model=dict)
def update_item(item_id: int, title: str = Body(default=None), description: str = Body(default=None),):
    updates = []
    params = []
    if title is not None:
        updates.append("title = ?")
        params.append(title)
    if description is not None:
        updates.append("description = ?")
        params.append(description)

    if not updates:
        raise HTTPException(status_code=400, detail="No updates specified")

    sql = "UPDATE items SET " + ", ".join(updates) + " WHERE id = ?"
    params.append(item_id)

    conn = get_db_connection()
    try:
        conn.execute(sql, tuple(params))
        conn.commit()
    except Exception as e:
        conn.rollback()
        raise HTTPException(status_code=400, detail=str(e))
    finally:
        conn.close()

    return {"id": item_id, "title": title or "No update", "description": description or "No update"}
```

#### データ削除

```Python
@app.delete("/items/{item_id}", response_model=dict)
def delete_item(item_id: int):
    conn = get_db_connection()
    conn.execute('delete from items where id = ?', (item_id,))
    conn.commit()
    conn.close()
    return {"message": "Item deleted successfully"}
```

### GraphQL 例

#### 事前準備

```Python
import sqlite3
import strawberry

from strawberry.fastapi import GraphQLRouter
from fastapi import FastAPI, HTTPException

from model import Item


app = FastAPI()

# モデル
@strawberry.type
class Item:
    id: int
    title: str
    description: str | None   # Null 許容

# DB 接続
def get_db_connection():
    conn = sqlite3.connect('a-s-sample.sqlite3')
    conn.row_factory = sqlite3.Row
    return conn

schema = strawberry.Schema(query=Query, mutation=Mutation)
graphql_app = GraphQLRouter(schema)

app.include_router(graphql_app, prefix="/graphql")
```

#### Query

```Python
@strawberry.type
class Query:
    @strawberry.field
    def items(self) -> list[Item]:   # 全件取得
        conn = get_db_connection()
        cur = conn.cursor()
        cur.execute("SELECT * FROM items")
        rows = cur.fetchall()
        conn.close()
        return [Item(**row) for row in rows]

    @strawberry.field
    def item(self, item_id: int) -> Item | None:   # 特定の id のデータ取得
        conn = get_db_connection()
        cur = conn.cursor()
        cur.execute("SELECT * FROM items WHERE id = ?", (item_id,))
        row = cur.fetchone()
        conn.close()
        if row:
            return Item(**row)

        return None
```

#### Mutation
``` Python
@strawberry.type
class Mutation:
    # データ作成
    @strawberry.mutation
    def create_item(self, title: str, description: str | None = None) -> Item:
        conn = get_db_connection()
        cur = conn.cursor()
        cur.execute("INSERT INTO items (title, description) VALUES (?, ?)", (title, description))
        conn.commit()
        item_id = cur.lastrowid
        conn.close()
        return Item(id=item_id, title=title, description=description)

    # データ更新
    @strawberry.mutation
    def update_item(self, item_id: int, title: str | None = None, description: str | None = None) -> Item:
        updates = []
        params = []
        if title is not None:
            updates.append("title = ?")
            params.append(title)
        if description is not None:
            updates.append("description = ?")
            params.append(description)

        if not updates:
            raise HTTPException(status_code=400, detail="No updates specified")

        sql = "UPDATE items SET " + ", ".join(updates) + " WHERE id = ?"
        params.append(item_id)

        conn = get_db_connection()
        try:
            conn.execute(sql, tuple(params))
            conn.commit()
        except Exception as e:
            conn.rollback()
            raise HTTPException(status_code=400, detail=str(e))
        finally:
            conn.close()

        return Item(id=item_id, title=title, description=description)

    # データ削除
    @strawberry.mutation
    def delete_item(self, item_id: int) -> str:
        conn = get_db_connection()
        conn.execute('delete from items where id = ?', (item_id,))
        conn.commit()
        conn.close()
        return f"Item id: {item_id} deleted successfully"
```

### 実装してみて

　用途ごとにエンドポイントを分けて、構文通りに実装するという点で、API の理解には RESTful API から始めると良さそうだなと思いました。また、単一エンドポイントで Query や Mutation の記述を理解すれば、複雑なデータ取得や柔軟なデータ取得の改修に GraphQL を用いることができそうだと感じました。

## RESTful API と GraphQL のメリデメ

　以上を踏まえて、自分なりに捉えたメリデメをまとめました。

### RESTful API メリット

- HTTP メソッドや JSON 形式で、簡単に開発ができる
- どんなデータやり取りをしているか理解しやすい
- Stateless なので拡張しやすい（疎結合）

### RESTful API デメリット

- コードレベルでの規定がなく、記述方式が定まらない
- 用途に合わせてエンドポイントをたくさん作らなければならない
- バージョン管理が必要になるシーンがある

### GraphQL メリット

- 取得するデータを任意に設定できる（これだけ、や色々欲しい、に柔軟に対応できる）
- リクエストを 1 つにまとめられる
- バージョン管理が不要で、データ構造の変更に影響されず API を提供できる

### GraphQL デメリット

- 学習コストが高い（RESTful API 比）
- クエリが複雑になる可能性がある
- キャッシュはクライアント側で管理する必要がある

## まとめ

　API の理解のために今回まとめてみましたが、以下のことを知ることができました。

- REST はどういう考え方のもとできたのか
- REST と RESTful API の言葉の意味
- GraphQL がどんな経緯でできたのか
- まずは RESTful API を作ることで、API の基礎を理解したほうが良い
- 複雑なデータ取得などを効率よく取得したいときに GraphQL が使えそう
- 初歩の API の構成要素

　理解しやすいことから REST に則った API をよく目にし実装してきましたが、GraphQL のような他の種類があることから、正しく概念を理解し使い分けることが重要だと考えています。さらに API について理解を深めていきたいです。

　長くなってしまいましたが、ここまで読んでくださった方々に御礼を申し上げます。　

## 参考文献

- [【図解】RESTful API とは何なのか【2024年版】](https://ramble.impl.co.jp/2886/#toc17)（ramble  - Impl Tech Media -）
- [GraphQLとRESTの比較─知っておきたい両者の違い](https://kinsta.com/jp/blog/graphql-vs-rest/)（Kinsta）
- [バックエンドエンジニアがGraphQL触ってみた](https://qiita.com/h-i-ist/items/160822552fc9842627b9) by @h-i-ist 様