GraphQL入門書:API設計の進化(パート2)
公開: 2022-03-10パート1では、過去数十年にわたってAPIがどのように進化し、それぞれが次のAPIにどのように移行したかを確認しました。 また、モバイルクライアント開発にRESTを使用することの特定の欠点のいくつかについても話しました。 この記事では、モバイルクライアントAPIの設計がどこに向かっているのか、特にGraphQLに重点を置いて見ていきたいと思います。
もちろん、何年にもわたってRESTの欠点に対処しようと試みてきた多くの人々、企業、プロジェクトがあります。HAL、Swagger / OpenAPI、OData JSON API、およびその他の数十の小規模または内部プロジェクトはすべて、スペックのないRESTの世界。 世界を理解して段階的な改善を提案したり、RESTを必要なものにするために十分に異なる部分を組み立てたりするのではなく、思考実験を試してみたいと思います。 過去に機能したテクニックと機能しなかったテクニックを理解した上で、今日の制約と非常に表現力豊かな言語を使用して、必要なAPIをスケッチしてみたいと思います。 実装を順方向に進めるのではなく、開発者の経験から逆方向に作業してみましょう(SQLを見ています)。
最小限のHTTPトラフィック
すべての(HTTP / 1)ネットワーク要求のコストは、遅延からバッテリー寿命までのかなりの数の測定値で高いことがわかっています。 理想的には、新しいAPIのクライアントは、必要なすべてのデータを可能な限り少ないラウンドトリップで要求する方法が必要になります。
最小ペイロード
また、平均的なクライアントは、帯域幅、CPU、およびメモリのリソースに制約があることもわかっているため、クライアントが必要とする情報のみを送信することを目標にする必要があります。 これを行うには、クライアントが特定のデータを要求する方法がおそらく必要になります。
人間が読める形式
私たちはSOAPの時代から、APIを操作するのは簡単ではないことを学びました。人々は、その言及にしかめっ面するでしょう。 エンジニアリングチームは、 curl
、 wget
、 Charles
、ブラウザのネットワークタブなど、長年使用してきたものと同じツールを使用したいと考えています。
ツーリングリッチ
XML-RPCとSOAPから学んだもう1つのことは、特にクライアント/サーバーコントラクトと型システムが驚くほど便利であるということです。 可能であれば、新しいAPIは、JSONやYAMLのような形式の軽さを持ち、より構造化されたタイプセーフなコントラクトのイントロスペクション機能を備えています。
ローカル推論の保存
何年にもわたって、大規模なコードベースを編成する方法のいくつかの指針に同意するようになりました。主なものは「関心の分離」です。 残念ながら、ほとんどのプロジェクトでは、これは一元化されたデータアクセス層の形で崩壊する傾向があります。 可能であれば、アプリケーションのさまざまな部分に、他の機能とともに独自のデータニーズを管理するオプションを設定する必要があります。
クライアント中心のAPIを設計しているので、このようなAPIでデータをフェッチするのがどのように見えるかから始めましょう。 最小限の往復を行う必要があることと、不要なフィールドを除外できる必要があることがわかっている場合は、大量のデータセットをトラバースし、その一部のみをリクエストする方法が必要です。私たちに役立ちます。 クエリ言語はここにうまく収まるようです。
データベースの場合と同じようにデータについて質問する必要はないため、SQLのような命令型言語は間違ったツールのように思われます。 実際、私たちの主な目標は、既存の関係を横断し、比較的単純で宣言的なもので実行できるはずのフィールドを制限することです。 業界は非バイナリデータのJSONにかなり慣れているので、JSONのような宣言型クエリ言語から始めましょう。 必要なデータを記述できる必要があり、サーバーはそれらのフィールドを含むJSONを返す必要があります。

宣言型クエリ言語は、最小のペイロードと最小のHTTPトラフィックの両方の要件を満たしますが、別の設計目標に役立つ別の利点があります。 クエリなどの多くの宣言型言語は、データであるかのように効率的に操作できます。 注意深く設計すれば、クエリ言語により、開発者は大きなリクエストを分解し、プロジェクトにとって意味のある方法でそれらを再結合することができます。 このようなクエリ言語を使用すると、ローカル推論の保存という最終的な目標に向かって進むことができます。
クエリが「データ」になると、エキサイティングなことがたくさんできます。 たとえば、仮想DOMがDOM更新をバッチ処理するのと同じように、すべてのリクエストをインターセプトしてバッチ処理したり、コンパイラを使用してビルド時に小さなクエリを抽出してデータを事前キャッシュしたり、高度なキャッシュシステムを構築したりできます。 ApolloCacheのように。

APIウィッシュリストの最後の項目はツールです。 クエリ言語を使用することですでにこの一部を取得していますが、型システムと組み合わせると真の力が得られます。 サーバー上の単純な型付きスキーマを使用すると、豊富なツールの可能性がほぼ無限に広がります。 クエリを静的に分析してコントラクトに対して検証したり、IDE統合でヒントやオートコンプリートを提供したり、コンパイラでクエリのビルド時の最適化を行ったり、複数のスキーマをつなぎ合わせて連続したAPIサーフェスを形成したりできます。

クエリ言語と型システムを組み合わせたAPIを設計することは劇的な提案のように聞こえるかもしれませんが、人々は何年もの間、さまざまな形でこれを実験してきました。 XML-RPCは、90年代半ばに型付き応答を要求し、その後継であるSOAPは何年にもわたって支配的でした。 最近では、MeteorのMongoDB抽象化、RethinkDB(RIP)Horizon、Netflix.comで何年も使用しているNetflixの素晴らしいFalcorなどがあり、最後にFacebookのGraphQLがあります。 このエッセイの残りの部分では、GraphQLに焦点を当てます。これは、Falcorのような他のプロジェクトが同様のことを行っている一方で、コミュニティのマインドシェアが圧倒的にそれを支持しているように見えるためです。
GraphQLとは何ですか?
まず、私は少し嘘をついたと言わなければなりません。 上で構築したAPIはGraphQLでした。 GraphQLは、データの型システムであり、データをトラバースするためのクエリ言語です。残りは詳細です。 GraphQLでは、データを相互接続のグラフとして記述し、クライアントは必要なデータのサブセットを具体的に要求します。 GraphQLが可能にするすべての素晴らしいことについて話すことや書くことはたくさんありますが、コアの概念は非常に管理しやすく、複雑ではありません。
これらの概念をより具体的にし、GraphQLがパート1の問題のいくつかにどのように対処しようとするかを説明するために、この投稿の残りの部分では、このシリーズのパート1のブログを強化できるGraphQLAPIを構築します。 コードに飛び込む前に、GraphQLについて覚えておくべきことがいくつかあります。
GraphQLは仕様です(実装ではありません)
GraphQLは単なる仕様です。 単純なクエリ言語とともに型システムを定義し、それだけです。 これから最初に落ちるのは、GraphQLが特定の言語に結び付けられていないということです。 HaskellからC ++まですべてに20を超える実装があり、そのうちJavaScriptは1つだけです。 仕様が発表された直後に、FacebookはJavaScriptでのリファレンス実装をリリースしましたが、内部で使用しないため、GoやClojureなどの言語での実装はさらに優れているか高速です。
GraphQLの仕様はクライアントやデータに言及していません
スペックを読むと、2つ目立って欠けていることに気付くでしょう。 まず、クエリ言語以外に、クライアント統合についての言及はありません。 GraphQLの設計により、Apollo、Relay、Lokaなどのツールが可能ですが、それらは決してその一部ではなく、使用に必要なものでもありません。 第二に、特定のデータ層についての言及はありません。 同じGraphQLサーバーは、異種のソースのセットからデータをフェッチでき、頻繁にフェッチします。 Redisにキャッシュされたデータを要求し、USPS APIからアドレス検索を実行し、protobuffベースのマイクロサービスを呼び出すことができます。クライアントは違いを知ることはありません。
複雑さの段階的開示
GraphQLは、多くの人にとって、パワーとシンプルさのまれな交差点に到達しました。 それは、単純なことを単純にし、難しいことを可能にするという素晴らしい仕事をします。 サーバーを実行し、型付きデータをHTTP経由で提供することは、想像できるほぼすべての言語で数行のコードになります。
たとえば、GraphQLサーバーは既存のREST APIをラップでき、そのクライアントは、他のサービスと対話する場合と同じように、通常のGETリクエストでデータを取得できます。 ここでデモを見ることができます。 または、プロジェクトでより高度なツールセットが必要な場合は、GraphQLを使用して、フィールドレベルの認証、Pub / Subサブスクリプション、プリコンパイル/キャッシュクエリなどを実行できます。
サンプルアプリ
この例の目的は、広範なチュートリアルを作成することではなく、JavaScriptの約70行でGraphQLのパワーとシンプルさを実証することです。 構文とセマンティクスについてはあまり詳しく説明しませんが、ここにあるすべてのコードは実行可能であり、記事の最後にプロジェクトのダウンロード可能なバージョンへのリンクがあります。 これを実行した後、もう少し深く掘り下げたい場合は、ブログにリソースのコレクションがあり、より大きく、より堅牢なサービスを構築するのに役立ちます。
デモではJavaScriptを使用しますが、手順はどの言語でも非常に似ています。 素晴らしいMocky.ioを使用していくつかのサンプルデータから始めましょう。
著者
{ 9: { id: 9, name: "Eric Baer", company: "Formidable" }, ... }
投稿
[ { id: 17, author: "author/7", categories: [ "software engineering" ], publishdate: "2016/03/27 14:00", summary: "...", tags: [ "http/2", "interlock" ], title: "http/2 server push" }, ... ]
最初のステップは、 express
およびexpress-graphql
ミドルウェアを使用して新しいプロジェクトを作成することです。
bash npm init -y && npm install --save graphql express express-graphql
そして、エクスプレスサーバーでindex.js
ファイルを作成します。
const app = require("express")(); const PORT = 5000; app.listen(PORT, () => { console.log(`Server running at https://localhost:${PORT}`); });
GraphQLの使用を開始するには、RESTAPIでデータをモデル化することから始めます。 schema.js
という新しいファイルに、次を追加します。
const { GraphQLInt, GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString } = require("graphql"); const Author = new GraphQLObjectType({ name: "Author", fields: { id: { type: GraphQLInt }, name: { type: GraphQLString }, company: { type: GraphQLString }, } }); const Post = new GraphQLObjectType({ name: "Post", fields: { id: { type: GraphQLInt }, author: { type: Author }, categories: { type: new GraphQLList(GraphQLString) }, publishDate: { type: GraphQLString }, summary: { type: GraphQLString }, tags: { type: new GraphQLList(GraphQLString) }, title: { type: GraphQLString } } }); const Blog = new GraphQLObjectType({ name: "Blog", fields: { posts: { type: new GraphQLList(Post) } } }); module.exports = new GraphQLSchema({ query: Blog });
上記のコードは、APIのJSON応答の型をGraphQLの型にマップします。 GraphQLObjectType
はJavaScript Object
に対応し、 GraphQLString
はJavaScript String
に対応します。 注意すべき特別なタイプの1つは、最後の数行のGraphQLSchema
です。 GraphQLSchema
は、GraphQLのルートレベルのエクスポートです。これは、クエリがグラフをトラバースするための開始点です。 この基本的な例では、 query
を定義するだけです。 ここで、ミューテーション(書き込み)とサブスクリプションを定義します。

次に、 index.js
ファイルでエクスプレスサーバーにスキーマを追加します。 これを行うには、 express-graphql
ミドルウェアを追加し、それにスキーマを渡します。
const graphqlHttp = require("express-graphql"); const schema = require("./schema.js"); const app = require("express")(); const PORT = 5000; app.use(graphqlHttp({ schema, // Pretty Print the JSON response pretty: true, // Enable the GraphiQL dev tool graphiql: true })); app.listen(PORT, () => { console.log(`Server running at https://localhost:${PORT}`); });
この時点では、データは返されていませんが、スキーマをクライアントに提供するGraphQLサーバーが機能しています。 アプリケーションの起動を簡単にするために、 package.json
に開始スクリプトも追加します。
"scripts": { "start": "nodemon index.js" },
プロジェクトを実行してhttps:// localhost:5000 /に移動すると、GraphiQLというデータエクスプローラーが表示されます。 HTTP Accept
ヘッダーがapplication/json
に設定されていない限り、GraphiQLはデフォルトでロードされます。 application/json
を使用してfetch
またはcURL
でこれと同じURLを呼び出すと、JSONの結果が返されます。 組み込みのドキュメントを自由に試して、クエリを作成してください。

サーバーを完成させるために残された唯一のことは、基礎となるデータをスキーマに接続することです。 これを行うには、 resolve
関数を定義する必要があります。 GraphQLでは、クエリはトップダウンで実行され、ツリーをトラバースするときにresolve
関数を呼び出します。 たとえば、次のクエリの場合:
query homepage { posts { title } }
GraphQLは、最初にposts.resolve(parentData)
を呼び出し、次にposts.title.resolve(parentData)
)を呼び出します。 ブログ投稿のリストでリゾルバーを定義することから始めましょう。
const Blog = new GraphQLObjectType({ name: "Blog", fields: { posts: { type: new GraphQLList(Post), resolve: () => { return fetch('https://www.mocky.io/v2/594a3ac810000053021aa3a7') .then((response) => response.json()) } } } });
ここではisomorphic-fetch
パッケージを使用してHTTPリクエストを作成しています。これは、リゾルバーからPromiseを返す方法をうまく示しているためですが、好きなものを使用できます。 この関数は、投稿の配列をブログタイプに返します。 GraphQLのJavaScript実装のデフォルトの解決関数は、 parentData.<fieldName>
です。 たとえば、作成者の名前フィールドのデフォルトのリゾルバは次のようになります。
rawAuthorObject => rawAuthorObject.name
この単一のオーバーライドリゾルバーは、postオブジェクト全体のデータを提供する必要があります。 Authorのリゾルバーを定義する必要がありますが、ホームページに必要なデータをフェッチするクエリを実行すると、それが機能していることがわかります。

投稿APIの作成者属性は作成者IDであるため、GraphQLが名前と会社を定義するオブジェクトを検索し、文字列を見つけると、 null
が返されます。 Authorを接続するには、Postスキーマを次のように変更する必要があります。
const Post = new GraphQLObjectType({ name: "Post", fields: { id: { type: GraphQLInt }, author: { type: Author, resolve: (subTree) => { // Get the AuthorId from the post data const authorId = subTree.author.split("/")[1]; return fetch('https://www.mocky.io/v2/594a3bd21000006d021aa3ac') .then((response) => response.json()) .then(authors => authors[authorId]); } }, ... } });
これで、RESTAPIをラップする完全に機能するGraphQLサーバーができました。 完全なソースは、このGithubリンクからダウンロードするか、このGraphQLランチパッドから実行できます。
このようなGraphQLエンドポイントを使用するために使用する必要のあるツールについて疑問に思われるかもしれません。 リレーやアポロのような選択肢はたくさんありますが、まずはシンプルなアプローチが一番だと思います。 GraphiQLを何度も試してみた場合、URLが長いことに気付いたかもしれません。 このURLは、クエリのURIエンコードされたバージョンです。 JavaScriptでGraphQLクエリを作成するには、次のようにします。
const homepageQuery = ` posts { title author { name } } `; const uriEncodedQuery = encodeURIComponent(homepageQuery); fetch(`https://localhost:5000/?query=${uriEncodedQuery}`);
または、必要に応じて、次のようにGraphiQLからURLをコピーして貼り付けることができます。
https://localhost:5000/?query=query%20homepage%20%7B%0A%20%20posts%20%7B%0A%20%20%20%20title%0A%20%20%20%20author%20%7B%0A%20%20%20%20%20%20name%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D&operationName=homepage
GraphQLエンドポイントとその使用方法があるので、RESTishAPIと比較できます。 RESTish APIを使用してデータをフェッチするために作成する必要のあるコードは、次のようになります。
RESTishAPIの使用
const getPosts = () => fetch(`${API_ROOT}/posts`); const getPost = postId => fetch(`${API_ROOT}/post/${postId}`); const getAuthor = authorId => fetch(`${API_ROOT}/author/${postId}`); const getPostWithAuthor = post => { return getAuthor(post.author) .then(author => { return Object.assign({}, post, { author }) }) }; const getHomePageData = () => { return getPosts() .then(posts => { const postDetails = posts.map(getPostWithAuthor); return Promise.all(postDetails); }) };
GraphQLAPIの使用
const homepageQuery = ` posts { title author { name } } `; const uriEncodedQuery = encodeURIComponent(homepageQuery); fetch(`https://localhost:5000/?query=${uriEncodedQuery}`);
要約すると、GraphQLを使用して次のことを行いました。
- 9つのリクエスト(投稿のリスト、4つのブログ投稿、および各投稿の作成者)を減らします。
- 送信されるデータの量を大幅に減らします。
- 信じられないほどの開発者ツールを使用して、クエリを作成します。
- クライアントでよりクリーンなコードを記述します。
GraphQLの欠陥
誇大広告は正当化されると私は信じていますが、特効薬はなく、GraphQLと同じくらい素晴らしいのですが、欠陥がないわけではありません。
データの整合性
GraphQLは、優れたデータのために特別に作成されたツールのように見えることがあります。 多くの場合、異種のサービスや高度に正規化されたテーブルをつなぎ合わせて、一種のゲートウェイとして最適に機能します。 消費するサービスから返されるデータが乱雑で構造化されていない場合、GraphQLの下にデータ変換パイプラインを追加することは非常に難しい場合があります。 GraphQL解決関数のスコープは、それ自体のデータとその子のデータのみです。 オーケストレーションタスクがツリー内の兄弟または親のデータにアクセスする必要がある場合、それは特に困難な場合があります。
複雑なエラー処理
GraphQLリクエストは任意の数のクエリを実行でき、各クエリは任意の数のサービスにヒットする可能性があります。 リクエスト全体が失敗するのではなく、リクエストの一部が失敗した場合、デフォルトでは、GraphQLは部分的なデータを返します。 部分的なデータは技術的には正しい選択である可能性が高く、非常に便利で効率的です。 欠点は、エラー処理がHTTPステータスコードのチェックほど単純ではなくなったことです。 この動作はオフにすることができますが、多くの場合、クライアントはより高度なエラーケースになります。
キャッシング
多くの場合、静的GraphQLクエリを使用することをお勧めしますが、任意のクエリを許可するGithubのような組織では、VarnishやFastlyなどの標準ツールを使用したネットワークキャッシュは使用できなくなります。
高いCPUコスト
クエリの解析、検証、および型チェックはCPUにバインドされたプロセスであり、JavaScriptなどのシングルスレッド言語でパフォーマンスの問題を引き起こす可能性があります。
これは、実行時のクエリ評価でのみ問題になります。
まとめ
GraphQLの機能は革命ではありません—それらのいくつかは30年近く前から存在しています。 GraphQLを強力なものにしているのは、洗練されたレベル、統合性、使いやすさにより、GraphQLがそのパーツの合計以上のものになっていることです。
GraphQLが達成することの多くは、努力と規律をもって、RESTまたはRPCを使用して達成できますが、GraphQLは、これを実行するための時間、リソース、またはツールがない可能性のある膨大な数のプロジェクトに最先端のAPIをもたらします。 GraphQLが特効薬ではないことも事実ですが、その欠陥は軽微でよく理解されている傾向があります。 かなり複雑なGraphQLサーバーを構築した人として、私は簡単にメリットがコストを上回っていると言うことができます。
このエッセイは、GraphQLが存在する理由とそれが解決する問題にほぼ完全に焦点を当てています。 これがそのセマンティクスとその使用方法についてもっと学ぶことに興味をそそられた場合は、ブログ、YouTube、または単にソースを読むかどうかにかかわらず、自分に最適な方法で学ぶことをお勧めします(How To GraphQLは特に優れています)。
この記事を楽しんだ場合(またはそれが嫌いな場合)、フィードバックをお寄せになりたい場合は、Twitterで@ebaerbaerbaerまたはLinkedInのericjbaerで私を見つけてください。