「ローカルでは動くのにデプロイ後だけ壊れる」——この一言で何時間も溶かしてきた開発者は少なくないはずです。
このサイト(gemilab.net)自体を Next.js App Router で構築しているので、Gemini API との連携は私が日常的に触っているテーマです。廣川政樹として 2013 年にスマートフォンアプリ個人開発を始めて以来、累計 5,000 万 DL を超えるアプリ事業を一人で運営しながら複数の Web サービスも同時に動かしていると、デバッグに費やす時間の短縮が生産性を直接左右することを痛感します。
Server Actions から Gemini API を呼び出す際に繰り返し踏んだパターンを整理しておきます。環境変数の undefined、ストリーミングの非互換、Edge Runtime との衝突——それぞれ原因と対処をまとめました。
エラー1:環境変数が Server Actions 内で undefined になる
最もよく見かけるのが、環境変数の参照ミスです。Server Actions はサーバーサイドで実行されます。つまり、NEXT_PUBLIC_ プレフィックス付きの環境変数を使う必要はありません。しかし「クライアントコンポーネントでも使いたいから NEXT_PUBLIC_ を付けた」変数を Server Actions 内でも参照しようとすると混乱が生じます。
// ❌ 動くが API キーがクライアントバンドルに露出してしまう
'use server'
import { GoogleGenerativeAI } from '@google/generative-ai'
const genAI = new GoogleGenerativeAI(process.env.NEXT_PUBLIC_GEMINI_API_KEY ?? '')
// ✅ サーバー専用変数を使う(NEXT_PUBLIC_ なし)
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY ?? '').env.local は以下のように整理するのが安全です。サーバー専用の変数(GEMINI_API_KEY)とクライアントから参照可能な変数(NEXT_PUBLIC_)を明確に分けてください。
# サーバー専用(Server Actions・Route Handlers のみ)
GEMINI_API_KEY=YOUR_API_KEY_HERE
# クライアントでも使いたい情報のみ(API キーは含めない)
NEXT_PUBLIC_SITE_NAME=Gemini LabVercel などのデプロイ環境では .env.local がそのままデプロイされないため、ダッシュボードから環境変数を手動設定する必要があります。「ローカルでは動くのにデプロイ後に GEMINI_API_KEY is undefined と出る」場合は、まず Vercel の Settings → Environment Variables を確認してください。私自身、ステージング環境と本番環境で環境変数の設定を分けて管理するようにしてから、この種のトラブルはほぼなくなりました。
エラー2:Server Actions からのストリーミングが動かない
「Gemini の generateContentStream を使いたいのに、Server Actions では上手く受け取れない」という相談をよく目にします。これは Next.js の仕様上の制限です。Server Actions は JSON シリアライズ可能な値しか返せません。AsyncGenerator や ReadableStream をそのまま返そうとすると、型エラーまたはランタイムエラーになります。
// ❌ AsyncGenerator はシリアライズ不可のため失敗する
'use server'
export async function generateWithStream(prompt: string) {
const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' })
const result = await model.generateContentStream(prompt)
return result.stream // ← シリアライズできないため失敗する
}解決策は2つあります。
解決策 A:Route Handler に切り替える
ストリーミングが必要な場合は、Server Actions ではなく Route Handler(/app/api/generate/route.ts)を使うのが最もシンプルです。ReadableStream をそのまま Response に渡せるので、Gemini のチャンクをリアルタイムにクライアントへ流せます。
// app/api/generate/route.ts
import { GoogleGenerativeAI } from '@google/generative-ai'
import { NextRequest } from 'next/server'
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY ?? '')
export async function POST(req: NextRequest) {
const { prompt } = await req.json()
const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' })
const result = await model.generateContentStream(prompt)
const stream = new ReadableStream({
async start(controller) {
for await (const chunk of result.stream) {
controller.enqueue(new TextEncoder().encode(chunk.text()))
}
controller.close()
},
})
return new Response(stream, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
})
}解決策 B:Vercel AI SDK の createStreamableValue を使う
Vercel AI SDK を導入している場合は createStreamableValue を使えます。これは AsyncGenerator をシリアライズ可能な形式にラップするユーティリティで、Server Actions からでも利用できます。
'use server'
import { createStreamableValue } from 'ai/rsc'
import { GoogleGenerativeAI } from '@google/generative-ai'
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY ?? '')
export async function generateText(prompt: string) {
const stream = createStreamableValue('')
const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' })
;(async () => {
const result = await model.generateContentStream(prompt)
for await (const chunk of result.stream) {
stream.update(chunk.text())
}
stream.done()
})()
return { output: stream.value }
}個人的には、Gemini API のストリーミングには Route Handler を使うほうが、コードがシンプルで本番環境のログも追いやすいと感じています。Server Actions はストリーミングを伴わない処理(フォーム送信後の生成、バックグラウンドでのコンテンツ作成など)に向いています。
エラー3:ローカルでは動くのにデプロイ後だけ出る Internal Server Error
これが最も原因特定に時間がかかるパターンです。典型的な原因は3つあります。
原因 A:SDK のインスタンス化をモジュールのトップレベルで行っている
GoogleGenerativeAI のインスタンス生成をファイルのトップレベルに置くと、モジュール評価時に実行されます。特定の環境では環境変数がまだ読み込まれていないケースがあり、ここで失敗します。関数スコープ内に移動するだけで解決します。
// ❌ モジュールロード時に実行されるリスクがある
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!)
export async function myAction(input: string) { ... }
// ✅ 関数スコープ内に移動して安全に初期化
export async function myAction(input: string) {
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY ?? '')
// ...
}原因 B:Vercel のサーバーレス関数タイムアウト
Gemini API への呼び出しが長いプロンプトや大きなコンテキストで遅延した場合、Vercel Free プランの 10 秒タイムアウトに引っかかります。Route Handler に maxDuration を設定することで上限を引き上げられます(Pro プランが必要)。
// Route Handler の場合(Next.js 14.2+)
export const maxDuration = 60 // 秒原因 C:Edge Runtime との非互換
export const runtime = 'edge' が残っているファイルでは、@google/generative-ai が利用する Node.js API が Edge Runtime で動きません。この設定を削除するか、明示的に Node.js runtime を指定してください。
// ❌ これがあると Gemini API が動かない
export const runtime = 'edge'
// ✅ 削除するか Node.js runtime を明示
export const runtime = 'nodejs'Route Handler と Server Actions の使い分け基準
複数のプロジェクトを同時に運営する中で、次の基準が使いやすいと感じています。
Server Actions が向いているのは、フォーム送信後に Gemini API を呼んで結果をデータベースに保存する処理や、ボタン操作に連動した一回完結の生成処理です。Next.js のキャッシュ・再検証の仕組みとも相性が良いです。
Route Handler が向いているのは、Gemini API のストリーミングレスポンスをクライアントに流す場合や、外部サービスのウェブフックを受け取る場合です。また、アプリの複数の箇所から共有して使える API エンドポイントを設けたいときにも Route Handler のほうが管理しやすいです。
この判断を設計初期に固めておくだけで、後からの変更コストが大幅に減ります。アーティスト活動と並行して Web サービスを運営している私自身にとって、設計段階の小さな判断が後工程の手間を左右するという実感は日々あります。
デバッグ手順の優先順位
ローカルで再現しないエラーを追うときは、以下の順序で確認するのが効率的です。まず Vercel のビルドログを確認します。環境変数のエラーはランタイムではなくビルド時点で出ることが多いためです。次に Functions タブでタイムアウトと実行時間を確認します。ローカルでは瞬時に返ってくる処理も、Vercel の環境では遅延することがあります。
それでも再現しない場合は next start(next dev ではなく)をローカルで実行してみてください。next dev は開発用の最適化が入っており、本番との差異が出やすいです。next start は本番ビルドに近い状態で動作するため、デプロイ後のエラーをローカルで再現しやすくなります。
同じ課題に取り組んでいる方の参考になれば幸いです。
よくある追加の落とし穴
Server Actions 内で 'use client' ディレクティブを持つコンポーネントから直接インポートされた関数を呼ぼうとすると、予期せぬエラーが出ることがあります。'use server' と 'use client' の境界は Next.js の内部で明確に管理されており、その境界をまたぐ呼び出し方は制限されています。
また、Server Actions を使って Gemini API に画像を送る場合も注意が必要です。ブラウザから受け取った File オブジェクトや Blob は、Server Actions を通じてそのまま Gemini API に渡せません。ArrayBuffer または Uint8Array に変換したうえで inlineData 形式にまとめる必要があります。この変換処理を忘れると、invalid argument: image data のようなエラーが静かに発生します。
複数のサービスを並行して開発していると、環境変数の命名規則が混在しがちです。プロジェクト初期から .env.local.example ファイルを作成し、どの変数が NEXT_PUBLIC_ を必要とするか、どの変数がサーバー専用かを明記しておくと、後からチームに加わった人にも分かりやすい構成になります。
エラーが出た際は、まずエラーメッセージをそのまま検索するのが最短ルートです。Gemini API のエラーコードには、対処法が比較的明確に記載されているものが多いです。それでも解決しない場合は、@google/generative-ai パッケージのバージョンを確認してください。マイナーバージョンのアップデートで API の挙動が変わることがあります。