Under+Ground

GraphQLのリクエストをプロクシしたい

Sun Jun 08 2025

背景

外部のAPIがGraphQLで動作していて、Next.jsのクライアントコンポーネントからそこにアクセスする場合を考えます。

もし外部のAPIが例えば何らかのAPIキーを要求する場合、クライアントコンポーネントから直接リクエストを飛ばすとAPIキーが見えてしまい、それを使って勝手にリクエストを送ることが可能になってしまいます。

よって、APIキーは秘匿にしたいわけです。

解決法

サーバーコンポーネントを使えばいいのですが、サーバーコンポーネントは遅いので、

クライアントコンポーネント→プロクシ→外部サーバーという手順を利用します。

src/
└── app/
    ├── api/
    │   └── graphql/
    │       └── route.ts
    └── chats/
        └── page.tsx

よくある感じだとこんな構成で、page.tsxからroute.tsを経由して外部APIにアクセスします。

もしGraphQLを利用する外部サーバーであればgraffleを使えばタイプセーフにリクエストを記述することができます。

https://github.com/graffle-js/grafflehttps://github.com/graffle-js/graffle

詰まった点

外部サーバーがデータを圧縮していたりする場合、レスポンスが正しくデコードできないことがありました。

import type { NextRequest } from 'next/server'

export async function POST(request: NextRequest) {
  const headers = new Headers(request.headers)
  headers.delete('accept-encoding')

  const body = await request.text()
  const response = await fetch('https://api.mito-shogi.com/graphql', {
    method: 'POST',
    headers,
    body
  })

  const resHeaders = new Headers(response.headers)
  resHeaders.delete('content-encoding')

  return new Response(await response.text(), {
    status: response.status,
    statusText: response.statusText,
    headers: resHeaders
  })
}

よって、現状ではroute.tsではエンコードを使わないようにリクエストしています。

ただ、これをやるとデータ通信量が増えてしまうので一長一短です。他に解決法がないか検討しています。

また、GraphQLClientを二重で使う方法も当然考えたのですが、GraphQLClientがレスポンスを自動でパースしてしまうため、パースが二重にかかってしまい正しくデータを受け取ることができませんでした。

二重対策

import { client } from '@/lib/client'
import { type NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const { query, variables } = await request.json()
  const response = await client.request(query, variables)
  return NextResponse.json(response)
}

こう定義したうえで、

import { GraphQLClient } from 'graphql-request'

export const client = new GraphQLClient(
  typeof window === 'undefined'
    ? process.env.NODE_ENV === 'development'
      ? 'http://host.docker.internal:8787/graphql'
      : 'https://example.com/graphql'
    : process.env.NODE_ENV === 'development'
      ? '/api/graphql'
      : '/api/graphql',
  {
    jsonSerializer:
      typeof window !== 'undefined'
        ? {
            parse: (v: unknown) => ({
              data: JSON.parse(v as string)
            }),
            // biome-ignore lint/suspicious/noExplicitAny: <explanation>
            stringify: (v: any) => JSON.stringify(v)
          }
        : undefined
  }
)

のように、パースする際にdataを付与してあげることにより正しくパースできるようになりました。

ただし、ステータスコードやエラーの原因がこのままだとちゃんと渡らないので、その場合はMiddlewareを使って対応して上げる必要がありそうです。