●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Next.js 15 App Router × Gemini API: The Complete Full-Stack
Build production-grade full-stack AI applications with Next.js 15 App Router and the Gemini API. Covers Server Actions, Streaming, RAG pipelines, authentication, rate limiting, and deployment.
Why Next.js + Gemini API Is a Powerful Combination
Next.js 15 reached stable release in late 2025, bringing App Router's Server Components, Server Actions, and Partial Prerendering (PPR) to production readiness. When combined with Gemini 2.5 Pro, this stack is one of the most productive choices available for building AI-powered web applications today.
This guide goes far beyond a "hello world" tutorial. It's a comprehensive, practical walkthrough for building full-stack AI apps that can stand up to real production workloads. You should already be familiar with Next.js fundamentals and want to integrate Gemini API into a serious product.
Here's what we'll cover:
Next.js 15 App Router architecture and how it integrates with the Gemini API
Calling Gemini from Server Components and Server Actions
Full Streaming response implementation with Edge Runtime support
Building a RAG (Retrieval-Augmented Generation) pipeline
Authentication, rate limiting, and production error handling
# .env.localGEMINI_API_KEY=YOUR_GEMINI_API_KEY# Used automatically by @ai-sdk/googleGOOGLE_GENERATIVE_AI_API_KEY=YOUR_GEMINI_API_KEY
Critical: Never prefix GEMINI_API_KEY with NEXT_PUBLIC_. Doing so exposes your key to the browser. All Gemini API calls must happen server-side — in Server Components, Server Actions, or Route Handlers.
Initializing the Gemini Client (Singleton Pattern)
// src/lib/gemini.tsimport { GoogleGenerativeAI } from "@google/generative-ai";// Prevent duplicate instances during hot reload in developmentconst globalForGemini = globalThis as unknown as { geminiClient: GoogleGenerativeAI | undefined;};export const geminiClient = globalForGemini.geminiClient ?? new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);if (process.env.NODE_ENV !== "production") { globalForGemini.geminiClient = geminiClient;}// Helper to get a configured modelexport function getModel(modelName = "gemini-2.5-pro") { return geminiClient.getGenerativeModel({ model: modelName });}
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Master production-quality architecture combining Next.js 15 App Router Server Components and Server Actions with the Gemini API
✦Implement Streaming responses, RAG pipelines, and rate limiting with complete, working code examples
✦Learn proven deployment strategies for Vercel and Cloudflare Workers, plus API key management and cost optimization techniques
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Server Components let you fetch data and generate AI content within the same component, without shipping any of that logic to the client. You can invoke the Gemini API at build time or request time, then render the result statically or dynamically.
// src/app/summary/[slug]/page.tsximport { getModel } from "@/lib/gemini";import { getArticleContent } from "@/lib/articles";interface Props { params: { slug: string };}// This entire function runs on the server — no API key exposureasync function generateSummary(content: string): Promise<string> { const model = getModel("gemini-2.5-flash"); // Flash for cost savings const result = await model.generateContent({ contents: [ { role: "user", parts: [ { text: `Summarize the following article in 3–5 concise sentences, highlighting the key takeaways:\n\n${content}`, }, ], }, ], generationConfig: { maxOutputTokens: 512, temperature: 0.3, }, }); return result.response.text();}export default async function SummaryPage({ params }: Props) { const article = await getArticleContent(params.slug); const summary = await generateSummary(article.content); return ( <article> <h1>{article.title}</h1> {/* AI-generated summary */} <div className="bg-blue-50 p-4 rounded-lg"> <h2 className="font-bold text-blue-800 mb-2">AI Summary</h2> <p className="text-blue-700">{summary}</p> </div> <div dangerouslySetInnerHTML={{ __html: article.html }} /> </article> );}// ISR: regenerate every hour to control costsexport const revalidate = 3600;
Full Streaming Implementation
Streaming is the single biggest UX improvement you can make to an AI chat interface. The Gemini API supports streaming via Server-Sent Events, and Next.js's Edge Runtime pairs with it for minimal latency.
Streaming Route Handler with Vercel AI SDK
// src/app/api/chat/route.tsimport { google } from "@ai-sdk/google";import { streamText, convertToCoreMessages } from "ai";import { NextRequest } from "next/server";// Run on the edge for lower latencyexport const runtime = "edge";export async function POST(req: NextRequest) { const { messages } = await req.json(); const result = await streamText({ model: google("gemini-2.5-pro"), system: `You are a helpful, knowledgeable AI assistant.Provide accurate, clear answers in the user's language.When sharing code, always include a brief explanation.`, messages: convertToCoreMessages(messages), maxTokens: 2048, temperature: 0.7, onFinish: async ({ usage, finishReason }) => { // Log token usage to your database in production console.log("Tokens used:", usage.totalTokens); console.log("Finish reason:", finishReason); }, }); return result.toDataStreamResponse();}
Server Actions are central to Next.js 15's programming model. They let you invoke server-side functions — including Gemini API calls — directly from client components, almost as if they were regular function calls.
// src/app/actions/ai.ts"use server";import { getModel } from "@/lib/gemini";import { revalidatePath } from "next/cache";import { z } from "zod";const generateContentSchema = z.object({ topic: z.string().min(1).max(500), style: z.enum(["blog", "tweet", "email", "summary"]),});type GenerateContentInput = z.infer<typeof generateContentSchema>;export async function generateContent(input: GenerateContentInput) { const validated = generateContentSchema.safeParse(input); if (!validated.success) { return { error: "Invalid input", details: validated.error.flatten() }; } const { topic, style } = validated.data; const prompts: Record<string, string> = { blog: `Write an informative 800-word blog post about: "${topic}". Include a clear introduction, main points with subheadings, and a conclusion.`, tweet: `Write three engaging tweets (max 280 chars each) about: "${topic}". Make each one distinct in angle and tone.`, email: `Write a professional business email about: "${topic}". Include a subject line and a clear, courteous body.`, summary: `Summarize the following topic in under 100 words: "${topic}".`, }; try { const model = getModel("gemini-2.5-pro"); const result = await model.generateContent(prompts[style]); const content = result.response.text(); revalidatePath("/dashboard"); return { success: true, content }; } catch (error) { console.error("Gemini API error:", error); return { error: "Content generation failed. Please try again." }; }}
RAG (Retrieval-Augmented Generation) lets you ground Gemini's responses in your own data — product documentation, knowledge bases, or proprietary datasets. Here's a practical implementation using Gemini's Embeddings API.
// src/lib/rag.tsimport { geminiClient } from "@/lib/gemini";const embeddingModel = geminiClient.getGenerativeModel({ model: "text-embedding-004",});export async function embedText(text: string): Promise<number[]> { const result = await embeddingModel.embedContent({ content: { parts: [{ text }], role: "user" }, taskType: "RETRIEVAL_DOCUMENT", }); return result.embedding.values;}export async function embedQuery(query: string): Promise<number[]> { const result = await embeddingModel.embedContent({ content: { parts: [{ text: query }], role: "user" }, taskType: "RETRIEVAL_QUERY", }); return result.embedding.values;}export function cosineSimilarity(a: number[], b: number[]): number { const dot = a.reduce((sum, v, i) => sum + v * b[i], 0); const magA = Math.sqrt(a.reduce((s, v) => s + v * v, 0)); const magB = Math.sqrt(b.reduce((s, v) => s + v * v, 0)); return dot / (magA * magB);}interface Document { id: string; content: string; embedding?: number[]; metadata?: Record<string, unknown>;}// In-memory vector store — replace with pgvector or Pinecone in productionclass SimpleVectorStore { private docs: (Document & { embedding: number[] })[] = []; async add(doc: Document): Promise<void> { const embedding = await embedText(doc.content); this.docs.push({ ...doc, embedding }); } async search(query: string, topK = 3): Promise<Document[]> { const qEmbed = await embedQuery(query); return this.docs .map((d) => ({ d, score: cosineSimilarity(qEmbed, d.embedding) })) .sort((a, b) => b.score - a.score) .slice(0, topK) .map(({ d }) => d); }}export const vectorStore = new SimpleVectorStore();export async function ragAnswer( query: string, systemPrompt = "You are a helpful AI assistant. Answer based on the provided context."): Promise<string> { const relevant = await vectorStore.search(query, 3); if (relevant.length === 0) { return "No relevant information found for your query."; } const context = relevant .map((doc, i) => `[Document ${i + 1}]\n${doc.content}`) .join("\n\n"); const model = geminiClient.getGenerativeModel({ model: "gemini-2.5-pro" }); const prompt = `${systemPrompt}Use the following context to answer the user's question.If the answer isn't in the context, say so clearly.Context:${context}Question: ${query}`; const result = await model.generateContent(prompt); return result.response.text();}
Authentication, Rate Limiting, and Error Handling
Protecting your Gemini API endpoints is non-negotiable in production. Without it, you risk runaway API costs and abuse.
Middleware-Based Rate Limiting
// src/middleware.tsimport { NextResponse } from "next/server";import type { NextRequest } from "next/server";// Use Upstash Redis in production for distributed rate limitingconst requestCounts = new Map<string, { count: number; resetAt: number }>();function checkRateLimit(ip: string, maxReq = 10, windowMs = 60_000): boolean { const now = Date.now(); const current = requestCounts.get(ip); if (!current || current.resetAt < now) { requestCounts.set(ip, { count: 1, resetAt: now + windowMs }); return true; } if (current.count >= maxReq) return false; current.count++; return true;}export function middleware(request: NextRequest) { if (request.nextUrl.pathname.startsWith("/api/")) { const ip = request.headers.get("x-forwarded-for")?.split(",")[0] ?? "unknown"; if (!checkRateLimit(ip)) { return NextResponse.json( { error: "Rate limit exceeded. Please wait a minute and try again." }, { status: 429, headers: { "Retry-After": "60" } } ); } } return NextResponse.next();}export const config = { matcher: "/api/:path*" };
Production Deployment: Vercel and Cloudflare Workers
Deploying to Vercel (Recommended)
Vercel provides first-class Next.js support with zero configuration for Edge Runtime, Streaming, and serverless functions.
npm i -g vercelvercel# Set environment variables for productionvercel env add GEMINI_API_KEY productionvercel env add GOOGLE_GENERATIVE_AI_API_KEY production
text-embedding-004: Embeddings for RAG vector generation
Response Caching with Next.js
// src/lib/cached-gemini.tsimport { unstable_cache } from "next/cache";import { getModel } from "@/lib/gemini";// Cache identical inputs for 1 hour — huge savings for repeated queriesexport const cachedGenerateSummary = unstable_cache( async (content: string): Promise<string> => { const model = getModel("gemini-2.5-flash"); const result = await model.generateContent( `Summarize the following in three sentences or fewer:\n\n${content}` ); return result.response.text(); }, ["gemini-summary"], { revalidate: 3600, tags: ["gemini-cache"], });// Usageconst summary = await cachedGenerateSummary(articleContent);
For documents you query repeatedly, Context Caching can cut input token costs by up to 75%, since cached tokens are billed at a fraction of standard rates.
Multi-Turn Conversations with System Instructions
A single-turn request works for basic use cases, but production chatbots need persistent conversation state and carefully crafted system instructions that shape the model's personality, expertise, and constraints.
Designing Effective System Instructions
System instructions are the highest-leverage prompt engineering lever you have. They run once per conversation and set the context for every subsequent exchange.
// src/lib/system-prompts.ts// Generic helpful assistantexport const GENERAL_ASSISTANT = `You are a helpful, knowledgeable AI assistant built on Gemini.- Be concise but thorough — aim for clarity over length- When you're unsure, say so rather than guessing- Format code blocks with the appropriate language tag- Use bullet points and headers to structure long responses`;// Domain-specific: customer supportexport const CUSTOMER_SUPPORT = `You are a customer support specialist for Acme Corp.- Only answer questions related to our product line- For billing issues, always direct users to support@acme.com- Never promise refunds or credits without manager approval- If a user is frustrated, acknowledge their experience before troubleshooting- Keep responses under 200 words unless technical depth is required`;// Domain-specific: code reviewerexport const CODE_REVIEWER = `You are an expert code reviewer with deep knowledge of TypeScript, React, and Node.js.- Identify bugs, security vulnerabilities, and performance issues- Suggest specific, actionable improvements with code examples- Explain the "why" behind each recommendation- Prioritize issues by severity: critical > high > medium > low`;
Persistent Multi-Turn Chat with History Truncation
Long conversations eventually exceed the model's context window. A sliding-window approach keeps the conversation coherent without hitting token limits:
Testing AI-powered code presents unique challenges — responses are non-deterministic, and real API calls are slow and costly. Here's a pragmatic testing strategy.
Unit Testing with Mocked Gemini Responses
// src/lib/__tests__/gemini.test.tsimport { jest } from "@jest/globals";// Mock the entire SDK before importsjest.mock("@google/generative-ai", () => ({ GoogleGenerativeAI: jest.fn().mockImplementation(() => ({ getGenerativeModel: jest.fn().mockReturnValue({ generateContent: jest.fn().mockResolvedValue({ response: { text: () => "Mocked AI response for testing", }, }), }), })),}));// Now import your module — it will use the mockimport { getModel } from "@/lib/gemini";describe("Gemini integration", () => { it("should call generateContent with the correct prompt", async () => { const model = getModel("gemini-2.5-flash"); const result = await model.generateContent("Test prompt"); expect(result.response.text()).toBe("Mocked AI response for testing"); });});
Integration Testing with Recorded Responses
For more realistic tests, record real API responses and replay them during CI:
// src/lib/__tests__/rag.integration.test.ts// Run with: RECORD_MODE=true npm test to capture real responses// Then commit the fixtures and run without the flag in CIconst FIXTURES_PATH = "__fixtures__/rag-responses.json";async function getOrRecordResponse( prompt: string, key: string): Promise<string> { if (process.env.RECORD_MODE === "true") { const model = getModel("gemini-2.5-flash"); const result = await model.generateContent(prompt); const response = result.response.text(); // Save to fixtures file (simplified — use proper file handling in practice) console.log(`Record: ${key} =`, response); return response; } // Load from fixtures in normal mode const fixtures = require(FIXTURES_PATH); if (!fixtures[key]) throw new Error(`No fixture for key: ${key}`); return fixtures[key];}describe("RAG pipeline", () => { it("should return relevant answers from context", async () => { const answer = await getOrRecordResponse( "What is Next.js App Router?", "nextjs-app-router-definition" ); expect(answer.length).toBeGreaterThan(50); expect(answer.toLowerCase()).toContain("next"); });});
End-to-End Testing the Chat UI
// e2e/chat.spec.ts (Playwright)import { test, expect } from "@playwright/test";test("chat interface streams a response", async ({ page }) => { await page.goto("/chat"); // Type a message await page.getByPlaceholder("Type a message...").fill("What is 2 + 2?"); await page.getByRole("button", { name: "Send" }).click(); // Wait for streaming to complete (AI response appears) await expect(page.locator(".bg-gray-100").last()).toContainText("4", { timeout: 30_000, // AI responses can take time }); // Verify the loading indicator disappears await expect(page.locator(".animate-bounce").first()).not.toBeVisible();});
Monitoring and Observability
In production, you need visibility into latency, token usage, error rates, and cost trends. Here's a lightweight observability setup that doesn't require a third-party service.
Token Usage Tracking
// src/lib/usage-tracker.tsinterface UsageRecord { timestamp: Date; model: string; promptTokens: number; completionTokens: number; totalTokens: number; latencyMs: number; userId?: string;}// In production, replace with a database write (Postgres, Planetscale, etc.)const usageLog: UsageRecord[] = [];export function trackUsage(record: UsageRecord): void { usageLog.push(record); // Log to your observability platform (Datadog, Grafana, etc.) console.log( JSON.stringify({ event: "gemini_api_call", ...record, }) );}// Wrap your API calls with timingexport async function trackedGenerateContent( model: ReturnType<typeof import("@/lib/gemini").getModel>, prompt: string, userId?: string) { const start = Date.now(); try { const result = await model.generateContent(prompt); const usage = result.response.usageMetadata; trackUsage({ timestamp: new Date(), model: "gemini-2.5-pro", promptTokens: usage?.promptTokenCount ?? 0, completionTokens: usage?.candidatesTokenCount ?? 0, totalTokens: usage?.totalTokenCount ?? 0, latencyMs: Date.now() - start, userId, }); return result; } catch (error) { console.error("Gemini API error after", Date.now() - start, "ms:", error); throw error; }}
Simple Admin Dashboard for Token Usage
// src/app/api/admin/usage/route.tsimport { NextRequest, NextResponse } from "next/server";import { getServerSession } from "next-auth";export async function GET(req: NextRequest) { // Protect this endpoint — only allow admin users const session = await getServerSession(); if (!session || session.user?.role !== "admin") { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } // In production, query your database // const usage = await db.query("SELECT * FROM usage_log WHERE timestamp > NOW() - INTERVAL '24 hours'"); return NextResponse.json({ message: "Usage data endpoint — connect to your database here", periodHours: 24, });}
Partial Prerendering: Mixing Static and Dynamic AI Content
PPR in Next.js 15 lets you serve a static shell instantly while streaming AI-generated content into the page progressively.
// src/app/dashboard/page.tsximport { Suspense } from "react";import { AiInsights } from "@/components/AiInsights";import { StaticHeader } from "@/components/StaticHeader";// Opt into PPR for this routeexport const experimental_ppr = true;export default function DashboardPage() { return ( <main> {/* Rendered immediately from CDN cache */} <StaticHeader /> {/* Streamed in dynamically after initial page load */} <Suspense fallback={<div className="animate-pulse h-32 bg-gray-100 rounded" />}> <AiInsights /> </Suspense> </main> );}
// next.config.tsimport type { NextConfig } from "next";const nextConfig: NextConfig = { experimental: { ppr: true, },};export default nextConfig;
Looking back
In this guide, we covered everything you need to ship a production-grade AI application with Next.js 15 App Router and Gemini API:
Initializing the Gemini client with a singleton pattern safe for Next.js hot-reload
Calling Gemini from Server Components and Server Actions without exposing API keys
Building a full Streaming chat with the Vercel AI SDK and useChat
Implementing a lightweight RAG pipeline using Gemini Embeddings
Protecting API routes with middleware rate limiting and unified error handling
Deploying to Vercel and Cloudflare Workers
Cutting costs with model selection, response caching, and Context Caching
Next.js combined with Gemini is a compelling foundation for AI-first products. The patterns in this guide scale from a weekend prototype to a production SaaS. If you want to go deeper on the React layer, the Gemini API React AI Chat UI guide is a great next step.
Share
Thank You for Reading
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.