This site (gemilab.net) runs on Next.js App Router, which means Gemini API integration in Server Actions is something I deal with regularly. I've been building and shipping independent apps since 2013 — with over 50 million cumulative downloads across my app portfolio — and one constant across every stack is this: the faster you can narrow down a deployment-only bug, the more time you have for the actual work.
Here's a rundown of the specific error patterns I've run into as indie developer Masaki Hirokawa when calling Gemini API from Next.js Server Actions, along with the fixes that actually worked.
Error 1: Environment Variable Returns undefined in Server Actions
This is the most common confusion, and the fix is almost always the same.
Server Actions run server-side only. That means you don't need — and shouldn't use — the NEXT_PUBLIC_ prefix on your Gemini API key. The problem arises when developers set NEXT_PUBLIC_GEMINI_API_KEY thinking they need it both client and server-side. It technically works, but it exposes your API key in the client bundle.
// ❌ Works, but leaks your API key to the client bundle
'use server'
import { GoogleGenerativeAI } from '@google/generative-ai'
const genAI = new GoogleGenerativeAI(process.env.NEXT_PUBLIC_GEMINI_API_KEY ?? '')
// ✅ Server-only variable — no NEXT_PUBLIC_ prefix
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY ?? '')Keep .env.local clean:
# Server-only — Server Actions and Route Handlers
GEMINI_API_KEY=YOUR_API_KEY_HERE
# Client-accessible — non-sensitive values only
NEXT_PUBLIC_SITE_NAME=My AppOn Vercel, .env.local is not deployed automatically. If the error only appears after deployment, check that GEMINI_API_KEY is configured in the Vercel dashboard under Settings → Environment Variables. Across the multiple services I run in parallel, keeping staging and production environment variables separated from day one has saved me more than a few last-minute surprises.
Error 2: Streaming Doesn't Work from Server Actions
"I'm trying to use generateContentStream but it doesn't work in a Server Action" — this comes up constantly.
The underlying reason: Server Actions can only return JSON-serializable values. An AsyncGenerator or ReadableStream can't be serialized, so returning one directly causes either a type error or a silent runtime failure.
// ❌ Broken — AsyncGenerator is not serializable
'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 // This fails at runtime
}Two fixes work here.
Fix A: Use a Route Handler for streaming
Route Handlers handle streaming cleanly:
// 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' },
})
}Fix B: Use Vercel AI SDK's createStreamableValue
If you're already on the Vercel AI SDK, createStreamableValue wraps async generators into a serializable format that works with 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 }
}My default is to keep Gemini streaming in Route Handlers. It's simpler, easier to debug in production logs, and doesn't require pulling in the AI SDK.
Error 3: Internal Server Error on Deploy Only, Not Locally
This is the hardest to debug because it only surfaces after deployment. Three root causes cover most cases.
Cause A: SDK initialized at module level
When GoogleGenerativeAI is instantiated at the top of the file, it runs during module evaluation — before environment variables are guaranteed to be loaded:
// ❌ Risky: runs at module load time
import { GoogleGenerativeAI } from '@google/generative-ai'
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!)
export async function myAction(input: string) { ... }
// ✅ Safe: move initialization inside the function
export async function myAction(input: string) {
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY ?? '')
// ...
}Cause B: Serverless function timeout (Vercel)
On Vercel's free plan, functions time out after 10 seconds. Long Gemini prompts or large context windows can hit this. Set maxDuration in your Route Handler:
// Next.js 14.2+
export const maxDuration = 60 // seconds (requires Vercel Pro above 10)Cause C: Edge Runtime incompatibility
If your file has export const runtime = 'edge', the @google/generative-ai package will fail because it depends on Node.js APIs unavailable in Edge Runtime:
// ❌ Breaks Gemini API
export const runtime = 'edge'
// ✅ Remove it, or explicitly specify Node.js
export const runtime = 'nodejs'When to Use Server Actions vs Route Handlers
Running multiple projects simultaneously as an indie developer, here's the split I default to:
Use Server Actions for:
- Form submissions that trigger Gemini API calls
- One-shot generation tied to a user action (button click → generate → save to DB)
- Anything where you want Next.js to manage caching and revalidation automatically
Use Route Handlers for:
- Streaming responses from Gemini API to the client
- Endpoints consumed by external services or webhooks
- Shared API endpoints across multiple parts of the app
Locking in this split early prevents painful refactors later.
Debugging Checklist
When a Gemini API call breaks only in production:
- Check Vercel build logs — environment variable errors often surface here before runtime
- Check the Functions tab in Vercel for timeout duration and execution time
- Run
next startlocally (notnext dev) to reproduce production-like behavior - Add
console.errorinside the Server Action and check Vercel's runtime logs
The gap between next dev and production is larger than it looks. Running next start before every deploy that touches Gemini API integration will catch most surprises before they hit users.