この記事は、リレーブログ企画「24新卒リレーブログ」の記事です。
はじめに
こんにちは。初めまして、新卒1年目の塚崎です。
現在、ジョブローテの1期目として、第一開発チーム(https://engineering.nifty.co.jp/blog/26940)でとあるサイトをリニューアルするプロジェクトを進めています。このリニューアルではフロントエンドでNext.jsを採用し、バックエンドではGraphQLを採用しています。今回のリニューアルで私は主にフロントエンドの実装を担当しているのですが、GraphQLについても学んだので、今回はGraphQLを使ったAPIサーバーの実装について記事にしたいと思います。
GraphQLとは?
GraphQL(https://graphql.org/)とは、Meta社によって開発されたWeb APIのクエリ言語です。GraphQLでは、クライアントが必要なデータだけを指定し、サーバーから取得することができるため、REST APIの課題であったデータの過剰取得を防ぎ、効率良くデータの取得が行えます。また、REST APIでは複数のエンドポイントからデータの取得を行うのに対して、GraphQLでは単一のエンドポイントから一度のリクエストで全てのデータを取得することができます。他にもスキーマと呼ばれる型やクエリを定義する仕組みによって、安全に開発ができることも大きなメリットです。
特徴
- 柔軟なデータ取得
- クライアント側で必要なデータだけを指定して取得可能
- 単一のエンドポイント
- 単一のエンドポイントで全てのデータ操作が可能
- 強力な型システム
- スキーマを定義することで型の安全性が保証される
- 階層的な構造
- データの関係性をクエリの構造に反映できる
- リアルタイム機能
- サブスクリプション機能を利用し、リアルタイムにデータを取得できる
- バージョン管理が不要
- 新しいフィールドの追加を容易に行うことができ、バージョン管理が不要
GraphQLサーバーの実装
実際にGraphQLサーバーを実装し、データの取得までをやってみます。
まずは任意の場所でGraphQL用のディレクトリ(graphql-server-example
)を作成します。
1 2 |
mkdir graphql-server-example cd graphql-server-example |
npmで初期化します。
1 |
npm init --yes && npm pkg set type="module" |
依存ライブラリである@apollo/serverとgraphqlをインストールします。
1 |
npm install @apollo/server graphql |
package.json
を開き、scripts
に"start": "node index.js"
を追加します。
スキーマの作成
今回は例としてアーティスト情報を返すAPIサーバーを構築してみます。
まずindex.js
を作成し、以下のようにスキーマ(APIの型やクエリを定義するもの)を書きます。typeDefs
はスキーマを定義する変数です。VSCodeでは以下の拡張機能を追加し、テンプレートリテラルの開始に#graphql
と書くことでシンタックスハイライトが機能するようになります。
今回はID、アーティスト名、ジャンルの3つをアーティストオブジェクトとして定義しました。またクエリとして全てのアーティストオブジェクトを取得するクエリも定義しています。
拡張機能(GraphQL: Syntax Highlighting)
https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql-syntax
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; const typeDefs = `#graphql type Artist { id: String name: String genres: [String] } type Query { allArtists: [Artist] } `; |
データの作成
次にサーバーから返すデータを作成します。
通常、クライアントに返すデータはデータベースと接続し、そこから取得を行いますが、今回はデータを配列でハードコーディングすることで擬似的に用意します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
const artists = [ { id: "2c431017-0a62-46de-805c-a2e4c401264f", name: "Bring Me The Horizon", genres: ["Metalcore", "Alternative Metal"], }, { id: "f575282f-aa8a-4636-8258-3ce2279871a6", name: "In Flames", genres: ["Alternative Metal"], }, { id: "2400debb-45aa-4467-991f-64063e7753aa", name: "Dream Theater", genres: ["Progressive Metal"], }, ]; |
リゾルバの作成
ここまででスキーマを定義し、データも用意することができました。あとはサーバーからフロントへデータを返す処理を追加すれば良さそうです。
データを返すリゾルバを定義します。リゾルバとは、データベースなどからデータを取得し、スキーマで定義された型に合わせてデータを返す処理を担当します。
今回、スキーマで定義したallArtists
は全てのアーティストオブジェクトを返すクエリのため、用意した配列のデータをそのまま返す処理を追加しています。
実際にはデータベースからデータを取得した際に、それがそのまま返せるケースは少ないので、リゾルバでスキーマと合うように整形してあげる必要があります。
1 2 3 4 5 |
const resolvers = { Query: { allArtists: () => artists, }, }; |
サーバーの起動
最後にApollo Serverを初期化する処理を追加してあげましょう。
1 2 3 4 5 6 7 8 9 10 |
const server = new ApolloServer({ typeDefs, resolvers, }); const { url } = await startStandaloneServer(server, { listen: { port: 4000 }, }); console.log(`Server ready at: ${url}`); |
サーバーを起動します。
npm start
サーバーを起動するとターミナルに以下が出力されていると思うので、リンクをブラウザで開きます。
Server ready at: http://localhost:4000/
リンクを開くと以下のような画面に遷移します。
この画面ではクエリを実行したり、クエリのレスポンス結果を確認したりすることができます。
それでは実際にクエリを実行してみましょう。
実行結果です。クエリで指定したフィールドの値を取得することができました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
{ "data": { "allArtists": [ { "id": "2c431017-0a62-46de-805c-a2e4c401264f", "name": "Bring Me The Horizon", "genres": [ "Metalcore", "Alternative Metal" ] }, { "id": "f575282f-aa8a-4636-8258-3ce2279871a6", "name": "In Flames", "genres": [ "Alternative Metal" ] }, { "id": "2400debb-45aa-4467-991f-64063e7753aa", "name": "Dream Theater", "genres": [ "Progressive Metal" ] } ] } } |
フィールドの追加
次にアーティスト数を取得するフィールドを追加してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const typeDefs = `#graphql type Artist { id: String name: String genres: [String] } type Query { allArtists: [Artist], # 以下を追加 totalArtists: Int } `; |
フィールド(totalArtists
)を追加したので、それに対応するリゾルバを作成する必要があります。今回は、アーティスト数を返すリゾルバとしたので、単純にアーティストの配列の長さを返します。
1 2 3 4 5 6 7 |
const resolvers = { Query: { allArtists: () => artists, // 以下を追加 totalArtists: () => artists.length, }, }; |
サーバーを再起動し、変更を反映させます。
リンクを開き画面の左側を見ると、フィールドとしてtotalArtists
が追加されていることが分かります。
クエリを実行してみましょう。
実行結果です。全てのアーティスト情報とアーティスト数を取得することができました。このようにクエリには複数のフィールドを追加して、実行することもできます。こういったケースでは、REST APIでは2つのエンドポイントに対してリクエストを送信する必要がありますが、GraphQLでは1回のリクエストで複数のデータを取得することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
{ "data": { "allArtists": [ { "id": "2c431017-0a62-46de-805c-a2e4c401264f", "name": "Bring Me The Horizon", "genres": [ "Metalcore", "Alternative Metal" ] }, { "id": "f575282f-aa8a-4636-8258-3ce2279871a6", "name": "In Flames", "genres": [ "Alternative Metal" ] }, { "id": "2400debb-45aa-4467-991f-64063e7753aa", "name": "Dream Theater", "genres": [ "Progressive Metal" ] } ], "totalArtists": 3 } } |
最後に各アーティストがリリースしたアルバム情報を取得できるように実装を追加してみます。
アルバムオブジェクトをスキーマとして定義します。またアーティストオブジェクトにリリースしたアルバム情報をリストで返すフィールド(releasedAlbums
)も追加してあげましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const typeDefs = `#graphql type Artist { id: String name: String genres: [String] # 追加 releasedAlbums: [Album] } # 追加 type Album { artistId: String name: String } type Query { allArtists: [Artist], totalArtists: Int } `; |
アルバム情報のデータを用意します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
const albums = [ { artistId: "2c431017-0a62-46de-805c-a2e4c401264f", name: "Post Human: Nex Gen", }, { artistId: "2c431017-0a62-46de-805c-a2e4c401264f", name: "That's the Spirit", }, { artistId: "2c431017-0a62-46de-805c-a2e4c401264f", name: "Sempiternal", }, { artistId: "f575282f-aa8a-4636-8258-3ce2279871a6", name: "Foregone", }, { artistId: "f575282f-aa8a-4636-8258-3ce2279871a6", name: "Whoracle", }, { artistId: "2400debb-45aa-4467-991f-64063e7753aa", name: "Images and Words", }, ]; |
リゾルバを追加します。配列のfilter
メソッドを使用することで、アーティストがリリースしたアルバム情報をリストで取得しています。
1 2 3 4 5 6 7 8 9 10 11 12 |
const resolvers = { Query: { allArtists: () => artists, totalArtists: () => artists.length, }, // 以下を追加 Artist: { releasedAlbums: (artist) => { return albums.filter((album) => album.artistId === artist.id); }, }, }; |
サーバーを再起動し、変更を反映させます。
クエリを実行してみます。
以下のようにデータを取得することができたと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
{ "data": { "allArtists": [ { "id": "2c431017-0a62-46de-805c-a2e4c401264f", "name": "Bring Me The Horizon", "genres": [ "Metalcore", "Alternative Metal" ], "releasedAlbums": [ { "name": "Post Human: Nex Gen" }, { "name": "That's the Spirit" }, { "name": "Sempiternal" } ] }, { "id": "f575282f-aa8a-4636-8258-3ce2279871a6", "name": "In Flames", "genres": [ "Alternative Metal" ], "releasedAlbums": [ { "name": "Foregone" }, { "name": "Whoracle" } ] }, { "id": "2400debb-45aa-4467-991f-64063e7753aa", "name": "Dream Theater", "genres": [ "Progressive Metal" ], "releasedAlbums": [ { "name": "Images and Words" } ] } ], "totalArtists": 3 } } |
まとめ
今回は、とあるプロジェクトのリニューアルでGraphQLについて学び、APIサーバーの実装をやってみました。実際にプロジェクトに導入し、実装を進める中で必要なデータだけを簡単に取得できる点や事前にスキーマを固めることでバックエンドの開発を待たずにフロントエンドの実装が進められる点がメリットとして実感できました。しかし、今回学んだ内容はGraphQLの基礎的な内容になるため、キャッシュやスキーマファーストといったパフォーマンスや設計に関する部分もこれから学んでいく必要があると感じました。ニフティでは、書籍購入費用補助制度やUdemyなども使えるので、それらを活用してこれからも勉強していきたいです。
次回は、けにさんです。どんな記事になるか楽しみですね!
参考資料
- 初めてのGraphQL ― Webサービスを作って学ぶ新世代API
- Introduction to GraphQL | GraphQL(https://graphql.org/learn/)
- Introduction to Apollo Server | Apollo GraphQL Docs(https://www.apollographql.com/docs/apollo-server/)