📗

Next.jsとNode.jsでスキーマファーストなGraphQLの環境を構築する

2021/08/15に公開

概要

  • Next.js(ts) + Node.js(ts) + apollo + graphql-code-generatorでスキーマファーストなGraphQL環境を構築していくよ!
  • 対象: ざっくりGraphQLでスキーマファーストな環境を構築する方法を知りたい人
  • 説明しないこと: .graphqlファイルの書き方とかGraphQLの設計とか、具体的なこと

GraphQLとは何か

  • APIにクエリを投げてFE側から返却するレスポンスを制限できるAPIの形式
  • 有名どころだとNetflixとかで利用されていて、APIの返却をFEで制御できるので『画面やデバイスによって表示する情報がちょっと差がある』みたいな時に便利

スキーマファーストとは何か

  • APIのIF設計を先にガッチリ決める方式
  • FEとBEの認識を同タイミングで合わせやすい
    • ただし、IF定義を先にきっちり作成する必要がある

手順

  • 定義 -> ドキュメント -> FE -> BEでやっていきます
  • 『おこづかいちょう』なデータをスキーマファーストで実装していきます。

データサンプル

{
  [
    {
      id: 1,
      in: 1000,
      memo: "おこづかい",
      date: "2021/04/01"
    },
    {
      id: 2,
      out: 80,
      memo: "えんぴつ",
      date: "2021/04/05"
    },
    {
      id: 3,
      out: 100,
      memo: "のーと",
      date: "2021/04/17"
    },
    {
      id: 4,
      out: 320
      memo: "けーき"
      date: "2021/04/28"
    },
  ]
}

定義

schema.graphql
scalar Date

type Transaction {
  id: ID!
  memo: String!
  in: Int
  out: Int
  transactionDate: Date!
}

input TransactionInput {
  memo: String!
  in: Int
  out: Int
  transactionDate: Date!
}

type Query {
  getTransaction(id: ID!): Transaction
  getTransactions: [Transaction!]
}

type Mutation {
  addTransaction(input: TransactionInput): Transaction!
  updateTransaction(id: ID!, input: TransactionInput): Transaction!
}

ドキュメント

schema.graphql
scalar Date

"""
取引レコード
"""
type Transaction {
  """
  取引ID
  """
  id: ID!
  """
  取引に関するメモ
  """
  memo: String!
  """
  入金額
  """
  in: Int
  """
  出金額
  """
  out: Int
  """
  取引日付
  """
  transactionDate: Date!
}

"""
取引のInput
"""
input TransactionInput {
  """
  取引に関するメモ
  """
  memo: String!
  """
  入金額
  """
  in: Int
  """
  出金額
  """
  out: Int
  """
  取引日付
  """
  transactionDate: Date!
}

type Query {
  """
  取引データの取得
  """
  getTransaction(
    """
    取引ID
    """
    id: ID!
  ): Transaction
  """
  取引一覧の取得
  """
  getTransactions: [Transaction!]
}

type Mutation {
  """
  取引の追加
  """
  addTransaction(
    """
    取引内容
    """
    input: TransactionInput
  ): Transaction!
  """
  取引の更新
  """
  updateTransaction(
    """
    取引ID
    """
    id: ID!, 
    """
    取引内容
    """
    input: TransactionInput
  ): Transaction!
}

FEとBEの型を生成

種別 Example
FE React-Apollo Hooks
BE Resolvers Signature
  • その結果がこんな感じ
codegen.yml
overwrite: true
schema:
  - ./schema.graphql
generates:
  ../back/types/graphql-resolver-types.ts:
    plugins:
      - typescript
      - typescript-resolvers
  ../front/types/graphql-client-api.tsx:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
  • そして実行していきます
yarn add graphql
yarn add -D @graphql-codegen/typescript @graphql-codegen/typescript-resolvers @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
yarn graphql-codegen

BE

mkdir back
cd back
yarn init --yes
yarn add apollo-server graphql typescript @types/node@16
npx tsc --init
mkdir src
touch src/index.ts
  • この先の実装に関しては、趣味によるブレがあるのでエッセンスだけ記載していきます
    • GraphQLのtypeDefsschema.graphqlをファイルごと読み込んでnew AppolloServerの引数にセット
    • Query, Mutationは自動生成したファイルにあるQueryResolvers, MutationResolversを利用してオブジェクトを作る
  • 結果、index.tsはこんな感じ
index.ts
import fs from 'fs';
import path from 'path';
import { ApolloServer, gql } from 'apollo-server';
import { Resolvers } from '../types/graphql-resolver-types'
import { Query } from './query'
import { Mutation } from './mutation'
const PORT = 4040;

const typeDefs = fs
  .readFileSync(path.join(__dirname, '../../graphql/schema.graphql'))
  .toString();

export const resolvers: Resolvers = {
  Query,
  Mutation,
};

const server = new ApolloServer({
  typeDefs: gql`${typeDefs}`,
  resolvers
});

server.listen({ port: PORT }).then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});
  • ちなみに起動したサーバにアクセスするとクエリを試し打ちできるPOSTMANみたいなやつと、ドキュメント的なものにアクセスできる

FE

index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
import styles from '../styles/Home.module.css'
import { gql } from "@apollo/client";
import client from "../apollo-client";
import { Maybe, Transaction } from "../types/graphql-client-api";
import { InferGetStaticPropsType } from 'next';

type Props = InferGetStaticPropsType<typeof getStaticProps>;

export async function getStaticProps() {
  const { data } = await client.query({
    query: gql`
      query {
        getTransactions {
          id
          memo
          in
          out
          transactionDate
        }
      }
    `,
  });

  return {
    props: {
      transactions: data.getTransactions.slice(0, 4),
    },
  };
}


function Transactions(props: Props) {
  const transactions = props.transactions;
  if (transactions.length !== 0) {
    return (
      transactions.map((country:Transaction) => (
        <tr key={country.id} className={styles.card}>
          <td>{country.transactionDate}</td>
          <td>{country.memo}</td>
          <td>{country.in}</td>
          <td>{country.out}</td>
        </tr>
      ))
    )
  }
  return (
    <tr>
      <td>データがありません</td>
    </tr>
  )
}

const Home: NextPage<Props> = ({ transactions }: {transactions: Maybe<Transaction[]>}) => {
  return (
    <div className={styles.container}>
      <Head>
        <title>おこづかい帳</title>
        <meta name="description" content="practice next.js and node.js with GraphQL" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>
          おこづかい帳
        </h1>
        <table className={styles.grid}>
          <tr>
            <th>取引日付</th>
            <th>用途</th>
            <th>入金</th>
            <th>出金</th>
          </tr>
          <Transactions transactions={transactions} />
        </table>
      </main>
    </div>
  )
}

export default Home

おわりに

  • ドキュメントから型が自動生成できるなんてかっこいい!!という軽い気持ちでやってみたら、思ったよりサクっと作れてステキ
  • 今回はバックエンドもフロントエンドもTypeScriptなので、型を共有するだけで良くない?という説は十二分にあるが、バックエンドをTypeScriptで組む機会は少ないと思うのでGoやJavaの型も作れるGraphQL code generatorの活躍の幅が広い予感!
  • React側は生成されたやつを活用しきれてないんじゃね?感があるので、もうちょっと研究を重ねたい

Discussion