NextのサーバとGraphQLのサーバを立てて、気持ちよくTypescriptでGraphQLの開発の練習をする

# 概要
何をやるか

0. モノレポ？みたいなYarnのWorkspaceを立てる
    * [oao](https://github.com/guigrpa/oao)を使って、配下のプロジェクト(`frontend`と`backend`)の`dev`スクリプトを一度に走らせることができるようにする
    * [graphql-codegen](https://graphql-code-generator.com)で自動で型の定義ファイルを`frontend`に吐くようにする

1. バックエンド編
    * [graphql-yoga](https://github.com/prisma/graphql-yoga)でGraphQLサーバーを立てる
    * [type-graphql](https://typegraphql.ml)でGraphQLのリゾルバを書く
    * コードファーストで、`schema.graphql`はサーバー起動時に吐くようにする
    * [ts-node-dev](https://github.com/whitecolor/ts-node-dev)で、コードの更新があればバックエンドサーバーを再起動させる

2. フロントエンド編
    * [Next.js](https://github.com/zeit/next.js)と[mobx](https://mobx.js.org)で気持ちよく書く
    * 投げるクエリを書いて、戻り値の型は`graphql-codegen`に自動生成させる

### 今日作るやつ

![画面収録 2019-07-07 1.48.08-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/54836677-eefe-28b4-f63b-267ba0c0e9c7.gif)


### できたやつ
https://github.com/NanimonoDemonai/greaphqltest

### ファイル構成

```
.
├── backend
│   ├── package.json
│   ├── index.ts
│   ├── resolvers
│   │   ├── AisatsuResolver.ts
│   │   └── CrabResolver.ts
│   ├── tsconfig.json
│   └── types
│       └── Crab.ts
├── frontend
│   ├── package.json
│   ├── lib
│   │   └── client.ts
│   ├── pages
│   │   └── index.tsx
│   ├── queries
│   │   ├── hello.ts
│   │   ├── kani.ts
│   │   └── types.ts
│   ├── store
│   │   └── pageController.ts
│   ├── .babelrc
│   └── tsconfig.json
├── package.json
├── codegen.yml
├── schema.graphql
├── tsconfig.json
└── yarn.lock

```

# ルートプロジェクトの作成

## yarn workspacesを設定する

`yarn init`を行い、`package.json`に以下を記載すれば`workspace`となり、この配下で作った`npm`プロジェクトで`yarn install`すればルートプロジェクトの`node_modules`に引き上げられる。

```json:package.json
{
  "private": true,
  "workspaces": [
    "frontend",
    "backend"
  ]
}
```

## ルートに`oao`をインストールする


ワークスペース上で`yarn add`するには`-W`オプションが必要になる。以下で`oao`がインストールされる。

```terminal
yarn add -D oao -W
```

`oao`を使うと配下プロジェクトのスクリプトを一気に走らせることができる。またこの際に`--parallel`をつけると並列で一気に走る。サーバーのように`exit`返さない奴を同時に走らせるときに限らずに早い。直列にする必要がなければ`--parallel`つけておけばいいんじゃないかな。知らんけど。


```json:package.json
"scripts": {
"dev": "oao run-script dev --parallel",
}
```

### 配下プロジェクトの`node_module`を全部消す

以下のコマンドで一気に消せる。

```terminal:terminal
yarn oao clean
```

### 配下プロジェクトの`package.json`を見て`node_module`をインストールする

`yarn`の`workspaces`を設定しているなら、ルートで`yarn`すれば入る。

```terminal:terminal
yarn
```

## ルートに`tsconfig.json`の雛形を書く

Next.jsの[GitHub](https://github.com/zeit/next.js#typescript)に書かれている`tsconfig.json`を引っ張ってきた。`target`などは配下のプロジェクトでこれを`extends`して設定する。

```js:tsconfig.json
{
  "compilerOptions": {
    "allowJs": true /* Allow JavaScript files to be type checked. */,
    "alwaysStrict": true /* Parse in strict mode. */,
    "esModuleInterop": true /* matches compilation setting */,
    "isolatedModules": true /* to match webpack loader */,
    "noEmit": true /* Do not emit outputs. Makes sure tsc only does type checking. */,

    /* Strict Type-Checking Options, optional, but recommended. */
    "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
    "noUnusedLocals": true /* Report errors on unused locals. */,
    "noUnusedParameters": true /* Report errors on unused parameters. */,
    "strict": true /* Enable all strict type-checking options. */
  }
}
```

# バックエンド編

バックエンドのプロジェクトを作る。`oao`は[lerna](https://github.com/lerna/lerna)のように、サブプロジェクト作るときに注意するべきことは特になさそうなので、普通に`mkdir`なりして、そこで`yarn`を叩けば良い。

```terminal:terminal
mkdir backend
cd backend
```

## 必要なパッケージを入れる

```terminal:terminal
yarn add graphql graphql-yoga type-graphql @types/graphql reflect-metadata uuid
yarn add -D @types/uuid ts-node-dev typescript
```

`dev`ではない依存関係に`@types/graphql`が入っていて気持ち悪いが、これでオッケー。

執筆当時の`backend/package.json`は以下のようになっていた。

```Js:backend/package.json
{
  "dependencies": {
    "@types/graphql": "^14.2.2",
    "graphql": "^14.4.2",
    "graphql-yoga": "^1.18.0",
    "reflect-metadata": "^0.1.13",
    "type-graphql": "^0.17.4",
    "uuid": "^3.3.2"
  },
  "devDependencies": {
    "@types/uuid": "^3.4.5",
    "ts-node-dev": "^1.0.0-pre.40",
    "typescript": "^3.5.2"
  }
}
```

ここに`dev`スクリプトを設定しておく。

```Js:backend/package.json
"scripts": {
    "dev": "ts-node-dev index.ts "
  },
```

### `tsconfig.json`を`extends`して書く

ルートに書いた`tsconfig.json`に足りないオプションや、変更が必要なオプションを書く。

```ts:backend/tsconfig.json
{
  "extends": "../tsconfig",
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "lib": ["es2016", "esnext.asynciterable"],
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
```

## HelloWorld GraphQLを立てる

今回の例のHelloWorldな`index`を書いておく。


```ts:backend/index.ts
import "reflect-metadata";
import { GraphQLServer } from "graphql-yoga";
import { buildSchema, Query, Resolver } from "type-graphql";

@Resolver()
class HelloResolver {
  @Query(() => String, {
    description: "新しい言語を学ぶ当たって、Hello Worldをしないと呪われる。"
  })
  sayHello() {
    return "Hello World!";
  }
}

const main = async () => {
  const schema = await buildSchema({
    resolvers: [HelloResolver]
  });

  const server = new GraphQLServer({ schema });

  server.start(
    () => console.log("Server is running on localhost:4000")
  );
};

main();

```

以上で`yarn dev`を行うと4000番ポートにGraphQLサーバーが立つ。また`ts-node-dev`はソースの変更があれば、サーバを自動的に再起動してくれる。便利。

```terminal:terminal(ワーキングディレクトリは/backend)
yarn dev

```

デフォルト設定では[graphql-playground](https://github.com/prisma/graphql-playground)が立っているので、好き勝手遊べばいいといいたところですが、今は`sayHello`しか立っていないないので面白みがない。
http://localhost:4000

![スクリーンショット 2019-07-07 2.37.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/cd4b1145-a1ed-c245-0d64-a0d539124371.png)
![スクリーンショット 2019-07-07 2.36.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/7aa0fa90-81e6-fcc4-180a-590267b6a978.png)

## `type-graphql`と`graphql-yoga`で立てる時のポイント

* `type-graphql`で定義を書くときは`import "reflect-metadata";`をつける
* IDEの補完を効かせるのに便利なスキーマは`buildSchema`に`emitSchemaFile: path.resolve(__dirname, "../schema.graphql")`オプションを加えてルートに吐かせる。
* あとあと3000番ポートで`next.js`サーバーを立てるので、`cors`オプションを`server.start`に食わせる。

```ts:backend/index.ts
import "reflect-metadata";
import { GraphQLServer } from "graphql-yoga";
import { buildSchema } from "type-graphql";
import { CrabResolver } from "./resolvers/CrabResolver";
import { AisatsuResolver } from "./resolvers/AisatsuResolver";
import path from "path";//追加

const main = async () => {
  const schema = await buildSchema({
    resolvers: [AisatsuResolver, CrabResolver],
    emitSchemaFile: path.resolve(__dirname, "../schema.graphql")//追加
  });

  const server = new GraphQLServer({ schema });

  server.start(
    {
      cors: {//追加
        credentials: true,
        origin: ["http://localhost:3000"]
      }
    },
    () => console.log("Server is running on localhost:4000")
  );
};

main();

```

### 今回書いたGraphQLサーバーの残りのコード

`Type-GraphQL`の説明は次の機会があればやってみようと思う。デコレータをやたら付けたコードからスキーマが出力される形式なので、スキーマと`typescript`の型の二度付けがいらなくなってありがたい。とはいえ、クライアントサイドからこれらのクラスを使って、`apollo-client`のクエリを発行するなり、戻り値の型を取得するなりができる感じじゃなかった。

しかし、リゾルバの書き心地が良かった。

```ts:backend/types/Crab.ts
import "reflect-metadata";
import { Field, ID, Int, ObjectType } from "type-graphql";

@ObjectType({ description: "かに" })
export class Crab {
  @Field(() => ID)
  id: string;

  @Field({ description: "かにの名前" })
  name: string;

  @Field({ description: "かにの説明", nullable: true })
  description: string;

  @Field(() => Int, { description: "かにの値段" })
  price: number;

  @Field(() => [String], { description: "かにの失った脚", nullable: true })
  lostLegs: string[];

  constructor(data: {
    id: string;
    name: string;
    description: string;
    price: number;
    lostLegs: string[];
  }) {
    this.id = data.id;
    this.name = data.name;
    this.description = data.description;
    this.price = data.price;
    this.lostLegs = data.lostLegs;
  }
}
```

```ts:backend/resolvers/CrabResolver.ts
import { Query, Resolver } from "type-graphql";
import { Crab } from "../types/Crab";
import uuid from "uuid";

@Resolver(Crab)
export class CrabResolver {
  private readonly kani: Crab[] = [
    new Crab({
      id: uuid.v4(),
      name: "カニ太郎",
      price: 50000,
      description: "つよいカニ",
      lostLegs: []
    }),
    new Crab({
      id: uuid.v4(),
      name: "カニ次郎",
      price: 55000,
      description: "よりつよいカニ",
      lostLegs: ["左後ろ脚"]
    })
  ];

  @Query(() => Crab, { description: "かにの長男を呼ぶ" })
  async getKani() {
    return this.kani[0];
  }

  @Query(() => [Crab], { description: "かにの一族を呼ぶ" })
  async getKanis() {
    return this.kani;
  }
}
```

# フロントエンド編

フロントエンドのプロジェクトを作る。

```terminal:terminal(/backendで)
cd..
mkdir frontend
cd frontend
```

## 必要なパッケージを入れる

```terminal:terminal(/frontendで)
yarn add apollo-boost graphql isomorphic-fetch mobx mobx-react-lite next react react-dom

yarn add -D @babel/core @types/graphql @types/node @types/react @types/react-dom babel-preset-mobx graphql-tag typescript
```

```Js:backend/package.json
{
  "dependencies": {
    "apollo-boost": "^0.4.3",
    "graphql": "^14.4.2",
    "isomorphic-fetch": "^2.2.1",
    "mobx": "^5.11.0",
    "mobx-react-lite": "^1.4.1",
    "next": "^8.1.1-canary.69",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  },
  "devDependencies": {
    "@babel/core": "^7.5.0",
    "@types/graphql": "^14.2.2",
    "@types/node": "^12.0.12",
    "@types/react": "^16.8.23",
    "@types/react-dom": "^16.8.4",
    "babel-preset-mobx": "^2.0.0",
    "graphql-tag": "^2.10.1",
    "typescript": "^3.5.2"
  }
}
```

`oao`で一気に`dev`が走ると嬉しいので、書くまでもないが`dev`スクリプトを用意しておく。

```Js:backend/package.json
"scripts": {
    "dev": "yarn next"
},
```

#### `tsconfig.json`を`extends`して書く

`next.js`を使うため、`"extends": "../tsconfig"`と`"experimentalDecorators": true,`さえ入れておけば一度`yarn next`すれば`tsconfig.json`を勝手に完成させてくれる。なお出力はこうなった。

```ts:frontend/tsconfig.json
{
  "extends": "../tsconfig",
  "compilerOptions": {
    "experimentalDecorators": true,
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "jsx": "preserve"
  },
  "exclude": ["node_modules"],
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}
```

`MobX`を使いたいので`.babelrc`をちょびっと書く

```Js:frontend/.babelrc
{
  "presets": ["next/babel", "mobx"]
}

```

## 型の自動生成

#### 使いそうな`GraphQL`クエリを書く

`queries`フォルダ配下に使いそうなクエリを書いてエクスポートしておく。なお、`query hogehoge`部分は省略できるが、次の型の自動生成の時の名前になるのであえてつける。決まり切った書き方するので、IDEのライブテンプレートなりを作っておくと気持ちよくなる。

```ts:frontend/queries/hello.ts
import gql from "graphql-tag";

export const getHello = gql`
  query getHello {
    sayHello
  }
`;

export const getHelloKani = gql`
    query getHelloKani  {
        sayHello
        sayKani
    }
`;
```

```ts:frontend/queries/kani.ts
import gql from "graphql-tag";

export const getKaniSay = gql`
  query getKaniSay {
    sayKani
  }
`;

export const getKani = gql`
  query getKani {
    getKani {
      name
    }
  }
`;
```

### `graphql-codegen`をルートにインストールする

ルートディレクトリに`graphql-codegen`とその親戚を入れる。

```terminal:terminal(プロジェクトルートで)
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations graphql -W
```

#### `graphql-codegen`の設定を書く

型生成の設定ファイルを書く。設定ファイルほど書きたくないものは世になし。

``` yaml:codegen.yaml
overwrite: true
schema: "http://localhost:4000" # サーバのアドレス。サーバが立ってさえ入れば勝手にスキーマを読み込んでくれる
documents: "./frontend/queries/*.ts" # 監視対象　ここのファイルに更新があれば自動的に型を作成
generates:
  ./frontend/queries/types.ts: # 出力先
    plugins:
      - typescript # 基本型とtypeの型を吐く
      - typescript-operations # クライアントからそのクエリを投げた時の戻り値型を作成してくれる
```

凝ったことをしたければ[ドキュメント](https://graphql-code-generator.com/docs/getting-started/)を読みましょう。

#### `graphql-codegen`のウォッチャを走らせる

`codegen`なるスクリプトを定義してこれを走らせる。` --watch`オプションをつけておけば、更新に応じて自動で型をどんどん`/frontend/queries/types.ts`に吐いてくれる。

```json:package.json
"scripts": {
    "dev": "oao run-script dev --parallel",
    "codegen": "graphql-codegen --config codegen.yml --watch"
  },
```

```terminal:terminal(ルートで)
yarn codegen
```

##### 生成される型
使い道としては、戻り値の型。

```ts:frontend/queries/types.ts
export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
};

/** かに */
export type Crab = {
  __typename?: "Crab";
  id: Scalars["ID"];
  /** かにの名前 */
  name: Scalars["String"];
  /** かにの説明 */
  description?: Maybe<Scalars["String"]>;
  /** かにの説明 */
  price: Scalars["Int"];
  /** かにの失った脚 */
  lostLegs?: Maybe<Array<Scalars["String"]>>;
};

export type Query = {
  __typename?: "Query";
  /** かにの長男を呼ぶ */
  getKani: Crab;
  /** かにの一族を呼ぶ */
  getKanis: Array<Crab>;
  /** あいさつは大事 */
  sayHello: Scalars["String"];
  /** かにもあいさつする */
  sayKani: Scalars["String"];
};
export type GetHelloQueryVariables = {};

export type GetHelloQuery = { __typename?: "Query" } & Pick<Query, "sayHello">;

export type GetHelloKaniQueryVariables = {};

export type GetHelloKaniQuery = { __typename?: "Query" } & Pick<
  Query,
  "sayHello" | "sayKani"
>;

export type GetKaniSayQueryVariables = {};

export type GetKaniSayQuery = { __typename?: "Query" } & Pick<Query, "sayKani">;

export type GetKaniQueryVariables = {};

export type GetKaniQuery = { __typename?: "Query" } & {
  getKani: { __typename?: "Crab" } & Pick<Crab, "name">;
};
```

### クライアントを書く

`ApolloClient from "apollo-boost"`はサーバー側とクライアント側でキャッシュ先とかブラウザの`fetch`とか`Node`の`fetch`なんか使っているのが違うっぽいので、よくわからん怒られが発生する。特に循環参照とかよくわらんのがでやがるので、多分これでうまくいくだろうというやり方を書いた。`apollo-boost`ではない`apollo-client`の`ApolloClient`を使うと楽に書けない代わりに、そこらへんをはっきりできていいと思う。知らんけど。

```ts:frontend/lib/client.ts
import ApolloClient from "apollo-boost";
import "isomorphic-fetch";

export class Client {
  static readonly uri = "http://localhost:4000";

  //サーバーなら呼んでも問題ないクライアント
  private static serverCLient = new ApolloClient({
    uri: Client.uri
  });

  //クライアントで呼ばれる日には安心なクライアント？
  private static clientClient: ApolloClient<any> | null;

  public static get client(): ApolloClient<any> {
    if (!process.browser) {
      return this.serverCLient;
    }
    if (!this.clientClient) {
      this.clientClient = new ApolloClient({ uri: Client.uri });
    }
    return this.clientClient;
  }
}
```

### ストアを書く

`import * as QueryType from "../queries/types";`が自動生成される戻り値の型。これのおかげでIDEの補完が捗りよろしい。

```ts:frontend/store/pageController.ts
import { observable } from "mobx";
import { Client } from "../lib/client";
import { getHello } from "../queries/hello";
import * as kaniQuery from "../queries/kani";
import * as QueryType from "../queries/types";

export interface PageModel {
  text: string;
}

export class PageController implements PageModel {
  @observable text: string = "";

  constructor(model: PageModel = { text: "" }) {
    this.initialize(model);
  }

  initialize(model: PageModel) {
    this.text = model.text;
  }

  async fetchKani() {
    const res = await Client.client.query<QueryType.GetKaniQuery>({
      query: kaniQuery.getKani
    });
    this.text = res.data.getKani.name;
  }

  async fetchKaniSay() {
    const res = await Client.client.query<QueryType.GetKaniSayQuery>({
      query: kaniQuery.getKaniSay
    });
    this.text = res.data.sayKani;
  }

  async fetchHello() {
    const res = await Client.client.query<QueryType.GetHelloQuery>({
      query: getHello
    });
    this.text = res.data.sayHello;
  }
}
```

#### ページを書く

```tsx:frontend/page/index.tsx
import { NextPage } from "next";
import { Observer } from "mobx-react-lite/";
import { PageController, PageModel } from "../store/pageController";
import { useState } from "react";

const Index: NextPage<{ model: PageModel }> = props => {
  const [controller] = useState(new PageController(props.model));
  return (
    <div>
      <button
        onClick={() => {
          controller.fetchHello();
        }}
      >
        へろー
      </button>
      <button
        onClick={() => {
          controller.fetchKani();
        }}
      >
        カニ
      </button>
      <Observer>{() => <p>{controller.text}</p>}</Observer>
    </div>
  );
};

Index.getInitialProps = async () => {
  const controller = new PageController();
  await controller.fetchKaniSay();
  return { model: controller };
};

export default Index;
```

Nextの`getInitialProps`は簡単なオブジェクトしか渡せないので、ここら辺の対応を工夫する必要がある。

## フロントサーバを立ち上げる

先ほどのGraphQLサーバが立っているならば、frontendで`yarn dev`を叩けばNextが立つ。

```terminal:terminal(/frontendで)
yarn dev
```


もし、先ほどのGraphQLサーバーを落としているなら、ルートで`yarn dev`をすれば、冒頭のように起動する。