GraphQLクライアントを使ってNuxtアプリケーションの実装をする
今更ながら、初めてGraphQLを使ったプロジェクトをやることになったので、
UIライブラリへの依存はないような設定手順などをまとめます。
とはいえ、動かすための場が必要なので Nuxt で SSG をするアプリケーションを作ることにします。
Nuxt への組み込みには @nuxtjs/compositon-api
を使います。
リポジトリはこちらです。
Nuxt プロジェクトのセットアップ
$ npx create-nuxt-app nuxt-graphql-example
TypeScript と Universal と SSG を選択してあとは適当に。
composition-api を使うのでそれも入れる。
普段ならディレクトリ構成いじったりするけど、とりあえずここまで。
$ npm run dev
はい。
GraphQL 周りのセットアップ
GraphQL サーバは GitHub の GraphQL API を使うことにします。
認証が必要だったりそれなりの規模なので遊ぶには十分という判断です。
エディタの設定
VSCode を使います。
GraphQL のクエリを書いているときに補完などが効くようにします。
GraphQL 拡張を入れます。
プロジェクトの推奨拡張機能に入れて、それをインストールするという手順を踏むのが個人的に好きです。
{
"recommendations": [
"graphql.vscode-graphql"
]
}
設定ファイルを書きます。
schema:
- https://api.github.com/graphql:
headers:
Authorization: "bearer ${GITHUB_PAT}"
documents: graphql/**/*.graphql
GITHUB_PAT
は GitHub のパーソナルアクセストークン (PAT) です。
以下を参考に作りましょう。
GITHUB_PAT
は環境変数です。.env
に書いて Git のコミットには含めないようにしましょう。
GITHUB_PAT=<ここにPATをかく>
試しに簡単なクエリを書いてみます。以下を参考に書いていきます。
query vueRepository {
repository(name: "vue", owner: "vuejs"){
createdAt
}
}
ちゃんと補完が効きます。
コード生成の設定
TypeScriptのプロジェクトなので、型情報を生成してほしいです。
また、クエリーに対応する API クライアントも欲しいのでそれを生成してもらうことにします。
GraphQL Code Generator を利用します。
以下に沿って進めます。
依存パッケージをインストール。
$ npm i graphql
$ npm install --save-dev @graphql-codegen/cli
$ npm install --save-dev @graphql-codegen/typescript
設定は graphql.config.yml
に書きます。
schema:
- https://api.github.com/graphql:
headers:
Authorization: "bearer ${GITHUB_PAT}"
documents: graphql/**/*.graphql
extensions:
codegen:
generates:
./types/graphql.ts:
plugins:
- typescript
いったん実行してみます。
$ npx graphql-codegen -r dotenv/config
graphql schema の型情報が出力されています。
export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
/** snip **/
/** A hovercard context with a message describing how the viewer is related. */
export type ViewerHovercardContext = HovercardContext & {
__typename?: 'ViewerHovercardContext';
/** A string describing this context */
message: Scalars['String'];
/** An octicon to accompany this context */
octicon: Scalars['String'];
/** Identifies the user who is related to this context. */
viewer: User;
};
APIクライアントも生成します。
プラグインなどをインストール
$ npm i graphql-request
$ npm i -D @graphql-codegen/typescript-operations @graphql-codegen/typescript-graphql-request
設定ファイルを修正
schema:
- https://api.github.com/graphql:
headers:
Authorization: "bearer ${GITHUB_PAT}"
documents: graphql/**/*.graphql
extensions:
codegen:
generates:
./infrastructure/graphql/generated-client.ts:
plugins:
- typescript
- typescript-operations
- typescript-graphql-request
コード生成
$ npx graphql-codegen -r dotenv/config
試しに実行してみます。
環境変数を読み込むようにパッケージ追加
$ npm i dotenv
実行のためのスクリプト実装
import { GraphQLClient } from 'graphql-request';
import { getSdk } from './generated-client';
import dotenv from "dotenv";
const { parsed: env } = dotenv.config()
async function main() {
const client = new GraphQLClient('https://api.github.com/graphql', {
headers: {
Authorization: `bearer ${env?.GITHUB_PAT}`
}
});
const sdk = getSdk(client);
const { repository } = await sdk.vueRepository();
console.log(`GraphQL data:`, repository);
}
main()
実行
$ npx ts-node ./infrastructure/graphql/index.ts
GraphQL data: { createdAt: '2013-07-29T03:24:51Z' }
生成したコードから GraphQL API へのリクエストができました。
アプリケーションへの組み込み
正直やや蛇足感がありますが、生成したクライアントをアプリケーションに組み込んでいくところまでやろうと思います。
ここでは、以下3つのOrganizationが持っているリポジトリを検索するアプリケーションを作ってみようと思います。(特に意味はないです)
アプリケーションで使う型定義
リポジトリ名とイシュー数とスター数を表示することにします。
また、APIクライアントの型とそれを注入するためのキーを用意しておきます。
import { InjectionKey } from "@nuxtjs/composition-api";
export type Repository = {
name: string;
numOfIssues: number;
numOfStars: number;
}
export type FetchRepositories = (group: string) => Promise<Repository[]>
export const fetchRepositoriesInjectionKey: InjectionKey<FetchRepositories> = Symbol("FetchRepositories")
GraphQL クエリの作成
ランタイムでの型チェックのために __typename
を含めておきます。
query fetchRepositories($query: String!) {
search(query: $query, type: REPOSITORY, first: 100) {
nodes {
... on Repository {
__typename
name
issues {
totalCount
}
stargazers {
totalCount
}
}
}
}
}
オブジェクトのコンバータ実装
型がついているとは言え、APIの実装をアプリケーションへもっていくのはあまり好きではないので、適当なレイヤでアプリケーションの型へ変換しておきます。
import { Repository } from '~/domain/repository'
import { FetchRepositoriesQuery } from './generated-client'
/**
* Repository型
*/
type FetchRepositoriesQueryNode = Exclude<
FetchRepositoriesQuery['search']['nodes'],
null | undefined
>[0]
type FetchRepositoriesQueryRepositoryNode = Extract<
FetchRepositoriesQueryNode,
{ readonly __typename?: 'Repository' }
>
/**
* Repository型のアサーション
*/
export type IsRepositoryNode = (
node: FetchRepositoriesQueryNode
) => node is FetchRepositoriesQueryRepositoryNode
export const isRepositoryNode: IsRepositoryNode = (
node
): node is FetchRepositoriesQueryRepositoryNode =>
node?.__typename === 'Repository'
/**
* 変換関数
*/
export type ConvertRepositoryFrom = (result: FetchRepositoriesQuery) => Repository[]
export const convertRepositoryFrom: ConvertRepositoryFrom = (result) => {
return result.search.nodes?.filter(isRepositoryNode).map((node) => ({
name: node.name,
numOfIssues: node.issues.totalCount,
numOfStars: node.stargazers.totalCount,
}))
|| []
}
公開するクライアントは、きれいになったオブジェクトを返すものにします。
import { GraphQLClient } from 'graphql-request'
import { getSdk } from './generated-client'
import { FetchRepositories } from '~/domain/repository'
import { convertRepositoryFrom } from './converter'
export const fetchRepositoriesFactory = (pat: string): FetchRepositories => (
group
) =>
getSdk(
new GraphQLClient('https://api.github.com/graphql', {
headers: {
Authorization: `bearer ${pat}`,
},
})
)
.fetchRepositories({ query: `org:${group}` })
.then(convertRepositoryFrom)
アプリケーションへ組み込み
あとはアプリケーションで使えるようにするだけです。
まずは、provide
で API クライアントを inject
できる状態にします。
import { onGlobalSetup, provide, defineNuxtPlugin } from '@nuxtjs/composition-api'
import { fetchRepositoriesInjectionKey } from "~/domain/repository";
import { fetchRepositoriesFactory } from "~/infrastructure/graphql/";
export default defineNuxtPlugin(({ $config }) => {
onGlobalSetup(() => {
provide(fetchRepositoriesInjectionKey, fetchRepositoriesFactory($config.AUTH_TOKEN))
})
})
PAT は runtimeConfig 経由で渡します。
/* snip */
privateRuntimeConfig: {
AUTH_TOKEN: process.env.GITHUB_PAT
},
publicRuntimeConfig: {
AUTH_TOKEN: process.env.NODE_ENV === "development" ? process.env.GITHUB_PAT : ""
}
/* snip */
後は、各ページでデータを取得すればOKです。
(マークアップ省略)
import {
defineComponent,
inject,
useContext,
useStatic,
computed,
ref,
} from '@nuxtjs/composition-api'
import { fetchRepositoriesInjectionKey } from '~/domain/repository'
export default defineComponent({
setup() {
const fetchRepository = inject(fetchRepositoriesInjectionKey)
if (!fetchRepository) {
throw new Error('fetchRepositories is not injected')
}
const query = ref('')
const { route } = useContext()
const org = computed(() => route.value.params.name)
const repositories = useStatic((org) => fetchRepository(org), org, 'org')
return {
repositories: computed(
() =>
(query.value === ''
? repositories.value
: repositories.value?.filter((i) =>
i.name.includes(query.value)
)) || []
),
query,
}
},
})
ビルド・動作確認
$ npm run generate
$ npm start
API への通信はない状態で動作します。
おわりに
この記事では、GraphQLクライアントを使った Webフロントエンドアプリケーションの実装を行いました。
また、VSCode の拡張機能を利用し、入力補完が効く状態にし、 GraphQL Code Generator を利用して、APIクライアントの生成を行いました。
APIクライアントを Nuxtアプリケーションへ組み込み、静的サイトの生成を行いました。
APIの実装をコンポーネントへ持ち込まないよにすることで、APIサーバがGraphQLから別の実装なった場合も影響が局所化されるような構成をとりました。
GraphQLは必要な情報だけを取得できるなど、開発の体験としてはとても良かったと思います。
一方で、APIへの問い合わせという目的に強く依存した形のデータが返ってくるので、このデータをコンポーネント内部へもっていくことは避けたほうが良いなー。というのが最初に思ったことでした。
パフォーマンス的な問題も発生しやすいと思うので、必要に応じて REST や別の実装に切り替えられるような作りにしておくことが大事だと思いました。
Discussion