typescript with express with react with graphqlの今の所の個人的最適解

## クソみたいなポエム

JavaScriptに飽きてTypeScriptに走ることは、とても自然なことである。
REST APIに飽きてGraphQLに興味を持つということも、とても自然なことである。
GraphQL queryを書きたくないと考えこともまた、とても自然なことである。
そして、私はTypeScriptでGraphQLする最適解を探し求める旅に出た。

## 素敵なツールたち

旅の結果、こういう素敵なツールたちに出会った

- [Apollo Server](https://github.com/apollographql/apollo-server)  
  GraphQL server。  
  使おう。
- [Apollo Client](https://github.com/apollographql/apollo-client)  
  一番人気であろうgraphqlクライアント。  
  これにはローカルのステート管理機能もついてるので、Reduxを使う必要がなくなる。  
  使おう。
- [TypeGraphQL](https://github.com/MichalLytek/type-graphql)  
  TypeScriptコードからTypeやResolverを生成するツール。  
  使わない手はない。
- [GraphQL Code Generator](https://github.com/dotansimha/graphql-code-generator)  
  GraphQL queryからTypeScriptコードを生成するツール。  
  これのおかげでGraphQL query書いて、それに合う型ファイルなどを自分で書くという苦行から開放される。

この三種の神器を持ってすれば、素敵なTypeScript GraphQLライフを満喫できるだろう。

## サンプルリポジトリ

神器たちを投入したサンプルリポジトリ。

[typescript-express-react-graphql](https://github.com/sijiaoh/typescript-express-react-graphql)

## Server
### GraphQL serverを立てる

サーバーサイドではTypeGraphQLを使い、TypeとResolverを作る。

```ts:server/src/graphql/types/Hello.ts
import {Field, ObjectType} from 'type-graphql';

@ObjectType()
export class Hello {
  @Field()
  str!: string;
  @Field()
  date!: Date;
}
```

```ts:server/src/graphql/resolvers/HelloResolver.ts
import {Query, Resolver} from 'type-graphql';
import {Hello} from '../types/Hello';

@Resolver()
export class HelloResolver {
  @Query(() => Hello)
  hello(): Hello {
    return {
      str: 'hello world',
      date: new Date(),
    };
  }
}
```

この二つをbuildSchema関数に投入すると、こういうSchemaが生成される。

```graphql
scalar DateTime

type Hello {
  date: DateTime!
  str: String!
}

type Query {
  hello: Hello!
}
```

このままresolversと一緒にApolloServerに渡せば、GraphQLサーバーが立つ。

```ts
import 'reflect-metadata';

import express from 'express';
import {ApolloServer} from 'apollo-server-express';
import {ResolversMap} from 'type-graphql';
import {buildSchema} from './graphql/buildSchema';
import {resolvers} from './graphql/resolvers';

const app = express();
const server = new ApolloServer({
  schema: buildSchema(),
  resolvers: resolvers as ResolversMap,
});

server.applyMiddleware({app});

app.listen(8080);
```

そう、なんと、スキーマを自分で書く必要がないのだ。

### Schemaをファイルとして出力する

クライアントサイドでGraphQL Code Generatorを使ってコード生成するためには、サーバー側のSchemaファイルが必要だ。
buildSchemaにemitSchemaFileオプションを渡すと、その場所にファイルを吐き出してくれるので、コマンドとして用意しておく。

```ts:server/bin/generate-graphql-schema.ts
import 'reflect-metadata';

import path from 'path';
import {buildSchema} from '../src/graphql/buildSchema';

buildSchema({
  emitSchemaFile: `${path.dirname(__dirname)}/generated/schema.gql`,
});
```

```ts:server/src/graphql/buildSchema.ts
import {resolvers} from './resolvers';
import {buildSchemaSync, NonEmptyArray, BuildSchemaOptions} from 'type-graphql';

export const buildSchema = (options: Partial<BuildSchemaOptions> = {}) => {
  return buildSchemaSync({
    resolvers: (Object.values(resolvers) as unknown) as NonEmptyArray<Function>,
    ...options,
  });
};
```

```json:server/package.json
{
  ...
  "scripts": {
    ...
    "generate-graphql-schema": "ts-node bin/generate-graphql-schema.ts",
    "codegen": "npm run generate-graphql-schema"
  },
  ...
}
```

これで`npm run codegen`と叩くと、server/generated/にこんなファイルが作られる。

```graphql:server/generated/schema.gql
# -----------------------------------------------
# !!! THIS FILE WAS GENERATED BY TYPE-GRAPHQL !!!
# !!!   DO NOT MODIFY THIS FILE BY YOURSELF   !!!
# -----------------------------------------------

"""
The javascript `Date` as string. Type represents date and time as the ISO Date string.
"""
scalar DateTime

type Hello {
  date: DateTime!
  str: String!
}

type Query {
  hello: Hello!
}
```

生成するファイルなので、gitignoreとeslintignoreへの追加をお忘れなく。

これで、サーバーサイドの仕事は終了。

## Client

Apollo Clientの初期化は公式通りにやれば良い。

### クエリを書く

```tsx:client/src/Hello.tsx
...
import {gql} from '@apollo/client';
...

gql`
  query Hello {
    hello {
      str
      date
    }
  }
`;

...
```

GraphQL Code Generatorがgql`というキーワードを認識して、以降の文字列をクエリとして認識してくれるため、これでいい。
使いまわしたいクエリは、client/src/graphql/queries/でも作って、.gqlファイルに書けばいい。

### コード生成

GraphQL Code Generatorの設定はこうする。

```yaml:client/codegen.yml
overwrite: true
schema: "../server/generated/schema.gql"
documents: [
  "src/**/*.ts",
  "src/**/*.tsx",
  "src/**/*.gql"
]
generates:
  src/generated/graphql.tsx:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"
```

schemaはサーバーで生成したものを使うところがミソ。
.tsや.tsxに書いたクエリも認識してほしいので、その設定もしておく。
この状態でnpm run codegenを叩くと、client/src/generated/にファイルが生成される。

```ts:client/src/generated/graphql.tsx
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
  /** The javascript `Date` as string. Type represents date and time as the ISO Date string. */
  DateTime: any;
};


export type Hello = {
  __typename?: 'Hello';
  date: Scalars['DateTime'];
  str: Scalars['String'];
};

export type Query = {
  __typename?: 'Query';
  hello: Hello;
};

export type HelloQueryVariables = Exact<{ [key: string]: never; }>;


export type HelloQuery = (
  { __typename?: 'Query' }
  & { hello: (
    { __typename?: 'Hello' }
    & Pick<Hello, 'str' | 'date'>
  ) }
);


export const HelloDocument = gql`
    query Hello {
  hello {
    str
    date
  }
}
    `;

/**
 * __useHelloQuery__
 *
 * To run a query within a React component, call `useHelloQuery` and pass it any options that fit your needs.
 * When your component renders, `useHelloQuery` returns an object from Apollo Client that contains loading, error, and data properties
 * you can use to render your UI.
 *
 * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
 *
 * @example
 * const { data, loading, error } = useHelloQuery({
 *   variables: {
 *   },
 * });
 */
export function useHelloQuery(baseOptions?: Apollo.QueryHookOptions<HelloQuery, HelloQueryVariables>) {
        return Apollo.useQuery<HelloQuery, HelloQueryVariables>(HelloDocument, baseOptions);
      }
export function useHelloLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<HelloQuery, HelloQueryVariables>) {
          return Apollo.useLazyQuery<HelloQuery, HelloQueryVariables>(HelloDocument, baseOptions);
        }
export type HelloQueryHookResult = ReturnType<typeof useHelloQuery>;
export type HelloLazyQueryHookResult = ReturnType<typeof useHelloLazyQuery>;
export type HelloQueryResult = Apollo.QueryResult<HelloQuery, HelloQueryVariables>;
```

生成するファイルなので、gitignoreとeslintignoreへの追加をお忘れなく。

### リクエストを投げる

client/src/generated/graphql.tsxにhookが生成されているので、それを普通に使う。
型が効いてていい感じ。

```tsx:client/src/Hello.tsx
import React from 'react';
import {gql} from '@apollo/client';
import {useHelloQuery} from './generated/graphql';

gql`
  query Hello {
    hello {
      str
      date
    }
  }
`;

export const Hello = () => {
  const {loading, error, data} = useHelloQuery();
  if (loading) return <div>Loading...</div>;
  if (error) return <div>{error.message}</div>;
  return (
    <div className={Hello.name}>
      <div>
        <div>str={data!.hello.str}</div>
        <div>date={data!.hello.date}</div>
      </div>
    </div>
  );
};
```

## 遺憾

この方式を使えば、サーバーサイドではGraphQL queryを一文字も書かずに済むが、クライアントサイドではどうしてもクエリを書かざるを得なくなってしまう。
これすらも解消すれば最強な気がしているが、その方法をまだ私は見つけられていない。
どこかのイケメンが教えてくれることを切に願い震えて待つ。