GraphQLクエリをCDNキャッシュに載せたい
https://gyazo.com/8c211bb56664804a0dbf7c16dab18e4d
大稻埕碼頭
はじめに
私が所属するモニクルのサービス「マネイロ」では、REST APIからGraphQLへの移行を進めています。フロントエンドはNext.jsを採用しており、REST APIのデータ取得にはSWRを使っています。これをApollo Clientで置き換えています。バックエンドではRailsにGraphQL Rubyを使ってクエリやミューテーションを作成しています。なお、Apollo Serverは使っていません。 さて、マネイロは資産運用に関する悩みを解決するお手伝いをするために無料でオンライン相談ができるサービスを提供しています。そのために相談予約可能な日時や時間帯を取得するAPIがありますが、このAPIレスポンスはCDNにキャッシュしています。GraphQLに移行しても同じようにCDNにキャッシュしたいのですが、GraphQLはデフォルトでPOSTリクエストするためVercelのCDNでキャッシュすることができません。また、そもそもGraphQLクエリをブラウザに露出してしまうのも避けたいところです。そこで、Persisted Queriesを使ってこの問題を解決することにしました。 Persisted Queries を使う方法として二種類のパターンを考えてみました。
Next.js(フロントエンド)からNext.js(API Routes)へのリクエストにのみハッシュ値を使う
Next.js(フロントエンド)からRails(バックエンド)に至るまで全てハッシュ値でやりとりする
今回は前者の実装だけで目的を達成することができるため、前者の実装を行うことにしました。
クエリとハッシュ値を相互に変換するテーブルを用意する
code:codegen.yml
overwrite: true
...
generates:
generated/server.graphql.gen.json:
plugins:
- graphql-codegen-persisted-query-ids:
output: server
algorithm: sha256
generated/client.graphql.gen.json:
plugins:
- graphql-codegen-persisted-query-ids:
output: client
algorithm: sha256
その後、コード生成コマンドを実行します。
code:console
./node_modules/.bin/graphql-codegen --config codegen.yml
これで、generated/server.graphql.gen.jsonとgenerated/client.graphql.gen.jsonのファイルがそれぞれ出来上がります。どのような形式になっているかは、前回の記事が参考になります。 Apollo Linkの設定
Http Linkを使います。以下のように設定していきます。
url
BFFへのエンドポイントを指定する
useGETForQueries
クエリにはGETリクエストを利用するためtrueにする
fetch
GETリクエストの場合は query をハッシュ値に置き換える
code:apolloClient.ts
import { ApolloClient, from, HttpLink } from "@apollo/client"
import queryHash from "generated/client.graphql.gen.json"
const endPointLink = new HttpLink({
uri: "/api/graphql",
useGETForQueries: true,
fetch: (uri, options: RequestInit) => {
if (options.method === "GET") {
// クエリをハッシュ値に変換します
const params = new URLSearchParams(queryString)
// params の中身についてはこちらが参考になります
const { query, operationName, ...others } = Object.fromEntries(params)
const hashedParams = new URLSearchParams({
operationName,
...others,
})
return fetch(${path}?${hashedParams}, options)
}
return fetch(uri, options)
},
})
export const apolloClient = (() =>
上記の例ではfetchを独自にオーバーライドしていますが、graphql-codegen-persisted-query-idsはApollo Link用のアダプタも用意されていて、こちらを使うことでApolloの仕様に沿ったリクエストを書くこともできます。
API Routesでの処理
最後にAPI Routesの処理を書いていきます。主な観点は以下の部分です。
next-http-proxy-middlewareを使用すると、簡単にProxyするBFFが書けて楽
ハッシュ値をクエリに戻した上で、GraphQLバックエンドにPOSTリクエストする
HTTP キャッシュヘッダーをセットする
code:pages/api/grpahql.ts
import type opnameHash from "generated/client.graphql.gen.json"
import hashQuery from "generated/server.graphql.gen.json"
import type httpProxy from "http-proxy"
import { NextApiHandler, NextApiRequest, NextApiResponse, PageConfig } from "next"
import httpProxyMiddleware from "next-http-proxy-middleware"
export const config: PageConfig = {
api: {
bodyParser: false,
externalResolver: true,
},
}
const handleProxyInit = (proxy: httpProxy) => {
proxy.on("proxyReq", (proxyReq, req, res) => {
if (proxyReq.method !== "GET") return
const params = new URLSearchParams(queryString)
// クエリのハッシュ値からクエリを復元する
const { q, operationName, ...others } = Object.fromEntries(params)
const query: string | undefined = hashQueryq if (!query) throw new Error(クエリが見つかりませんでした。 ${q})
// GETリクエストをPOSTリクエストとして送信する
proxyReq.method = "POST"
proxyReq.path = ${path}?operationName=${operationName}
const bodyData = JSON.stringify({
query,
operationName,
...others,
})
proxyReq.setHeader("Content-Length", Buffer.byteLength(bodyData))
proxyReq.write(bodyData)
})
proxy.on("proxyRes", (proxyRes, req, res) => {
// ※ req オブジェクトからリクエスト情報が取得できるので、クエリに応じたキャッシュの設定などをすると良いでしょう
})
}
const h: NextApiHandler = (req: NextApiRequest, res: NextApiResponse): Promise<void> =>
httpProxyMiddleware(req, res, {
..., // ※ RailsバックエンドへのProxyの設定を書く
onProxyInit: handleProxyInit,
})
export default h
これで完了です。Apollo Linkを使っているため、フロント側のuseQueryフックは変更する必要がありません。これでCDNでのキャッシュと、クエリをブラウザに露出させることを防ぐことができるようになりました。
まとめ
Persisted QueriesはApolloが提唱する仕様通りに作ることもできますが、今回の例のように自分で変換処理を書くことで、最低限の仕組みだけで実装することが可能です。
一方でApolloの仕様に則ったライブラリも豊富なので、そちらに実装に合わせておくと良い場合もありそうです。いずれにしても仕組み自体は難しくないので、覚えておくと何かと便利な仕組みだと思いました。
https://i.gyazo.com/51454271bc5520c96d424a29a19498c4.png https://twitter.com/share?url=https%3A%2F%2Fscrapbox.io%2Fwada-blog%2FGraphQL%25E3%2582%25AF%25E3%2582%25A8%25E3%2583%25AA%25E3%2582%2592CDN%25E3%2582%25AD%25E3%2583%25A3%25E3%2583%2583%25E3%2582%25B7%25E3%2583%25A5%25E3%2581%25AB%25E8%25BC%2589%25E3%2581%259B%25E3%2581%259F%25E3%2581%2584&text=GraphQL%E3%82%AF%E3%82%A8%E3%83%AA%E3%82%92CDN%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E3%81%AB%E8%BC%89%E3%81%9B%E3%81%9F%E3%81%84 < Twitterでシェア
宣伝
株式会社モニクルでは、はたらく世代・子育て世代が抱えるお金や資産運用に関する不安や悩みを解決するお手伝いをさせていただき、現在から老後に至るまでの資金計画や最適なポートフォリオの提案を行っております。