LoginSignup
3
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-07-06

概要

何をやるか

  1. モノレポ?みたいなYarnのWorkspaceを立てる

    • oaoを使って、配下のプロジェクト(frontendbackend)のdevスクリプトを一度に走らせることができるようにする
    • graphql-codegenで自動で型の定義ファイルをfrontendに吐くようにする
  2. バックエンド編

    • graphql-yogaでGraphQLサーバーを立てる
    • type-graphqlでGraphQLのリゾルバを書く
    • コードファーストで、schema.graphqlはサーバー起動時に吐くようにする
    • ts-node-devで、コードの更新があればバックエンドサーバーを再起動させる
  3. フロントエンド編

    • Next.jsmobxで気持ちよく書く
    • 投げるクエリを書いて、戻り値の型はgraphql-codegenに自動生成させる

今日作るやつ

画面収録 2019-07-07 1.48.08-2.gif

できたやつ

ファイル構成

.
├── 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に引き上げられる。

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

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

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

yarn add -D oao -W

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

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

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

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

terminal
yarn oao clean

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

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

terminal
yarn

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

Next.jsのGitHubに書かれているtsconfig.jsonを引っ張ってきた。targetなどは配下のプロジェクトでこれをextendsして設定する。

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. */
  }
}

バックエンド編

バックエンドのプロジェクトを作る。oaolernaのように、サブプロジェクト作るときに注意するべきことは特になさそうなので、普通にmkdirなりして、そこでyarnを叩けば良い。

terminal
mkdir backend
cd backend

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

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は以下のようになっていた。

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スクリプトを設定しておく。

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

tsconfig.jsonextendsして書く

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

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

HelloWorld GraphQLを立てる

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

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(ワーキングディレクトリは/backend)
yarn dev

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

スクリーンショット 2019-07-07 2.37.03.png
スクリーンショット 2019-07-07 2.36.41.png

type-graphqlgraphql-yogaで立てる時のポイント

  • type-graphqlで定義を書くときはimport "reflect-metadata";をつける
  • IDEの補完を効かせるのに便利なスキーマはbuildSchemaemitSchemaFile: path.resolve(__dirname, "../schema.graphql")オプションを加えてルートに吐かせる。
  • あとあと3000番ポートでnext.jsサーバーを立てるので、corsオプションをserver.startに食わせる。
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のクエリを発行するなり、戻り値の型を取得するなりができる感じじゃなかった。

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

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;
  }
}
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(/backendで)
cd..
mkdir frontend
cd frontend

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

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
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スクリプトを用意しておく。

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

tsconfig.jsonextendsして書く

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

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をちょびっと書く

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

型の自動生成

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

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

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

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

export const getHelloKani = gql`
    query getHelloKani  {
        sayHello
        sayKani
    }
`;
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(プロジェクトルートで)
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations graphql -W

graphql-codegenの設定を書く

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

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

凝ったことをしたければドキュメントを読みましょう。

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

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

package.json
"scripts": {
    "dev": "oao run-script dev --parallel",
    "codegen": "graphql-codegen --config codegen.yml --watch"
  },
terminal(ルートで)
yarn codegen
生成される型

使い道としては、戻り値の型。

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とかNodefetchなんか使っているのが違うっぽいので、よくわからん怒られが発生する。特に循環参照とかよくわらんのがでやがるので、多分これでうまくいくだろうというやり方を書いた。apollo-boostではないapollo-clientApolloClientを使うと楽に書けない代わりに、そこらへんをはっきりできていいと思う。知らんけど。

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の補完が捗りよろしい。

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;
  }
}

ページを書く

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(/frontendで)
yarn dev

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

3
6
1

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
3
6