8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TypeScriptでApollo-Serverを構築する

Last updated at Posted at 2021-08-14

概要

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のインストール

必要なライブラリのインストール

  • 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
{
  "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が起動していたが、変わったらしい

ab43fbf8-5143-4bb5-8953-582fa9ea916e.png

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 の時とだいぶ異なるようなので、また色々試してみる

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?