ApolloServerとSequelizeを使用してNode.jsでGraphQLサーバーをセットアップする方法
はじめに
GraphQLは仕様であるため、言語に依存しません。 Node.jsを使用したGraphQL開発では、graphql-js 、 express-graphql 、apollo-serverなどのさまざまなオプションを利用できます。 このチュートリアルでは、ApolloServerを使用してNode.jsでフル機能のGraphQLサーバーをセットアップします。
Apollo Server 2のリリース以来、Apollo Serverを使用したGraphQLサーバーの作成は、それに付属する他の機能は言うまでもなく、より効率的になりました。
このデモンストレーションの目的で、レシピアプリ用のGraphQLサーバーを構築します。
前提条件
このチュートリアルを完了するには、次のものが必要です。
- Node.jsのローカル開発環境。 Node.jsをインストールしてローカル開発環境を作成する方法に従ってください。
- GraphQLの基本的な知識。
このチュートリアルは、ノードv14.4.0、npm
v6.14.5、apollo-server
v2.15.0、graphql
v15.1.0、sequelize
v5.21.13、およびsqlite3
v4.2.0。
GraphQLとは何ですか?
GraphQLは、APIの宣言型データフェッチ仕様およびクエリ言語です。 Facebookによって作成されました。 GraphQLは、アンダーフェッチやオーバーフェッチなど、RESTのいくつかの欠点を克服するために作成されたため、RESTの効果的な代替手段です。
RESTとは異なり、GraphQLは1つのエンドポイントを使用します。 これは、エンドポイントに対して1つのリクエストを行い、1つの応答をJSONとして受け取ることを意味します。 このJSON応答には、必要な数のデータを含めることができます。 GraphQLはプロトコルに依存しませんが、RESTと同様に、GraphQLはHTTP経由で操作できます。
典型的なGraphQLサーバーは、スキーマとリゾルバーで構成されています。 スキーマ(またはGraphQLスキーマ)には、GraphQLAPIを構成する型定義が含まれています。 タイプ定義にはフィールドが含まれ、各フィールドには返されると予想されるものが含まれています。 各フィールドは、リゾルバーと呼ばれるGraphQLサーバー上の関数にマップされます。 リゾルバーには、実装ロジックとフィールドの戻りデータが含まれています。 つまり、スキーマには型定義が含まれ、リゾルバーには実際の実装が含まれます。
ステップ1—データベースのセットアップ
まず、データベースを設定します。 データベースにはSQLiteを使用します。 また、Node.jsのORMである Sequelize を使用して、データベースと対話します。
まず、新しいプロジェクトを作成しましょう。
mkdir graphql-recipe-server
新しいプロジェクトディレクトリに移動します。
cd graphql-recipe-server
新しいプロジェクトを初期化します。
npm init -y
次に、Sequelizeをインストールしましょう。
npm install sequelize sequelize-cli sqlite3
Sequelizeのインストールに加えて、Node.js用のsqlite3
パッケージもインストールしています。 プロジェクトの足場を作るために、同様にインストールしているSequelizeCLIを使用します。
CLIを使用してプロジェクトの足場を作りましょう。
node_modules/.bin/sequelize init
これにより、次のフォルダが作成されます。
config
:データベースへの接続方法をSequelizeに指示する構成ファイルが含まれています。models
:プロジェクトのすべてのモデルが含まれ、すべてのモデルを統合するindex.js
ファイルも含まれています。migrations
:すべての移行ファイルが含まれています。seeders
:すべてのシードファイルが含まれます。
このチュートリアルでは、シーダーは使用しません。 config/config.json
を開き、次のコンテンツに置き換えます。
config / config.json
{ "development": { "dialect": "sqlite", "storage": "./database.sqlite" } }
dialect
をsqlite
に設定し、storage
がSQLiteデータベースファイルを指すように設定します。
次に、プロジェクトのルートディレクトリ内にデータベースファイルを直接作成する必要があります。
touch database.sqlite
これで、SQLiteを使用するためのプロジェクトの依存関係がインストールされました。
ステップ2—モデルと移行の作成
データベースのセットアップが邪魔にならないので、プロジェクトのモデルの作成を開始できます。 レシピアプリには、User
とRecipe
の2つのモデルがあります。 これには、SequelizeCLIを使用します。
node_modules/.bin/sequelize model:create --name User --attributes name:string,email:string,password:string
これにより、models
ディレクトリ内にuser.js
ファイルが作成され、migrations
ディレクトリ内に対応する移行ファイルが作成されます。
User
モデルのフィールドをnull許容にしたくないので、それを明示的に定義する必要があります。 migrations/XXXXXXXXXXXXXX-create-user.js
を開き、フィールド定義を次のように更新します。
移行/XXXXXXXXXXXXXX-create-user.js
name: { allowNull: false, type: Sequelize.STRING }, email: { allowNull: false, type: Sequelize.STRING }, password: { allowNull: false, type: Sequelize.STRING }
次に、User
モデルでも同じことを行います。
models / user.js
name: { allowNull: false, type: DataTypes.STRING }, email: { allowNull: false, type: DataTypes.STRING }, password: { allowNull: false, type: DataTypes.STRING }
次に、Recipe
モデルを作成しましょう。
node_modules/.bin/sequelize model:create --name Recipe --attributes title:string,ingredients:string,direction:string
User
モデルで行ったのと同じように、Recipe
モデルでも同じことを行います。 migrations/XXXXXXXXXXXXXX-create-recipe.js
を開き、フィールド定義を次のように更新します。
移行/XXXXXXXXXXXXXX-create-recipe.js
userId: { allowNull: false, type: Sequelize.INTEGER.UNSIGNED }, title: { allowNull: false, type: Sequelize.STRING }, ingredients: { allowNull: false, type: Sequelize.STRING }, direction: { allowNull: false, type: Sequelize.STRING },
追加のフィールドuserId
があることに気付くでしょう。これは、レシピを作成したユーザーのIDを保持します。 これについてはまもなく詳しく説明します。
Recipe
モデルも更新します。
models / recipe.js
title: { allowNull: false, type: DataTypes.STRING }, ingredients: { allowNull: false, type: DataTypes.STRING }, direction: { allowNull: false, type: DataTypes.STRING }
ユーザーモデルとレシピモデルの間の1対多の関係を定義しましょう。
models/user.js
を開き、User.associate
機能を次のように更新します。
models / user.js
User.associate = function(models) { // associations can be defined here User.hasMany(models.Recipe) };
Recipe
モデルの関係の逆も定義する必要があります。
models / recipe.js
Recipe.associate = function(models) { // associations can be defined here Recipe.belongsTo(models.User, { foreignKey: 'userId' }); };
デフォルトでは、Sequelizeは、対応するモデル名のキャメルケース名とその主キーを外部キーとして使用します。 したがって、この場合、外部キーはUserId
であると想定されます。 列に別の名前を付けたため、関連付けでforeignKey
を明示的に定義する必要があります。
これで、移行を実行できます。
node_modules/.bin/sequelize db:migrate
これで、モデルと移行のセットアップが完了しました。
ステップ3—GraphQLサーバーを作成する
前述のように、GraphQLサーバーの構築にはApolloサーバーを使用します。 それで、それをインストールしましょう:
npm install apollo-server graphql bcryptjs
Apolloサーバーは依存関係としてgraphql
を必要とするため、それもインストールする必要があります。 また、bcryptjs
をインストールします。これは、後でユーザーパスワードをハッシュするために使用します。
それらをインストールしたら、src
ディレクトリを作成し、その中にindex.js
ファイルを作成して、次のコードを追加します。
src / index.js
const { ApolloServer } = require('apollo-server'); const typeDefs = require('./schema'); const resolvers = require('./resolvers'); const models = require('../models'); const server = new ApolloServer({ typeDefs, resolvers, context: { models }, }); server .listen() .then(({ url }) => console.log('Server is running on localhost:4000'));
ここでは、Apollo Serverの新しいインスタンスを作成し、それにスキーマとリゾルバーを渡します(どちらもまもなく作成します)。 また、モデルをコンテキストとしてApolloサーバーに渡します。 これにより、リゾルバーからモデルにアクセスできるようになります。
最後に、サーバーを起動します。
ステップ4—GraphQLスキーマを定義する
GraphQLスキーマは、GraphQLAPIが持つ機能を定義するために使用されます。 GraphQLスキーマはタイプで構成されています。 タイプは、ドメイン固有のエンティティの構造を定義するためのものにすることができます。 ドメイン固有のエンティティの型を定義することに加えて、GraphQL操作の型を定義することもできます。これは、GraphQLAPIが持つ機能に変換されます。 これらの操作は、クエリ、ミューテーション、およびサブスクリプションです。 クエリは、GraphQLサーバーで読み取り操作(データのフェッチ)を実行するために使用されます。 一方、ミューテーションは、GraphQLサーバーで書き込み操作(データの挿入、更新、または削除)を実行するために使用されます。 サブスクリプションは、GraphQLサーバーにリアルタイム機能を追加するために使用されるため、これら2つとは完全に異なります。
このチュートリアルでは、クエリとミューテーションのみに焦点を当てます。
GraphQLスキーマとは何かを理解したので、アプリのスキーマを作成しましょう。 src
ディレクトリ内に、schema.js
ファイルを作成し、次のコードを追加します。
src / schema.js
const { gql } = require('apollo-server'); const typeDefs = gql` type User { id: Int! name: String! email: String! recipes: [Recipe!]! } type Recipe { id: Int! title: String! ingredients: String! direction: String! user: User! } type Query { user(id: Int!): User allRecipes: [Recipe!]! recipe(id: Int!): Recipe } type Mutation { createUser(name: String!, email: String!, password: String!): User! createRecipe( userId: Int! title: String! ingredients: String! direction: String! ): Recipe! } `; module.exports = typeDefs;
まず、apollo-server
のgql
パッケージをrequire
します。 次に、それを使用してスキーマを定義します。 理想的には、GraphQLスキーマがデータベーススキーマを可能な限りミラーリングすることを望んでいます。 したがって、モデルに対応するUser
とRecipe
の2つのタイプを定義します。 User
タイプでは、User
モデルのフィールドを定義するだけでなく、ユーザーのレシピを取得するために使用されるrecipes
フィールドも定義します。 Recipe
タイプと同じです。 user
フィールドを定義します。このフィールドは、レシピのユーザーを取得するために使用されます。
次に、3つのクエリを定義します。単一のユーザーをフェッチするため、作成されたすべてのレシピをフェッチするため、および単一のレシピをフェッチするためです。 user
クエリとrecipe
クエリはどちらも、それぞれユーザーまたはレシピを返すか、IDに対応する一致が見つからなかった場合はnull
を返すことができます。 allRecipes
クエリは常にレシピの配列を返します。レシピがまだ作成されていない場合は、空になる可能性があります。
注: !
はフィールドが必須であることを示し、[]
はフィールドがアイテムの配列を返すことを示します。
最後に、新しいユーザーを作成するためのミューテーションと、新しいレシピを作成するためのミューテーションを定義します。 両方のミューテーションは、作成されたユーザーとレシピをそれぞれ返します。
ステップ5—リゾルバーを作成する
リゾルバーは、スキーマ内のフィールドの実行方法を定義します。 言い換えれば、私たちのスキーマはリゾルバーなしでは役に立たないのです。 src
ディレクトリ内にresolvers.js
ファイルを作成し、その中に次のコードを追加します。
src / resolvers.js
const resolvers = { Query: { async user(root, { id }, { models }) { return models.User.findById(id); }, async allRecipes(root, args, { models }) { return models.Recipe.findAll(); }, async recipe(root, { id }, { models }) { return models.Recipe.findById(id); }, }, }; module.exports = resolvers;
注:最新バージョンのsequelize
は、findById
を廃止し、findByPk
に置き換えました。 models.Recipe.findById is not a function
やmodels.User.findById is not a function
などのエラーが発生した場合は、このスニペットを更新する必要があります。
まず、クエリのリゾルバーを作成します。 ここでは、モデルを使用してデータベースに対して必要なクエリを実行し、結果を返します。
まだsrc/resolvers.js
内で、ファイルの先頭にあるbcryptjs
をインポートしましょう。
src / resolvers.js
const bcrypt = require('bcryptjs');
次に、Query
オブジェクトの直後に次のコードを追加します。
src / resolvers.js
Mutation: { async createUser(root, { name, email, password }, { models }) { return models.User.create({ name, email, password: await bcrypt.hash(password, 10), }); }, async createRecipe( root, { userId, title, ingredients, direction }, { models } ) { return models.Recipe.create({ userId, title, ingredients, direction }); }, },
createUser
ミューテーションは、ユーザーの名前、電子メール、およびパスワードを受け入れ、提供された詳細を使用してデータベースに新しいレコードを作成します。 パスワードをデータベースに永続化する前に、必ずbcrypt
パッケージを使用してパスワードをハッシュしてください。 新しく作成されたユーザーを返します。 createRecipe
ミューテーションは、レシピを作成しているユーザーのIDとレシピ自体の詳細を受け入れ、それらをデータベースに保持して、新しく作成されたレシピを返します。
リゾルバーで締めくくるために、カスタムフィールド(User
のrecipes
およびRecipe
のuser
)をどのように解決するかを定義しましょう。 Mutation
オブジェクトの直後のsrc/resolvers.js
内に次のコードを追加します。
src / resolvers.js
User: { async recipes(user) { return user.getRecipes(); }, }, Recipe: { async user(recipe) { return recipe.getUser(); }, },
これらは、getRecipes()
およびgetUser()
のメソッドを使用します。これらのメソッドは、定義した関係により、Sequelizeによってモデルで使用可能になります。
ステップ6—GraphQLサーバーをテストする
GraphQLサーバーをテストする時が来ました。 まず、サーバーを次のコマンドで起動する必要があります。
node src/index.js
これはlocalhost:4000
で実行され、アクセスするとGraphQLPlaygroundが実行されます。
新しいユーザーを作成してみましょう。
# create a new user mutation{ createUser( name: "John Doe", email: "[email protected]", password: "password" ) { id, name, email } }
これにより、次の結果が生成されます。
Output{ "data": { "createUser": { "id": 1, "name": "John Doe", "email": "[email protected]" } } }
新しいレシピを作成して、作成したユーザーに関連付けてみましょう。
# create a new recipe mutation { createRecipe( userId: 1 title: "Salty and Peppery" ingredients: "Salt, Pepper" direction: "Add salt, Add pepper" ) { id title ingredients direction user { id name email } } }
これにより、次の結果が生成されます。
Output{ "data": { "createRecipe": { "id": 1, "title": "Salty and Peppery", "ingredients": "Salt, Pepper", "direction": "Add salt, Add pepper", "user": { "id": 1, "name": "John Doe", "email": "[email protected]" } } } }
ここで実行できるその他のクエリには、user(id: 1)
、recipe(id: 1)
、およびallRecipes
があります。
結論
このチュートリアルでは、ApolloServerを使用してNode.jsでGraphQLサーバーを作成する方法を確認しました。 また、Sequelizeを使用してデータベースをGraphQLサーバーと統合する方法も確認しました。
このチュートリアルのコードは、GitHubで入手できます。