blog.takurinton.dev

GraphQL入門

2021-05-17

はじめに

こんにちは、僕です。

この記事は 技術メモ にまとめたものの総括みたいな感じです。

ということでGraphQL に入門したのでまとめます。
ここでは Query と Mutation については触れますが Subscription については触れません。(これで入門したとか言うな)
一応 Subscription については後日追記予定です。
手を動かした時間で言うと10時間もないのでそこまで深くやっているわけではありません。
一旦ここにまとめてから深めて行きたいなと思っています。

勉強するときに参考にしたサイトなど

概念のお勉強は書籍を使った方がいいと思ってる人間なのですが、今回はネット上のものだけを拾いながら勉強しました(辛い)
参考にしたサイトはいかです。あとは手を動かして頑張りました。

ここら辺を参考にしました。
最初は何言ってるかわからなかったのでちょこちょこ行ったり来たりしながらやりました。辛かった〜〜。

基本的なところ

まず導入としてここがよくわかりませんでした。
GraphQL にはどうやら QueryMutation というものがあるということで何してるかわからなかったです。ここに一番時間使った気がします。

  • 共通の考え方
    • 基本的には POST リクエストの body に情報を持たせることが多いらしい
    • GET のクエリパラメータとして情報を持たせることもできる
  • Query
    • 情報を取得する際に使用する
  • Mutation
    • 情報を送る際に取得する

こんな感じです。
基本的にはこれだけでいいと思います。あとはやって覚えたほうが早い気がする。

リクエストを送る形式について

次にリクエストを送る形式についてを説明します。
GraphQL は REST とはだいぶ異なる形でリクエストを送ります。
REST では完全に JSON という形でしたが GraphQL ではそうではありません。

まず Query と Mutation の場合で書き方が違います(ほぼ同じだけど)
ここではわかりやすいように以下のようなシンプルな JSON を使用して説明をしたいと思います。

[
    {
        "id": 1,
        "name": "takurinton"
    }, 
    {
        "id": 2,
        "name": "hoge"
    }, 
    {
        "id": 3,
        "name": "fuga"
    }
]

共通のこと

上で GraphQL は GET でも POST でもリクエストを送信できるという話をしました。
イメージとしては、GET リクエストの場合はクエリパラメータとして、POST リクエストの場合は body として持たせてあげる感じです。
REST の思想が強めの人はここで GET は取得、POST は作成という概念を消してください。(もっと言うと PUT, PATCH, DELETE のことも忘れてください)
また、基本的にはエンドポイントは1つです、それも頭に入れておくと良きかもしれません。

以下でちょこちょこ出てくる なんとかUser というのはアプリケーション側で任意の名前をつけることができるものです。

 

Query の場合

Query の場合は以下のような書き方をします。
まずは先ほどの JSON の内容を全て取得したいと思います。

GET リクエストの場合は以下のような形で投げることができます。

curl -g http://localhost:8888/graphql?query={getUsers{id,name}}

次に POST リクエストの場合は以下のような形で投げることができます。

curl -X POST -H "Content-Type: application/json" --data '{ query { getUsers { id name } } }' http://localhost:8888/graphql

GET でも POST でもやってることは同じで、レスポンスは上の JSON が返ってきます。

また、id が1のユーザーを取得するなどといった、可変の値を渡すこともできます。以下の POST リクエストのような形で渡してあげることができます。

curl -X POST -H "Content-Type: application/json" --data '{ "query": " getUser(id:\"1\") { id name } " }' http://localhost:8888/graphql

これはアプリケーション側で getUser は引数を取るよということを明示的に定める必要があります。
このような形で値を渡すこともできます。

Mutation の場合

Mutation は更新系の処理をしたい場合に使用します。
REST でいう GET 以外の部分がここに含まれると思ってください。

POST でリクエストを送ってみたいと思います。
以下の例は先ほどの JSON に値を追加したい時に送る Mutation です。

curl -X POST -H "Content-Type: application/json" --data '{ "query": "mutation { createUser(id:4, name:\"hogehoge\") { id name } }" }' http://localhost:8888/graphql

登録したい内容を送信しないといけないので先ほど同様、可変の値を投げる必要があります。もしこれが DB アクセスをするような場合だと id はオートインクリメントになりそうだからいらないなあとか考えますね。
GraphQL の基本的な形式はこのような形です。

コードがないとわからないよという人は GraphQL に入門した(Go) を見てください。

レスポンスの形式

レスポンスの形式は返ってくる値は JSON ですが REST とは若干異なります。
GraphQL のレスポンスの形式は以下のようになります。

{
    "data":{"任意の名前":{"key":"value",...},
    "errors": [ ... ]}

data はうまく行った時のレスポンスの値、errors は何かしらのミスがあったときにエラーを格納します。

例として、上の全件取得(Query)の場合のレスポンスは以下のようになります。

{"data":{"getUsers":[{"id":1, "name": "takurinton"},{"id":2, "name": "hoge"},{"id":3, "name": "fuga"}]}

一番外側に data がついていて、任意の名前でラッピングされてるだけと思ったらそこまで難しくなく、普通の JSON じゃんと思うかもしれません。そうです。返ってくるのは JSON です。   あまり難しいことはしていないことがわかります。

エラーの場合も同様に errors が返ってきます。例えば querymutation かを明示的に定めないでリクエストを投げると以下のようなエラーが得られます。

{"data":null,"errors":[{"message":"Must provide an operation.","locations":[]}]}

エラーの中にはメッセージが格納されていて、何が悪いのかを教えてくれます。
REST だと自分で丸めてた部分をやってくれてる感じです。便利。

GET と POST の違い

上で GraphQL は GET と POST が使用できるという話をしましたが、これらの違いはなんなのでしょうか?
APOLLO DOCS POST and GET format で触れられています。そもそもの形式が違うらしいです。
また、GET を使う場合は URL をキャッシュしたいときなどに使用できるかもしれないなあなどと思いました。あまり詳しくないのでここらへん有識者の方アドバイスお願いします。

application/json と application/graphql の違い

GraphQL でのやりとりは JSON を使用しているので HTTP header の Content-Type には application/json を使用します。しかし、どうやら application/graphql が使用できるらしいです。
これは iana.org の Media Types で確認することができますが、application/graphql は含まれていません。つまりこれは標準ではなく GraphQL 独自の仕様ということがわかります。
使い分けとしては、 GraphQL のドキュメント で触れられています。

If the "application/graphql" Content-Type header is present, treat the HTTP POST body contents as the GraphQL query string.

とのことなので、body を勝手に GraphQL として扱ってくれるみたいです。意図しない挙動を防ぐためにここら辺は理解したみがあります。

preact でフロントエンドを書いてみる

最終的なコードは以下のようになります。

ディレクトリ構成としては以下のようになっています。

  • src
    • pages
      • ページコンポーネントが入っている
    • querys
      • query が入っている
      • mutation は未実装
    • router
      • 自作のルーターが入っている
      • preact-router と同じ動きをする
    • App.jsx
      • ルーターを呼び出してる
    • main.jsx
      • render する場所
  • vite.config.js
    • vite でバンドルする設定ファイル
  • index.html
    • エントリポイント

実装するもの

簡単な TODO リストを実装してみたいと思います。
フロントエンドは取得だけをします。ごめんなさい。あとで投稿もできるようにするので。(今の段階で TODO リストと呼べるのか怪しい)
データの形式は以下のような形になっています。 (サーバサイドから引用)

MariaDB > show columns from todo;
+------------+---------------+------+-----+---------------------+----------------+
| Field      | Type          | Null | Key | Default             | Extra          |
+------------+---------------+------+-----+---------------------+----------------+
| id         | int(11)       | NO   | PRI | NULL                | auto_increment |
| title      | varchar(127)  | YES  |     | NULL                |                |
| content    | varchar(1023) | YES  |     | NULL                |                |
| is_active  | tinyint(1)    | NO   |     | 1                   |                |
| created_at | timestamp     | NO   |     | current_timestamp() |                |
| updated_at | timestamp     | NO   |     | current_timestamp() |                |
+------------+---------------+------+-----+---------------------+----------------+
6 rows in set (0.001 sec)

Query を定義する

まずはフロントエンドからリクエストを投げる際に使用する query を定義します。
PostQuery では Int 型の引数を受け取るようにしています。

// querys/querys.js

export const PostQuery = `
query PostQuery($id: Int){
  post (id: $id){
    id
    title
    content
    is_active
    created_at
  }
}
`
export const PostsQuery = `
query PostsQuery {
  posts {
    id
    title
    content
    is_active
    created_at
  }
}
`

ページを作成する

本質ではないので基本的な構成などはコードから理解してください。ここでは Posts.jsxPost.jsx についてのみ説明します。

TODO 一覧を取得する

TODO の一覧を取得するためには query の定義が必要です。ということで先ほど定義した query を使用して書いていきます。

const { data, fetching, error } = result
if (fetching) return <p>Loading...</p>
if (error) return <p>Oh no... {error.message}</p>

この部分ですが、ドキュメントに記載があった通りに実装しました。一般的な書き方がわからないので有識者の方教えてください。

全体は以下のようになります。
あまり難しことはしていないので react の hook がわかれば問題ないと言った感じです。

// pages/Posts.jsx

import { Link } from '../router/prefetch';
import { PostsQuery } from '../querys/querys';
import { useQuery } from '@urql/preact';

import { Post } from './Post';

export const Posts = () => {
    const [result] = useQuery({
      query: PostsQuery,
    });
    
    const { data, fetching, error } = result
    if (fetching) return <p>Loading...</p>
    if (error) return <p>Oh no... {error.message}</p>

    return (
      <>
      <h1>All Posts</h1>
      {
        data.posts.map(post => 
          <Link href={`/post/${post.id}`}>
            <Post id={post.id}>{ post.title }</Post>
          </Link>
        )
      }
      </>
    )
}

TODO を取得する

TODO の一覧を取得する際は以下のような形になります。 この部分で引数を渡すことができます。これはライブラリの使用ではなく GeaphQL の仕様です。覚えておくようにしましょう。

const [result] = useQuery({
    query: PostQuery,
    variables: { id },
});

全体のコードは以下のような形になります。

// pages/Post.jsx

import { PostQuery } from '../querys/querys';
import { useQuery } from '@urql/preact';

export const Post = ({ id }) => {
    const [result] = useQuery({
      query: PostQuery,
      variables: { id },
    });
    
    const { data, fetching, error } = result  
    if (fetching) return <p>Loading...</p>
    if (error) return <p>Oh no... {error.message}</p>
    
    return (
      <>
        <h1>title: { data.post.title }</h1>
      </>
    )
}

Go でサーバサイドを書いてみる

最終的なコードは以下のようになります。

ディレクトリ構成としては以下のようになっています。

  • db
    • データベース接続用の関数が入っている
  • model
    • struct が入っている
  • repository
    • データベースアクセスをするための関数が入っている
  • schema
    • GraphQL のスキーマが定義されている
    • Query も Mutation も同じ
  • main.go
    • 実行

実装するもの

簡単な TODO リストを実装してみたいと思います。
データの形式は以下のような形になっています。

MariaDB > show columns from todo;
+------------+---------------+------+-----+---------------------+----------------+
| Field      | Type          | Null | Key | Default             | Extra          |
+------------+---------------+------+-----+---------------------+----------------+
| id         | int(11)       | NO   | PRI | NULL                | auto_increment |
| title      | varchar(127)  | YES  |     | NULL                |                |
| content    | varchar(1023) | YES  |     | NULL                |                |
| is_active  | tinyint(1)    | NO   |     | 1                   |                |
| created_at | timestamp     | NO   |     | current_timestamp() |                |
| updated_at | timestamp     | NO   |     | current_timestamp() |                |
+------------+---------------+------+-----+---------------------+----------------+
6 rows in set (0.001 sec)

データベースとの接続

まずはローカルに DB を作っているのでそれと接続する関数を定義します。 DB は彼女の名前に近いので MariaDB を使用しています。
この関数をデータベースとやりとりするたびに呼び出すという感じです。

// db/init.go

package db

import (
    "os"

    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
)

func DBConn() (*gorm.DB, error) {
    DBMS := "mysql" // mariadb 
    HOSTNAME := os.Getenv("HOSTNAME")
    USERNAME := os.Getenv("USERNAME")
    DBNAME := os.Getenv("DB_NAME")
    PASSWORD := os.Getenv("PASSWORD")
    PORT := os.Getenv("PORT")

    CONNECT := USERNAME + ":" + PASSWORD + "@(" + HOSTNAME + ":" + PORT + ")/" + DBNAME + "?parseTime=true"
    db, err := gorm.Open(DBMS, CONNECT)
    if err != nil {
        return nil, err
    }

    return db, nil
}

Query を定義する

次に Query を定義します。
Query を定義するためにまずは型を定義します。
当然ですが型はデータベースの型と合わせましょう。

// schema/schema.go

var TodoType = graphql.NewObject(graphql.ObjectConfig{
    Name: "Todo",
    Fields: graphql.Fields{
        "id": &graphql.Field{
            Type: graphql.Int,
        },
        "title": &graphql.Field{
            Type: graphql.String,
        },
        "content": &graphql.Field{
            Type: graphql.String,
        },
        "is_active": &graphql.Field{
            Type: graphql.Boolean,
        },
        "created_at": &graphql.Field{
            Type: graphql.DateTime,
        },
        "updated_at": &graphql.Field{
            Type: graphql.DateTime,
        },
    },
})

次にフィールドを定義します。フィールドは上の型を使って TODO を全件取得するものと id に紐づく TODO を取得するものの2パターン作成します。
TodoFields では特定の id に紐づく TODO を1つ使用します。
Args を使用すると引数を受け取ることができます。
Resolve はその後の処理を定義することができます。また、Resolve は GraphQL のレスポンスにもなります。

TodosFieldsTodoFields よりもいくらかシンプルです。
Resolve の中で全件取得するようの関数を呼び出してそのまま戻り値としています。

// schema/schema.go

var TodoFields = &graphql.Field{
    Type:        TodoType,
    Description: "get post detail",
    Args: graphql.FieldConfigArgument{
        "id": &graphql.ArgumentConfig{
            Type: graphql.Int,
        },
    },
    Resolve: func(p graphql.ResolveParams) (interface{}, error) {
        id, ok := p.Args["id"].(int)
        if ok {
            post, err := repository.GetTodo(id)
            if err != nil {
                return model.Todo{}, nil
            }
            return post, nil
        }
        return model.Todo{}, nil
    },
}

var TodosFields = &graphql.Field{
    Type:        graphql.NewList(TodoType),
    Description: "get all post",
    Resolve: func(p graphql.ResolveParams) (interface{}, error) {
        return repository.GetTodos(), nil
    },
}

Mutation を定義する

Mutation も同様に定義することができます。
型は先ほどと同様に定義することができるため、フィールドのみの実装となります。
とは言ってもやってることは変わらないのでそこまで難しくないのかなと思います。
TODO を作成するための CreateTodoFields と更新するための UpdateTodoFields の2つを定義します。

CreateTodoFieldsUpdateTodoFields 共に先ほどと同様 Args で引数を取得します。

// schema/schema.go

var CreateTodoFields = &graphql.Field{
    Type:        TodoType,
    Description: "Create new todo",
    Args: graphql.FieldConfigArgument{
        "title": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
        "content": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
        "is_active": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.Boolean),
        },
    },
    Resolve: func(params graphql.ResolveParams) (interface{}, error) {
        title, _ := params.Args["title"].(string)
        content, _ := params.Args["content"].(string)
        isActive, _ := params.Args["is_active"].(bool)

        _newTodo := model.Todo{
            Title:    title,
            Content:  content,
            IsActive: isActive,
        }

        newTodo, err := repository.CreateTodo(_newTodo)
        if err != nil {
            fmt.Println("create data faild")
        }

        return newTodo, nil
    },
}

var UpdateTodoFields = &graphql.Field{
    Type:        TodoType,
    Description: "Create new todo",
    Args: graphql.FieldConfigArgument{
        "id": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.Int),
        },
        "title": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
        "content": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
        "is_active": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.Boolean),
        },
    },
    Resolve: func(params graphql.ResolveParams) (interface{}, error) {
        id := int64(params.Args["id"].(int)) // ちょっと汚い
        title, _ := params.Args["title"].(string)
        content, _ := params.Args["content"].(string)
        isActive, _ := params.Args["is_active"].(bool)

        _updateTodo := model.Todo{
            Id:       id,
            Title:    title,
            Content:  content,
            IsActive: isActive,
        }

        updateTodo, err := repository.UpdateTodo(_updateTodo)
        if err != nil {
            fmt.Println("update data faild")
        }

        return updateTodo, nil
    },
}

Schema の定義

最後にこれらを1つの Schema として定義します。
先ほど一生懸命定義した関数を当てはめるだけです、簡単です。

// schema/schema.go

var Schema = graphql.SchemaConfig{
    Query: graphql.NewObject(
        graphql.ObjectConfig{
            Name: "TodoQuery",
            Fields: graphql.Fields{
                "getTodo":  TodoFields,
                "getTodos": TodosFields,
            },
        },
    ),
    Mutation: graphql.NewObject(
        graphql.ObjectConfig{
            Name: "TodoMutation",
            Fields: graphql.Fields{
                "createTodo": CreateTodoFields,
                "updateTodo": UpdateTodoFields,
            },
        },
    ),
}

最後に schema.go で実装した内容をまとめると以下のようになります。

// schema/schema.go

package schema

import (
    "fmt"
    "graphql_suburi/backend/model"
    "graphql_suburi/backend/repository"

    "github.com/graphql-go/graphql"
)

var TodoType = graphql.NewObject(graphql.ObjectConfig{
    Name: "Post",
    Fields: graphql.Fields{
        "id": &graphql.Field{
            Type: graphql.Int,
        },
        "title": &graphql.Field{
            Type: graphql.String,
        },
        "content": &graphql.Field{
            Type: graphql.String,
        },
        "is_active": &graphql.Field{
            Type: graphql.Boolean,
        },
        "created_at": &graphql.Field{
            Type: graphql.DateTime,
        },
        "updated_at": &graphql.Field{
            Type: graphql.DateTime,
        },
    },
})

var TodoFields = &graphql.Field{
    Type:        TodoType,
    Description: "get post detail",
    Args: graphql.FieldConfigArgument{
        "id": &graphql.ArgumentConfig{
            Type: graphql.Int,
        },
    },
    Resolve: func(p graphql.ResolveParams) (interface{}, error) {
        id, ok := p.Args["id"].(int)
        if ok {
            post, err := repository.GetTodo(id)
            if err != nil {
                return model.Todo{}, nil
            }
            return post, nil
        }
        return model.Todo{}, nil
    },
}

var TodosFields = &graphql.Field{
    Type:        graphql.NewList(TodoType),
    Description: "get all post",
    Resolve: func(p graphql.ResolveParams) (interface{}, error) {
        return repository.GetTodos(), nil
    },
}

var CreateTodoFields = &graphql.Field{
    Type:        TodoType,
    Description: "Create new todo",
    Args: graphql.FieldConfigArgument{
        "title": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
        "content": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
        "is_active": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.Boolean),
        },
    },
    Resolve: func(params graphql.ResolveParams) (interface{}, error) {

        title, _ := params.Args["title"].(string)
        content, _ := params.Args["content"].(string)
        isActive, _ := params.Args["is_active"].(bool)

        _newTodo := model.Todo{
            Title:    title,
            Content:  content,
            IsActive: isActive,
        }

        newTodo, err := repository.CreateTodo(_newTodo)
        if err != nil {
            fmt.Println("create data faild")
        }

        return newTodo, nil
    },
}

var UpdateTodoFields = &graphql.Field{
    Type:        TodoType,
    Description: "Create new todo",
    Args: graphql.FieldConfigArgument{
        "id": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.Int),
        },
        "title": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
        "content": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
        "is_active": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.Boolean),
        },
    },
    Resolve: func(params graphql.ResolveParams) (interface{}, error) {
        id := int64(params.Args["id"].(int)) // ちょっと汚い
        title, _ := params.Args["title"].(string)
        content, _ := params.Args["content"].(string)
        isActive, _ := params.Args["is_active"].(bool)

        _updateTodo := model.Todo{
            Id:       id,
            Title:    title,
            Content:  content,
            IsActive: isActive,
        }

        updateTodo, err := repository.UpdateTodo(_updateTodo)
        if err != nil {
            fmt.Println("update data faild")
        }

        return updateTodo, nil
    },
}

var Schema = graphql.SchemaConfig{
    Query: graphql.NewObject(
        graphql.ObjectConfig{
            Name: "TodoQuery",
            Fields: graphql.Fields{
                "getTodo":  TodoFields,
                "getTodos": TodosFields,
            },
        },
    ),
    Mutation: graphql.NewObject(
        graphql.ObjectConfig{
            Name: "TodoMutation",
            Fields: graphql.Fields{
                "createTodo": CreateTodoFields,
                "updateTodo": UpdateTodoFields,
            },
        },
    ),
}

呼び出す

ここまで定義したきたのであとは呼び出すだけです。
main.go で net/http を使用してサーバを立ち上げ動作確認をしましょう。
25行目ではバリデーションをかけています。ここでリクエストの形式が違ったり不正があったりしたら弾き、CORS エラーになるようにしています。
また、35行目からの graphql.Do() で GraphQL を実行しています。

// main.go 

package main

import (
    "encoding/json"
    "fmt"
    "graphql_suburi/backend/model"
    "graphql_suburi/backend/schema"
    "log"
    "net/http"

    _ "github.com/go-sql-driver/mysql"
    "github.com/graphql-go/graphql"
)

func main() {
    http.HandleFunc("/graphql", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Headers", "*")
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")

        var p model.PostData
        if r.Method == "OPTIONS" {
        } else if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
            w.WriteHeader(400)
            return
        }

        schema, err := graphql.NewSchema(schema.Schema)
        if err != nil {
            log.Fatalf("failed to get schema, error: %v", err)
        }

        result := graphql.Do(graphql.Params{
            Context:        r.Context(),
            Schema:         schema,
            RequestString:  p.Query,
            VariableValues: p.Variables,
            OperationName:  p.Operation,
        })

        if err := json.NewEncoder(w).Encode(result); err != nil {
            fmt.Printf("could not write result to response: %s", err)
        }
    })

    fmt.Println("listening on :8888 ...")
    if err := http.ListenAndServe(":8888", nil); err != nil {
        log.Fatalln(err)
    }
}

動作確認

動作確認をします。curl でリクエストを投げたいと思います。

TODO を全件取得

リクエスト

curl -X POST -H "Content-Type: application/json" --data '{ "query": "{ getTodos { id title content  } }" }' http://localhost:8888/graphql

レスポンス

{"data":{"getTodos":[{"content":"hoge","id":1,"title":"takumi"},{"content":"marinyan","id":2,"title":"marina"},{"content":"takurinton","id":3,"title":"test3"}]}}

id が 1 の TODO を取得

リクエスト

curl -X POST -H "Content-Type: application/json" --data '{ "query": "{ getTodo(id:1) { id title content  } }" }' http://localhost:8888/graphql

レスポンス

{"data":{"getTodo":{"content":"hoge","id":1,"title":"takumi"}}}

TODO を作成する

リクエスト

curl -X POST -H "Content-Type: application/json" --data '{ "query": "mutation { createTodo(title:\"takurinton\",content:\"wakuwakuwakuwaku\",is_active:true) { id title content is_active created_at } }" }' http://localhost:8888/graphql

レスポンス

{"data":{"createTodo":{"content":"wakuwakuwakuwaku","created_at":"2021-04-15T12:57:22.5392956Z","id":4,"is_active":true,"title":"takurinton"}}}

id が 1 の TODO を更新

リクエスト

curl -X POST -H "Content-Type: application/json" --data '{ "query": "mutation { updateTodo(id:1,title:\"takumi katayama\",content:\"hoge\",is_active:false) { id title content is_active created_at } }" }' http://localhost:8888/graphql

レスポンス

{"data":{"updateTodo":{"content":"hoge","created_at":"0001-01-01T00:00:00Z","id":1,"is_active":false,"title":"takumi katayama"}}}

このような形でそれぞれのメソッドがしっかり動いてることを確認することができました。めでたしめでたし。

まとめ

リクエストやレスポンスに型を持たせることができるのは非常に体験が良くなるなと感じました。
ただ、自分の中でまだまだ良さに気づけていない部分や深めなければいけない部分、ベストプラクティス、一般的な書き方についての理解が足りていないのでもっと頑張ってやっていきたいと思います。