概要
TypeScriptでApollo-Serverを構築して、resolverを実装した際のメモ
ディレクトリ構成
$ tree -I node_modules
.
├── nodemon.json
├── package-lock.json
├── package.json
├── src
│ ├── mocks
│ │ ├── message.ts
│ │ ├── room.ts
│ │ └── user.ts
│ ├── resolvers
│ │ ├── message.ts
│ │ ├── query.ts
│ │ ├── room.ts
│ │ └── user.ts
│ ├── resolvers.ts
│ ├── schema.ts
│ └── server.ts
└── tsconfig.json
準備
node.jsのインストール
- Apollo-Serverの構築にはnode.jsが必要なので、インストールする
- インストール手順は下記を参考に、nodebrewをインストールし現在の最新(v16.6.2)を利用する
必要なライブラリのインストール
- TypeScriptでApollo-Server利用するための必要最低限のライブラリのみをインストールしている(nodemonは入れる)
$ npm install -save-dev typescript nodemon ts-node ts-loader
$ npm install apollo-server graphql
- package.json の中身を確認し、scriptにコマンドを追記する
package.json
{
"name": "grapql",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"ts-node": ".\\node_modules\\.bin\\ts-node",
"start": "NODE_ENV=development nodemon --config ./nodemon.json"
},
"dependencies": {
"apollo-server": "^3.1.2",
"graphql": "^15.5.1"
},
"devDependencies": {
"nodemon": "^2.0.12",
"ts-loader": "^9.2.5",
"ts-node": "^10.2.0",
"typescript": "^4.3.5"
}
}
- nodemonを使用して、ファイル更新時に自動的にサーバ再起動を行なうように、node.jsonを作成
node.json
{
"watch": ["src"],
"ext": "*",
"exec": "ts-node ./src/server.ts"
}
- tsconfig.jsonを作成。設定はサバイバルType Scriptから拝借しています
tsconfig.json
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"lib": [
"es2020"
],
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"moduleResolution": "node",
"baseUrl": "src",
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*"
],
"exclude": [
"dist",
"node_modules"
],
"compileOnSave": false
}
Apollo-Serverの構築
- TypeScriptを利用してApollo-Serverを構築して行く準備ができたので、起動のための設定を行なう
- Apollo-Server起動用ファイルと、Graphqlのスキーマ定義を行うだけで起動ができる
スキーマ定義
- 簡単なチャットサービスを想定したスキーマ定義を行った。今回使用するのはQueryのみ作成を行ってみた
schema.ts
import { gql } from 'apollo-server';
export const typeDefs = gql`
type Query {
getRooms(id: ID): [Room]
getUsers(id: ID): [User]
getMessages(id: ID): [Message]
}
type Room {
id: ID!
name: String
messages: [Message]
}
type Message {
id: ID!
body: String
user: User
room: Room
}
type User {
id: ID!
name: String
messages: [Message]
}
`;
サーバ起動用の設定ファイル
- 必要最低限の設定だけを記述した、サーバ起動用のファイルを作成
server.ts
import { ApolloServer } from 'apollo-server';
import { typeDefs } from './schema';
const server = new ApolloServer({
typeDefs
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
export default server;
起動
- package.jsonに記述したscriptをして、サーバを起動する
$ npm run start
> grapql@1.0.0 start
> NODE_ENV=development nodemon --config ./nodemon.json
[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src/**/*
[nodemon] watching extensions: (all)
[nodemon] starting `ts-node ./src/server.ts`
🚀 Server ready at http://localhost:4000/
- ブラウザで
http://localhost:4000/
にアクセスすると、Apollo Studioが表示される。Apollo-Server 3 以前では、playgroudが起動していたが、変わったらしい
Resolverを実装してデータを取得する
- ルートクエリに対するresolverを実装して、Apollo Studioを使用してデータを取得してみる
- ここではDBへの接続等は行わず、jsonで作成したmockデータを利用する
Resolversの実装
- 全てのリゾルバーを一箇所にまとめるためのファイルを作成
resolvers.ts
import { query } from './resolvers/query';
export const resolvers = {
Query: query
};
- rootクエリーに対するResolverを実装する
- contextを経由してmockデータの中身を検索している
- 動かしてみることを目的にしているのでanyをたくさん使ってます・・・。user、room、message等の型を作製するのが良さそう
query.ts
export const query = {
getUsers: (parent: any, args: any, context: any) => {
let result = context.users.find((v: any) => v.id === args.id);
return [result];
},
getRooms: (parent: any, args: any, context: any) => {
let result = context.rooms.find((v: any) => v.id === args.id);
return [result];
},
getMessages: (parent: any, args: any, context: any) => {
let result = context.messages.find((v: any) => v.id === args.id);
return [result];
}
};
mockデータの作製
- 各スキーマもmockデータを作成。DBのテーブルのレコードを想定しています。
users.ts
export const users = [
{
id: '1',
name: 'user1'
},
{
id: '2',
name: 'user2'
},
{
id: '3',
name: 'user3'
},
{
id: '4',
name: 'user4'
}
];
rooms.ts
export const messages = [
{
id: '1',
body: 'hello1',
user: '1',
room: '1'
},
{
id: '2',
body: 'hello2',
user: '2',
room: '1'
},
{
id: '3',
body: 'hello3',
user: '3',
room: '2'
},
{
id: '4',
body: 'hello4',
user: '4',
room: '2'
}
];
messages.ts
export const rooms = [
{
id: '1',
name: 'room1'
},
{
id: '2',
name: 'room2'
},
{
id: '3',
name: 'room3'
},
{
id: '4',
name: 'room4'
}
];
Apollo-Server構築
- server.tsに対して、作成したResolverと、mockデータを利用す流ように追記
- contextを利用して、mockデータをresolver全体で扱えるように設定している
server.ts
import { ApolloServer } from 'apollo-server';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';
import { users } from './mocks/user';
import { messages } from './mocks/message';
import { rooms } from './mocks/room';
const server = new ApolloServer({
typeDefs,
resolvers,
context: () => {
return {
users: users,
messages: messages,
rooms: rooms
};
}
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
export default server;
クエリ実行
- Apollo Studio上で、下記のクエリーを実行してデータの取得を行えることが確認できる
query sample {
getUsers(id: "1") {
id
name
}
getMessages(id: "1") {
id
body
}
getRooms(id: "1") {
id
name
}
}
- クエリーの実行結果
{
"data": {
"getUsers": [
{
"id": "1",
"name": "user1"
}
],
"getMessages": [
{
"id": "1",
"body": "hello1"
}
],
"getRooms": [
{
"id": "1",
"name": "room1"
}
]
}
}
子ノードのResolverを実装する
- GraphQLがグラフ構造を持つことで得られる一番のメリットだとおもう
- 親ノードのデータ取得結果を利用して、子ノードデータを取得できる
- Resolverは各ノード(各スキーマ)のことだけを考えて実装するのが理想的
Resolverを実装
- rootクエリ以外のResolverを実装する
resolvers.ts
import { query } from './resolvers/query';import { query } from './resolvers/query';
import { user } from './resolvers/user';
import { message } from './resolvers/message';
import { room } from './resolvers/room';
export const resolvers = {
Query: query,
User: user,
Message: message,
Room: room
};
Resolverの定義
- 引数であるparentを利用して、親ノードのデータ取得結果を利用して、子ノードのデータを取得できるできるように実装を行っていく
resolvers/user.ts
export const user = {
messages: (parent: any, args: any, context: any) => {
let result = context.messages.filter((message: any) => {
return message.user === parent.id;
});
return result;
}
};
resolvers/message.ts
export const message = {
user: (parent: any, args: any, context: any) => {
let result = context.users.find((user: any) => {
return user.id === parent.user;
});
return result;
},
room: (parent: any, args: any, context: any) => {
let result = context.rooms.find((room: any) => {
return room.id === parent.room;
});
return result;
}
};
resolvers/room.ts
export const room = {
messages: (parent: any, args: any, context: any) => {
let result = context.messages.filter((message: any) => {
return message.room === parent.id;
});
return result;
}
};
クエリの実行
- rootクエリであるgetMessagesのデータ取得結果を利用して、子ノードであるuser、roomのデータを取得できる
query sample {
getMessages(id: "4") {
id
body
user {
id
name
}
room {
id
name
}
}
}
- クエリの実行結果
{
"data": {
"getMessages": [
{
"id": "4",
"body": "hello4",
"user": {
"id": "4",
"name": "user4"
},
"room": {
"id": "2",
"name": "room2"
}
}
]
}
}
感想
各ノード(スキーマ)に対するResolverを定義するというのが興味深かった。サーバの監視方法やセキュリティ等は REST API の時とだいぶ異なるようなので、また色々試してみる