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、およびsqlite3v4.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で入手できます。