はじめに
本記事は公式サイト「GraphQL Learn」をGraphQL入門向けに一部抜粋したものです。
目次
概要
GraphQL(グラフキューエル)は、Facebook が 2012 年に社内プロジェクトとして開発を始め、その後 2015 年にオープンソース化された API クエリ言語およびランタイムです。さまざまなプログラミング言語で実装されています。
スキーマ
API 開発者は GraphQL を使用してスキーマを作成し、クライアントがこのサービスを通じてクエリできるすべてのデータを記述します。
GraphQL スキーマはオブジェクトタイプで構成され、どの種類のオブジェクトをリクエストでき、どのフィールドを指定できるかを定義します。
クエリを受け取ると、GraphQL はクエリをスキーマに照合して検証し、検証されたクエリを実行します。
API 開発者は、スキーマの各フィールドにリゾルバーという関数を割り当てます。実行中にリゾルバーが呼び出されて値を生成します。
GraphQL はあらゆるバックエンドフレームワークやプログラミング言語と併用できるため、ここでは実装に依存しない概念のみを扱います。
スキーマはスキーマ定義言語(SDL(Schema Definition Language))を使って表現します。
オブジェクト型とフィールド
GraphQL スキーマの最も基本的な構成要素は オブジェクト型(Object Type) です。
スキーマ定義言語(SDL) では、以下のように定義されます:
type Character {
name: String!
appearsIn: [Episode!]!
}
-
name
とappearsIn
は、Character
型の フィールド。GraphQL クエリの中でCharacter
にアクセスする場合に選択できるフィールドはこれらです -
String
は 組み込みスカラー型の一つで、スカラー値(単一の値)を返します。クエリでサブフィールドを持つことはできません -
String!
の ! は Non-Null 型 を意味します。GraphQL サービスはこのフィールドに対して必ず値を返すことを約束します -
[Episode!]!
は Episode 型のリストです。! によって、リスト自体も null にならず、リスト内の各要素も null でないことを表します
引数(Arguments)
GraphQL のオブジェクト型の各フィールドには、引数(arguments)を設定できます。
例:
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}
- length フィールドには unit という引数が定義されています
- GraphQL ではすべての引数が「名前付き」で渡されます(JavaScript や Python のように順番で渡すのではなく、名前で指定)
スカラ型(Scalar Types)
GraphQL のオブジェクト型のフィールドは、最終的に 具体的なデータ型に解決される必要があります。これを担うのが スカラ型(Scalar Types)です。
例:
{
hero {
name
appearsIn
}
}
ここで name や appearsIn は、サブフィールドを持たない「葉(leaf)」の値であり、スカラ型に該当します。
組み込みのスカラ型
-
Int
:32ビット符号付き整数 -
Float
:倍精度浮動小数点数 -
String
:UTF-8 文字列 -
Boolean
:真偽値(true / false) -
ID
:一意な識別子
カスタムスカラ型の定義
例えば、日付を扱う Date 型を定義するには:
scalar Date
この型のシリアライズ/デシリアライズ/バリデーション方法は実装側で定義します。
列挙型(Enum Types)
列挙型(Enum)は、許可された値の集合から選ばれた特定の値だけを使用できる、特別な種類のスカラ型です。
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
実装によっては、言語ごとに列挙型の取り扱いが異なります
型修飾子(Type Modifiers)
GraphQL の型は、デフォルトでは「nullable(null許容)」かつ「単一」として扱われます。
しかし、以下の2つの修飾子を使うことで意味を変えることができます:
-
Non-Null(
!
):null を許可しない -
List(
[]
):配列(リスト)として扱う
Non-Null の例:
type Character {
name: String!
}
リゾルバが null を返すと、GraphQL の実行時エラーになります。
引数にも Non-Null を使うことができます
List の例:
type Character {
name: String!
appearsIn: [Episode]!
}
この例では、appearsIn は Episode 型の配列です。
リストと Non-Null を組み合わせた定義
型定義 | 意味 |
---|---|
myField: [String!] |
リストは null 可、ただしリストの中の各要素は null 不可 |
myField: [String]! |
リスト自体は null 不可、ただし中の要素は null 可 |
myField: [String!]! |
リストもその中の要素もすべて null 不可 |
インターフェース型
インターフェース型は、ある特定のフィールドのセットを定義し、それを実装する具体的なオブジェクト型や他のインターフェース型がそのフィールドを必ず含むようにします。
例:Star Wars におけるキャラクターを表すインターフェース
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
この Character インターフェースを実装するすべての型は、同じフィールド名・引数・戻り値の型を持っていなければなりません。
実装例:
type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
starships: [Starship]
totalCredits: Int
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
primaryFunction: String
}
上記のように、両方の型が Character の必須フィールドを持ちつつ、それぞれ固有のフィールド(starships、totalCredits、primaryFunction)を追加できます。
インターフェース型を使用する際の注意点
インターフェースを返すフィールドに対して、具体的な型にしか存在しないフィールドを直接クエリすることはできません。
NG例:
{
hero(episode: JEDI) {
name
primaryFunction
}
}
上記はエラーになります。なぜなら hero は Character 型を返す可能性があり、その型には primaryFunction が存在しないからです。
OK な書き方(インラインフラグメントを使用):
{
hero(episode: JEDI) {
name
... on Droid {
primaryFunction
}
}
}
インターフェース同士の継承
インターフェース型は 他のインターフェースを実装することも可能です。
ただし、自分自身を実装したり、循環的に参照することはできません。
interface Node {
id: ID!
}
interface Character implements Node {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
ユニオン型
複数の 具体的なオブジェクト型をまとめる
メンバー型には オブジェクト型しか指定できない(インターフェースや他のユニオン型は不可)
定義例:
union SearchResult = Human | Droid | Starship
この場合、SearchResult 型を返すクエリは、Human, Droid, Starship のいずれかのデータを返す可能性があります。
ユニオン型を使うクエリの例:
{
search(text: "an") {
__typename
... on Human {
name
height
}
... on Droid {
name
primaryFunction
}
... on Starship {
name
length
}
}
}
レスポンス例:
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo",
"height": 1.8
},
{
"__typename": "Human",
"name": "Leia Organa",
"height": 1.5
},
{
"__typename": "Starship",
"name": "TIE Advanced x1",
"length": 9.2
}
]
}
}
入力オブジェクト型
Enum や String などのスカラー値を引数として使うだけでなく複雑なオブジェクトを引数として渡す場合には、Input Object Type を使います。
注意点:
- Input Object Type のフィールドには 他の Input Object Type を使うことは可能
SDLでの定義例:
input ReviewInput {
stars: Int!
commentary: String
}
type Mutation {
createReview(episode: Episode, review: ReviewInput!): Review
}
クエリの例:
mutation {
createReview(
episode: JEDI,
review: {
stars: 5
commentary: "This is a great movie!"
}
) {
stars
commentary
}
}
Queries
クライアント側から、最もよく利用される GraphQL の操作はクエリとミューテーションです。これらを CRUD モデルの create (作成)、read (読み取り)、update (更新)、delete (削除) と比較すると、クエリは read (読み取り) に相当します。ミューテーションがその他すべて (create、update、delete) に対応します。
GraphQL のクエリ言語は基本的に「オブジェクトのフィールドを選択すること」に特化しています。
hero が返すオブジェクトに対して、name と appearsIn のフィールドを選択します。
GraphQL の仕様では、レスポンスのトップレベルの data キーに結果が格納されると定義されています。
エラーが発生した場合は、errors キーに何が起きたかが記載されます。
クエリ(Operation):
{
hero {
name
appearsIn
}
}
レスポンス(Response):
{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
フィールドはオブジェクト型(やその配列)を返すこともできます。
その場合、さらにそのオブジェクトの中のフィールドも選択することができます。
{
hero {
name
friends {
name
}
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{ "name": "Luke Skywalker" },
{ "name": "Han Solo" },
{ "name": "Leia Organa" }
]
}
}
}
このように GraphQL のクエリは関連オブジェクトとそのフィールドをたどることができるため、従来の REST アーキテクチャのように複数回 API を呼び出すことなく、1 回のリクエストで大量の関連データを取得可能です。
この例では friends フィールドが配列を返しています。GraphQL クエリの構文は、単一要素でもリストでも同じですが、スキーマを見ればどちらかを判断できます。
引数(Arguments)
オブジェクトとそのフィールドをたどるだけでも GraphQL は非常に便利ですが、フィールドに引数を渡せることで、さらに強力な機能を発揮します。
type Query {
human(id: ID!): Human
}
この場合、クライアントは id を指定してクエリを送信する必要があります:
{
human(id: "1000") {
name
height
}
}
操作タイプと名前(Operation type and name)
先ほどまでは省略構文で query キーワードを明記していませんでしたが、実際は以下のように操作タイプを明示し、オペレーションに名前を付けることが可能です:
query HeroNameAndFriends {
hero {
name
friends {
name
}
}
}
- 操作タイプは query、mutation、subscription のいずれかで、どの操作をするかを明示します。省略構文でない限り(特に mutation や subscription の場合)、このキーワードは必須です
- 操作名(オペレーション名)はその操作に割り当てる識別名で、デバッグやサーバー側のログでクエリを特定しやすくなるため、1 件のクエリしか送らない場合でも付けておくことが推奨されます
エイリアス(Aliases)
注意深い人はすでに気づいたかもしれませんが、GraphQL のレスポンスのフィールド名はクエリと一致するため、同じフィールドに異なる引数を渡して複数回呼び出すことができません。
そこで役立つのが エイリアス です。エイリアスを使えば、フィールド名を任意に変更してレスポンスを分けて取得できます。
query {
empireHero: hero(episode: EMPIRE) {
name
}
jediHero: hero(episode: JEDI) {
name
}
}
変数(Variables)
GraphQL では「変数(variables)」という仕組みを使って、動的な値をクエリの外に切り出して渡すことができます。
※ 変数を使うには、必ずクエリに操作タイプ(例: query)と名前を指定する必要があります。
Operation(クエリ):
query HeroNameAndFriends($episode: Episode) {
hero(episode: $episode) {
name
friends {
name
}
}
}
Variables(変数):
{
"episode": "JEDI"
}
デフォルト変数(Default Variables)
変数にデフォルト値を持たせることもできます:
query HeroNameAndFriends($episode: Episode = JEDI) {
hero(episode: $episode) {
name
friends {
name
}
}
}
フラグメント(Fragments)
GraphQL には フラグメント(fragment) という再利用可能なフィールドセットがあります
...comparisonFields で共通のフィールドセットを再利用でき、複雑なクエリを簡潔に保つことができます。
これはクライアント側の機能です
query {
leftComparison: hero(episode: EMPIRE) {
...comparisonFields
}
rightComparison: hero(episode: JEDI) {
...comparisonFields
}
}
fragment comparisonFields on Character {
name
appearsIn
friends {
name
}
}
インラインフラグメント(Inline Fragments)
フィールドがインターフェースやユニオン型を返す場合、実際の具体型のフィールドにアクセスするには「インラインフラグメント」が必要です:
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
メタフィールド
ユニオン型など、返される型が不確定な場合、クライアント側でどの型が返ってきたかを判断する必要があります。
GraphQL ではそのために、__typename という メタフィールド をどの階層でも取得できます
Operation:
{
search(text: "an") {
__typename
... on Human {
name
}
... on Droid {
name
}
... on Starship {
name
}
}
}
Response:
{
"data": {
"search": [
{ "__typename": "Human", "name": "Han Solo" },
{ "__typename": "Human", "name": "Leia Organa" },
{ "__typename": "Starship", "name": "TIE Advanced x1" }
]
}
}
Mutations
これらを CRUD モデルの create (作成)、read (読み取り)、update (更新)、delete (削除) と比較すると、クエリは read (読み取り) に相当します。ミューテーションがその他すべて (create、update、delete) に対応します。
スキーマで定義されたミューテーションの例:
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
input ReviewInput {
stars: Int!
commentary: String
}
type Mutation {
createReview(episode: Episode, review: ReviewInput!): Review
}
操作:
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
バリデーションの例
GraphQL サーバーがリクエストにバリデーションエラーを検出すると、レスポンスの errors キーに詳細が返されます。
また、GraphQL ではバリデーションエラーがあると 実行フェーズに入る前にリクエストエラーを返すため、部分的なデータがレスポンスに含まれることはありません。
さらに、GraphQL の仕様により、すべての実装がスキーマに対してリクエストを検証することが義務付けられているため、開発者はランタイムでのバリデーション処理を個別に実装する必要はありません。
存在しないフィールドの要求
フィールドをクエリする際は、そのフィールドが該当の型に定義されていなければなりません。
# 無効:Character に favoriteSpaceship は存在しない
query {
hero {
favoriteSpaceship
}
}
セレクションセットとリーフフィールド
スカラー型または列挙型以外の型を返すフィールドをクエリする場合、取得したいサブフィールドを選択する必要があります。
# 無効:hero はスカラー型でないので、サブフィールドが必要
query {
hero
}
逆に、スカラー型や列挙型のフィールドにサブフィールドを指定すると無効になります
循環的なフラグメントの展開
以下のように 同じフラグメントを再帰的に自分自身に展開しようとすると無効になります:
query {
hero {
...NameAndAppearancesAndFriends
}
}
fragment NameAndAppearancesAndFriends on Character {
name
appearsIn
friends {
...NameAndAppearancesAndFriends
}
}
Response
GraphQL の強みのひとつは、サーバーのレスポンスがクエリと同じ構造を持つ点ですが、実行中やその前に予期せぬことが起きた場合には、補足情報(エラーなど)も含まれることがあります。
data
GraphQL リクエストが実行されると、結果はレスポンスの data キーの中に格納されます。
data キーが含まれているのは、GraphQL の実装ごとの設計ではなく、GraphQL の仕様で定められているものです。
なお、GraphQL の仕様では レスポンス形式(シリアライズ形式)は定められていません が、通常は JSON で返されます。
また、使用される 通信プロトコル(トランスポート)にも指定はなく、クエリやミューテーションは HTTP 経由で、長寿命なサブスクリプションは WebSocket や Server-Sent Events(SSE)を通じて行われるのが一般的です。
errors
data に加えて、GraphQL 仕様では errors キーの形式も定義されています。
エラーが発生した場合でも、フィールドの一部でデータ取得が成功していれば、data と errors の両方を含むレスポンス(部分レスポンス)になることがあります。
リクエストエラー(Request errors)
これは主に クライアント側のミスによって起こるエラーで、例えば構文エラー(括弧の抜けなど)や不正なルート操作型の使用が原因です。
これらのリクエストエラーは、実行フェーズに入る前に検出されるため、data キーはレスポンスに含まれません。
バリデーションエラー(Validation errors)
構文的には正しくても、スキーマに照らして不正な場合に発生します。
これらのバリデーションエラーは、実行フェーズに入る前に検出されるため、data キーはレスポンスに含まれません。
フィールドエラー(Field errors)
実行中に何らかの問題が発生した場合に発生します。
例えば:
- リゾルバ関数で明示的にエラーを投げた
- Non-Null 型のフィールドに
null
を返した
この場合、GraphQL は可能な限り他のフィールドを実行し、errors にエラー情報を、data に取得できた部分データを含むレスポンスを返します。
例:2 つの starship を削除するミューテーションのうち、1 つが失敗
mutation {
firstShip: deleteStarship(id: "3001")
secondShip: deleteStarship(id: "3010")
}
{
"errors": [
{
"message": "Starship not found",
"locations": [{ "line": 3, "column": 3 }],
"path": ["secondShip"]
}
],
"data": {
"firstShip": "3001",
"secondShip": null
}
}
ネットワークエラー(Network errors)
GraphQL に特有ではありませんが、通信中に発生する以下のようなエラーもあり得ます:
- SSL エラー
- タイムアウト
- 接続断
これらは GraphQL の処理に入る前に発生するため、通常の data
や errors
とは異なる方法で処理される必要があります。クライアントやサーバーライブラリには、自動再試行などのエラーハンドリング機能が組み込まれている場合もあります。
拡張情報(extensions)
GraphQL 仕様において、レスポンスで許可されているもう一つのトップレベルキーが extensions
です。
- 任意の追加情報を含めるために使用される
- 中身はオブジェクトでなければならないが、内容には制限なし
例:
- テレメトリ情報
- レートリミット情報
- キャッシュヒント など
Introspection
GraphQL スキーマがどんな機能をサポートしているかを知るには、しばしばスキーマ自身に尋ねるのが便利です。GraphQL はインスペクション(introspection)機能を通じてこれを可能にしています。
インスペクション・クエリは、GraphQL API のスキーマ情報を取得するための特殊なクエリで、GraphQL 開発ツールの多くもこの仕組みを利用しています。
ここでは詳細は割愛します。
Serving over HTTP
GraphQL仕様では、APIリクエストおよびレスポンスの送受信に特定のクライアント–サーバープロトコルを要求していませんが、その汎用性ゆえにHTTPが最も一般的な選択肢となっています。
APIエンドポイント
RESTが「リソース」をURLで識別するのに対し、GraphQLはエンティティのグラフを扱います。そのため、GraphQLサーバーは通常 /graphql のような単一のエンドポイントで動作し、当該サービスへのすべてのリクエストはこのURLに送られます。
リクエストの形式
Accept
クライアントは、サーバーから受け取れるメディアタイプを Accept ヘッダーで示します。推奨値は
application/graphql-response+json
過去の互換性
2025年1月1日以前のレガシーサーバーとの互換性を保つには、Accept ヘッダーに以下を両方含めます。
application/graphql-response+json;charset=utf-8, application/json;charset=utf-8
HTTPメソッド
- 必須:POST
- クエリ/ミューテーション操作にはHTTP POSTを必ずサポートすること
- 任意:GET
- クエリ操作に限り、GETメソッドを受け付けてもかまいません
POSTリクエストとボディ
-
ヘッダーに
Content-Type: application/json
を設定し、以下のようなJSONボディを送信します。{ "query": "...", "operationName": "...", "variables": { "myVariable": "someValue", ... }, "extensions": { "myExtension": "someValue", ... } }
-
"query"
は必須で、GraphQLドキュメント(クエリ/ミューテーション/フラグメント)を含みます -
"operationName"
,"variables"
,"extensions"
は任意です。複数の操作を含む場合は"operationName"
が必要になります -
Content-Type
ヘッダーがないとサーバーは4xxを返すべきです。エンコーディング指定がなければUTF-8が想定されます
レスポンスの形式
ボディ
-
レスポンスは常にJSONオブジェクトで返し、以下のキーを含みます。
{ "data": { ... }, "errors": [ ... ], // エラーがない場合は省略 "extensions": { ... } // 任意 }
-
実行前のエラー(バリデーションエラー等)がある場合は
"data"
を含まず、必ず"errors"
を返します。 -
"extensions"
は実装に応じて任意で追加できます。
ステータスコード
- 2xx
- レスポンスに
"data"
キーが存在し、その値がnull
でない場合は、部分的にエラーが含まれていても2xxを返します(HTTPには「部分的成功」を表すステータスがないため)。
- レスポンスに
- 400
- GraphQL操作の実行を開始できないバリデーションエラー時に返されるのが一般的です。一部のレガシーサーバーは
application/json
で2xxを返す場合があります。
- GraphQL操作の実行を開始できないバリデーションエラー時に返されるのが一般的です。一部のレガシーサーバーは
- 4xx/5xx
-
application/graphql-response+json
で実装準拠のGraphQLリクエストが失敗した場合は4xxまたは5xxを返します。application/json
の場合は解釈の曖昧さを避けるため2xxを返すのが推奨されますが、実装により異なります。
-
バージョニング
GraphQL ではクライアントが明示的にリクエストしたデータのみを返すため、新しい型や既存型へのフィールド追加はクライアントに不要なデータを返さず、破壊的変更を引き起こしません。そのため、破壊的変更を常に避け、バージョンレスの API を提供することが一般的な慣習になっています。