GraphQLを使ってTODOアプリを作る

GraphQLを使ってTODOアプリを作る

2024-05-14

はじめに

今回はPythonのFastAPIとStrawberryを使ってGraphQL APIを開発する方法を解説します。 Todoアプリの開発を通してGraphQL APIを開発する方法を学びます。

GitHubリポジトリはこちらにあります。ブランチが3つあり、それぞれが章での作業と連動しています。

目次

対象読者と非対称読者

対象読者

  • GraphQLについてある程度知っている人
  • Pythonの基礎的な構文について知っている人
  • PythonでGraphQLサーバーを作ってみたい人

非対称読者

  • GraphQLを知らない
  • Pythonを知らない

環境

  • MacOS : Sonoma 14.2.1
  • Python : 3.11.4
  • FastAPI: 0.111.0

環境の準備

適当な作業ディレクトリで以下コマンドを実行

pip install fastapi 'uvicorn[standard]' strawberry-graphql

0: PythonでGraphQLを開発できるライブラリ

DjangoやFlask, FastAPIなどWebAPIを開発できるPythonのフレームワークが複数あるように、GraphQL APIを開発するフレームワークもいくつか存在します。

  1. Graphene : 最も広く使われているライブラリの一つ
  2. Ariadne : スキーマファースト開発を目的として構築されたライブラリ
  3. Strawberry : スキーマモデルを簡単に実装できるようにするライブラリ
  4. Tartiflette: スキーマファーストアプローチを使って構築できる

今回はFastAPIと相性の良いStrawberryを使ってGraphQL APIを作っていきます。

1: とりあえずサーバー作る

まずはサーバーを立てるところから始めます。ブランチは1-create-serverです。

import strawberry
from fastapi import FastAPI
from strawberry.asgi import GraphQL

# (1)型の定義
@strawberry.type
class Todo:
    title: str

# (2)クエリの定義
@strawberry.type
class Query:

    # (3)リゾルバを作成
    @strawberry.field
    def todo(self) -> Todo:
        return Todo(title='first todo')

# (4)スキーマ定義
schema = strawberry.Schema(query=Query)

# (5)アプリケーションの起動
graphql_app = GraphQL(schema)

app = FastAPI()

app.add_route('/graphql', graphql_app)

型の定義

# (1)型の定義
@strawberry.type
class Todo:
    title: str

まずは型の定義です。今回はTodoを定義しています。まずはStringのTitleのみ定義しています。

strawberryではIntやBoolean, IDなどの方がサポートされています。

参考:https://strawberry.rocks/docs/general/schema-basics#scalar-types

クエリ・リゾルバの定義

# (2)クエリの定義
@strawberry.type
class Query:

    # (3)リゾルバを作成
    @strawberry.field
    def todo(self) -> Todo:
        return Todo(title='first todo')

クエリとリゾルバの定義を行います。

@strawberry.filedを使って、リゾルバを作成します。

今回はfirst todoというタイトルのTodoを返却しています。

スキーマ定義とアプリの起動

# (4)スキーマ定義
schema = strawberry.Schema(query=Query)

# (5)アプリケーションの起動
graphql_app = GraphQL(schema)

app = FastAPI()

app.add_route('/graphql', graphql_app)

最後にスキーマ定義とアプリケーションの起動を行っています。

サーバーの起動

以下コマンドでサーバーを起動します。

uvicorn main:app --reload

localhost:8000にアクセスして、クエリを実行すると結果が得られます。

query{
  todo{
    title
  }
}
{
  "data": {
    "todo": {
      "title": "first todo"
    }
  }
}

2: Mutationの作成

次にMutationを作成します。ブランチは2-create-mutationです。


# import 追加
from typing import List

import strawberry
from fastapi import FastAPI
from strawberry.asgi import GraphQL

# 省略...

# (1) Todoを保存するリストの作成
todos: List[Todo] = []

@strawberry.type
class Query:

    # (2): クエリをTodoのリストを返却するように変更
    @strawberry.field
    def todo(self) -> List[Todo]:
        return todos

# (3): Mutationを追加
@strawberry.type
class Mutation:
    @strawberry.mutation
    def create_todo(self, title: str) -> Todo:
        new_todo = Todo(title=title)
        todos.append(new_todo)
        return new_todo

# (4): スキーマ定義にMutationを追加
schema = strawberry.Schema(query=Query, mutation=Mutation)

graphql_app = GraphQL(schema)

app = FastAPI()

app.add_route('/graphql', graphql_app)

保存リストの作成

# (1) Todoを保存するリストの作成
todos: List[Todo] = []

今回はデータの保存にDBは使わずに、リストに格納していくことにします。 そのためのTodoを保存しておくためのリストの宣言を行っています。

クエリの修正

@strawberry.type
class Query:

    # (2): クエリをTodoのリストを返却するように変更
    @strawberry.field
    def todo(self) -> List[Todo]:
        return todos

これまではクエリは決められたただ一つのTodoを返すだけでしたが、ここでは(1)で作成した、Todoのリストを返却するように変更しています。

Mutationの追加

# (3): Mutationを追加
@strawberry.type
class Mutation:
    @strawberry.mutation
    def create_todo(self, title: str) -> Todo:
        new_todo = Todo(title=title)
        todos.append(new_todo)
        return new_todo

# (4): スキーマ定義にMutationを追加
schema = strawberry.Schema(query=Query, mutation=Mutation)

最後にMutationの追加を行っています。Mutationクラスを作成し、その中に@strawberry.mutationを使用して、Mutationの実際の処理を記述していきます。

create_todoメソッドではTodoのtitleを引数として受け取り、新しいTodoクラスをインスタンス化します。

作成したインスタンスをtodosにappendして、生成したTodoをリストに追加します。

最後に新しく作成したインスタンスを返却して処理は終了です。

作成したMutationwoGraphQLサーバーで実行するためにスキーマ定義にmutationを渡します。

サーバーの起動

先ほど同様に以下コマンドでサーバーを起動します。

uvicorn main:app --reload

localhost:8000にアクセスして、まずはミューテーションを実行します。

mutation{
  createTodo(title: "New Todo 1") {
    title
  }
}

クエリを実行するとミューテーションで作成したTodoが得られます。

query{
  todo{
    title
  }
}
{
  "data": {
    "todo": [
      {
        "title": "New Todo 1"
      }
    ]
  }
}

3: プロパティの追加

最後にプロパティの追加を行います。現在TodoモデルはStringのtitleというプロパティしか保持していません。これに以下のプロパティを追加します。

  • ID : Todoを一意に表すID
  • limit(datetime): Todoの期限を保持する
  • priority(enum) : Todoの優先順位を保持する
  • subTodos : TodoのサブTodo

ブランチは3-add-relationshipです。

まずはmain.pyを以下のようにします。

from typing import List

# import 追加
from datetime import datetime
import uuid
import enum

import strawberry
from fastapi import FastAPI
from strawberry.asgi import GraphQL

# import 追加
from strawberry.scalars import ID

# (1): Priorityで選択可能なenumを定義
@strawberry.enum
class Priority(enum.Enum):
    HIGH = "高い"
    MEDIUM = "中くらい"
    LOW = "低い"

# (2): SubTodo型の定義
@strawberry.type
class SubTodo:
    id: ID
    subtitle: str

# (3): プロパティの追加
@strawberry.type
class Todo:
    id: ID
    title: str
    limit: datetime
    priority: Priority
    subTodos: List[SubTodo] = strawberry.field(default_factory=list)

todos: List[Todo] = []

@strawberry.type
class Query:

    @strawberry.field
    def todo(self) -> List[Todo]:
        return todos

@strawberry.type
class Mutation:

    # (4): todo作成時に各種プロパティを設定
    @strawberry.mutation
    def create_todo(self, title: str,
                    limit: datetime,
                    priority: Priority
                    ) -> Todo:
        new_todo = Todo(
                    id=strawberry.ID(str(uuid.uuid4())),
                    title=title,
                    limit=limit,
                    priority=priority
                   )
        todos.append(new_todo)
        return new_todo

    # (5): SubTodoの作成用mutationの定義
    @strawberry.mutation
    def create_sub_todo(self, todo_id: strawberry.ID, subtitle: str) -> SubTodo:

        for todo in todos:
            if todo.id == todo_id:
                new_sub_todo = SubTodo(id=strawberry.ID(str(uuid.uuid4())), subtitle=subtitle)
                todo.subTodos.append(new_sub_todo)
                return new_sub_todo

        raise ValueError(f"Todo with ID {todo_id} not found")

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

graphql_app = GraphQL(schema)

app = FastAPI()

app.add_route('/graphql', graphql_app)

Enumの定義

# (1): Priorityで選択可能なenumを定義
@strawberry.enum
class Priority(enum.Enum):
    HIGH = "高い"
    MEDIUM = "中くらい"
    LOW = "低い"

Todoのpriorityフィールドでは、優先順位を高い、中くらい、低い、の中から選べるようにしたいので、enumを使って定義を行います。

SubTodoの定義

# (2): SubTodo型の定義
@strawberry.type
class SubTodo:
    id: ID
    subtitle: str

Todoでは複数のSubTodoを持てるように変更します。そのためSubTodo型を定義しておきます。

Todoのプロパティの追加

# (3): プロパティの追加
@strawberry.type
class Todo:
    id: ID
    title: str
    limit: datetime
    priority: Priority
    subTodos: List[SubTodo] = strawberry.field(default_factory=list)


Todoモデルにプロパティの追加を行っています。

    # (4): todo作成時に各種プロパティを設定
    @strawberry.mutation
    def create_todo(self, title: str,
                    limit: datetime,
                    priority: Priority
                    ) -> Todo:
        new_todo = Todo(
                    id=strawberry.ID(str(uuid.uuid4())),
                    title=title,
                    limit=limit,
                    priority=priority
                   )
        todos.append(new_todo)
        return new_todo

また、create_todoメソッドでも引数を追加し、作成時に渡すように変更します。

SubTodo作成用mutationの作成

    # (5): SubTodoの作成用mutationの定義
    @strawberry.mutation
    def create_sub_todo(self, todo_id: strawberry.ID, subtitle: str) -> SubTodo:

        for todo in todos:
            if todo.id == todo_id:
                new_sub_todo = SubTodo(id=strawberry.ID(str(uuid.uuid4())), subtitle=subtitle)
                todo.subTodos.append(new_sub_todo)
                return new_sub_todo

        raise ValueError(f"Todo with ID {todo_id} not found")

MutationクラスにSubTodoを作成するためのmutationメソッドを追加します。

SubTodoを追加するためのTodoIDを受け取り、IDが一致するTodoに対してさぶTodoを追加します。

もし一致するIDがなければエラーを発生させます。

サーバーの起動

以下コマンドでサーバーを起動します。

uvicorn main:app --reload

localhost:8000にアクセスして、まずはミューテーションを実行します。

mutation {
  createTodo(title: "MainTodo", limit: "2024-05-16", priority: MEDIUM) {
    id
    title
  }
}

Mutationの実行結果からIDを取得し、SubTodoを追加します。

mutation {
  createSubTodo(todoId: "生成されたID", subtitle: "Sub Todo"){
    id
    subtitle
  }
}

Todoが作成されたか確認するために、クエリを投げます。

query {
  todo{
    id
    title
    limit
    priority
    subTodos {
      subtitle
    }
  }
}
{
  "data": {
    "todo": [
      {
        "id": "b58d5881-f39e-4405-b9c1-103bc51b2996",
        "title": "MainTodo",
        "limit": "2024-05-16T00:00:00",
        "priority": "MEDIUM",
        "subTodos": [
          {
            "subtitle": "Sub Todo"
          }
        ]
      }
    ]
  }
}

以上でStrawberryを使って、GraphQLサーバを構築する方法の解説を終了します。

参考