📗
Next.jsとNode.jsでスキーマファーストなGraphQLの環境を構築する
概要
- 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
を書きます- スキーマ定義は今回は公式のmutationとinput型についての定義を参考にしました
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!
}
ドキュメント
- gqldocを利用してドキュメントも自動生成
- 公式のHow to useの『Generate docs from graphql schema files』に従ってファイルから生成します
-
gqldoc -s schema.graphql -o ../doc
- package.jsonにscript追加しておくと後が楽
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の型を生成
- graphql-codegen で型定義を生成する (React, Apollo, TypeScript) - Qiitaを参考にFEとBEの型を生成し、FEとBEをを作っていきます。
- GraphQL Code Generatorの公式のサンプルの『Choose Live Example』を利用すると自分が必要なのがサクッとわかります。今回は下記のExampleの時のymlを参考に、ymlを書いていきます
種別 | 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
-
apollo公式のappolo-serverのGet startedを参考に作っていきます。ts化はよしなに。
- node.jsプロジェクトをTypeScript化する手順はTypeScript + Node.js プロジェクトのはじめかた2020 - Qiitaを参考にしています
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の
typeDefs
はschema.graphql
をファイルごと読み込んでnew AppolloServer
の引数にセット - Query, Mutationは自動生成したファイルにある
QueryResolvers
,MutationResolvers
を利用してオブジェクトを作る
- GraphQLの
- 結果、
index.ts
はこんな感じ-
index.ts
以外はgithubをご確認ください
-
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
-
Next.jsの公式のGetting started を参考に
yarn create next-app --typescript
でセットアップ - 続きはapollo-BlogのGetting Started With Apollo Client in Next.jsの記事を参考にNext.jsにapolloを載せていきます。概ね下記を書き換えれば大丈夫
- クエリの書き方を自作APIにあわせる
- 記事のコピペ部分はtsのエラーはうまく解消する
- 型は生成したものを利用する
- その結果の
index.tsx
はこんな感じ。- それ以外の部分はgithubをご確認ください
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