
Nano Banana Builder
- 16 installs
- 139 repo stars
- Updated December 25, 2025
- chongdashu/cc-skills-nanobananapro
Build full-stack Next.js web apps powered by Google Gemini Nano Banana image generation APIs, including editors and galleries.
About
Guides building production image-generation web apps around gemini-2.5-flash-image and gemini-3-pro-image-preview, covering components, server actions, storage, and rate limiting. Used when a developer integrates Nano Banana image APIs into a Next.js app.
- Exact Gemini image model names and pitfalls
- Conversational, multi-turn image generation patterns
Nano Banana Builder by the numbers
- 16 all-time installs (skills.sh)
- Ranked #1,021 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/chongdashu/cc-skills-nanobananapro --skill nano-banana-builderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 16 |
|---|---|
| repo stars | ★ 139 |
| Last updated | December 25, 2025 |
| Repository | chongdashu/cc-skills-nanobananapro ↗ |
What it does
Build full-stack Next.js web apps powered by Google Gemini Nano Banana image generation APIs, including editors and galleries.
Files
Nano Banana Builder
Build production-ready web applications powered by Google's Nano Banana image generation APIs—creating everything from simple text-to-image generators to sophisticated iterative editors with multi-turn conversation.
---
CRITICAL: Exact Model Names
Use ONLY these exact model strings. Do not invent, guess, or add date suffixes.
| Model String (use exactly) | Alias | Use Case |
|---|---|---|
gemini-2.5-flash-image | Nano Banana | Fast iterations, drafts, high volume |
gemini-3-pro-image-preview | Nano Banana Pro | Quality output, text rendering, 2K |
Common mistakes to avoid:
- ❌
gemini-2.5-flash-preview-05-20— wrong, date suffixes are for text models - ❌
gemini-2.5-pro-image— wrong, 2.5 Pro doesn't do image generation - ❌
gemini-3-flash-image— wrong, doesn't exist - ❌
gemini-pro-vision— wrong, that's for image input, not generation
The only valid image generation models are `gemini-2.5-flash-image` and `gemini-3-pro-image-preview`.
---
Philosophy: Conversational Image Generation
Nano Banana isn't just another image API—it's conversational by design. The core insight is that image generation works best as a dialogue, not a one-shot prompt.
Think of it as working with an AI art director:
- Iterative refinement → Build up images through conversation, not perfection in one prompt
- Context awareness → The model "remembers" previous generations and edits
- Natural language editing → Describe changes conversationally, not with parameters
Before Building, Ask
- What's the primary use case? Text-to-image generation? Image editing? Multi-image composition? Style transfer?
- Which model fits the need? Nano Banana (speed/iterations) or Nano Banana Pro (quality/complex prompts)?
- What's the user journey? Single generation? Iterative refinement? Gallery browsing?
- What are production constraints? Rate limits? Storage? Cost per image? User volume?
Core Principles
1. Conversation over configuration: Leverage Nano Banana's iterative editing rather than complex parameter UIs 2. Model selection matters: Use gemini-2.5-flash-image for speed/iterations, gemini-3-pro-image-preview for quality/complexity 3. State as conversation history: Track generations as chat messages to enable multi-turn editing 4. Rate limit awareness: Image generation has strict quotas—implement queuing and caching 5. Storage strategy: Store generated images (Vercel Blob/S3), not just inline base64
Model Selection Framework
Choose based on use case:
| Use Case | Model | Why |
|---|---|---|
| Rapid iterations, drafts | gemini-2.5-flash-image | Fast (2-5s), lower cost per image |
| Final output, quality | gemini-3-pro-image-preview | Superior quality, thinking, text rendering |
| Text-heavy images | gemini-3-pro-image-preview | Best typography, 2K resolution |
| Multi-turn editing | Either | Both support conversational editing |
| High volume | gemini-2.5-flash-image | Lower cost, faster throughput |
---
Quick Start
Basic Server Action
// app/actions/generate.ts
'use server'
import { google } from '@ai-sdk/google'
import { generateText } from 'ai'
export async function generateImage(prompt: string) {
const result = await generateText({
model: google('gemini-2.5-flash-image'),
prompt,
providerOptions: {
google: {
responseModalities: ['IMAGE'],
imageConfig: { aspectRatio: '16:9' }
}
}
})
return result.files[0] // { base64, uint8Array, mediaType }
}Client Component with useChat
// app/components/ImageGenerator.tsx
'use client'
import { useChat } from '@ai-sdk/react'
export function ImageGenerator() {
const { append, messages, isLoading } = useChat({
api: '/api/generate'
})
return (
<div>
{messages.map(m => (
<div key={m.id}>
{m.parts?.map((part, i) =>
part.type === 'image' && (
<img key={i} src={part.url} alt="Generated" />
)
)}
</div>
))}
<button
disabled={isLoading}
onClick={() => append({
role: 'user',
content: 'A futuristic cityscape at dusk'
})}
>
Generate
</button>
</div>
)
}---
Advanced Implementation
For complete implementations including:
- Server Actions with model selection, storage, and error handling
- API Routes with streaming responses
- Client Components with iterative editing and galleries
- Advanced Patterns like multi-image composition and batch generation
See references/advanced-patterns.md
---
Configuration & Operations
For detailed configuration and operational concerns:
- Provider Options (responseModalities, imageConfig, thinkingConfig)
- Storage Strategy (Vercel Blob, S3/R2 implementations)
- Rate Limiting (Upstash Redis patterns, quota management)
- Cost Optimization strategies
See references/configuration.md
---
Anti-Patterns to Avoid
❌ Inventing model names or adding date suffixes: Why wrong: Image generation models have specific names; date suffixes like -preview-05-20 are for text models only Better: Use exactly gemini-2.5-flash-image or gemini-3-pro-image-preview — no variations
❌ Using Gemini 2.5 Pro for images: Why wrong: Gemini 2.5 Pro doesn't generate images directly Better: Use gemini-2.5-flash-image or gemini-3-pro-image-preview
❌ Storing only base64 in database: Why wrong: Blobs database, expensive storage, slow retrieval Better: Store in object storage (Vercel Blob/S3), save URL only
❌ No rate limit handling: Why wrong: Will hit 429 errors in production, poor UX Better: Implement rate limiting with user-friendly error messages
❌ Ignoring multi-turn context: Why wrong: Wastes Nano Banana's conversational editing strength Better: Track chat history for iterative refinement
❌ Hardcoding API keys client-side: Why wrong: Exposes credentials, security risk Better: Use server actions / API routes with environment variables
❌ Using wrong aspect ratio: Why wrong: 21:9 on 1:1 request wastes tokens, unexpected crop Better: Match aspect ratio to intended use case
❌ No loading states: Why wrong: Image generation takes 5-30s, users think it's broken Better: Show progress indicators and estimated wait time
❌ Generating on every keystroke: Why wrong: Wastes quota, slow response Better: Debounce prompts, require explicit action
---
Variation Guidance
IMPORTANT: Every app should feel uniquely designed for its specific purpose.
Vary across dimensions:
- UI Style: Minimal, brutalist, playful, professional, dark, light
- Color Scheme: Warm, cool, monochrome, vibrant, muted
- Layout: Single page, multi-step wizard, sidebar, grid, list
- Interaction: Click-to-generate, drag-and-drop, real-time typing, batch
Avoid overused patterns:
- ❌ Default Tailwind purple gradients
- ❌ Generic "AI startup" aesthetic
- ❌ Same component libraries for every project
- ❌ Inter/Roboto fonts without thought
Context should drive design:
- Meme generator → Bold, fun, casual
- Product mockup tool → Clean, professional, grid-based
- Art exploration → Gallery-first, visual-heavy
- Brand asset creator → Polished, template-guided
---
Environment Setup
# .env.local
GEMINI_API_KEY=your_api_key_here
# For Vercel Blob storage
BLOB_READ_WRITE_TOKEN=your_vercel_token
# For S3 (optional)
S3_BUCKET=your-bucket
S3_ENDPOINT=https://your-endpoint.r2.cloudflarestorage.com
S3_ACCESS_KEY_ID=your_key
S3_SECRET_ACCESS_KEY=your_secret
# For Upstash rate limiting (optional)
UPSTASH_REDIS_REST_URL=your_url
UPSTASH_REDIS_REST_TOKEN=your_token# Install dependencies
npm install @ai-sdk/google ai @ai-sdk/react @vercel/blob
# Or if using separate packages
npm install google-genai---
Remember
Nano Banana enables conversational image generation that feels like working with a creative partner, not a tool.
The best apps:
- Leverage multi-turn editing for refinement
- Choose models intentionally (speed vs quality)
- Handle rate limits gracefully
- Store images efficiently
- Provide great loading states
- Feel uniquely designed for their purpose
You're building more than an image generator—you're creating a creative experience. Design it thoughtfully.
Advanced Patterns for Nano Banana Builder
Complete implementations for production-ready Nano Banana web applications.
Server Actions
Image Generation with Model Selection
// app/actions/generate.ts
'use server'
import { google } from '@ai-sdk/google'
import { generateText } from 'ai'
import { put } from '@vercel/blob'
interface GenerateConfig {
prompt: string
model: 'nano' | 'pro'
aspectRatio?: '1:1' | '16:9' | '21:9'
storeImage?: boolean
}
export async function generateImage(config: GenerateConfig) {
const { prompt, model, aspectRatio = '1:1', storeImage = true } = config
const modelName = model === 'pro'
? 'gemini-3-pro-image-preview'
: 'gemini-2.5-flash-image'
const result = await generateText({
model: google(modelName),
prompt,
providerOptions: {
google: {
responseModalities: ['IMAGE'],
imageConfig: {
aspectRatio,
...(model === 'pro' && { imageSize: '2K' })
}
}
}
})
const imageFile = result.files[0]
if (storeImage && imageFile?.base64) {
const buffer = Buffer.from(imageFile.base64, 'base64')
const blob = await put(`generated/${Date.now()}.png`, buffer, {
access: 'public'
})
return { url: blob.url, base64: imageFile.base64 }
}
return { url: `data:${imageFile.mediaType};base64,${imageFile.base64}` }
}Iterative Editing (Multi-Turn)
// app/actions/edit.ts
'use server'
import { google } from '@ai-sdk/google'
import { generateText } from 'ai'
interface EditConfig {
imageBase64: string
editPrompt: string
model: 'nano' | 'pro'
history?: Array<{role: string; content: any}>
}
export async function editImage(config: EditConfig) {
const { imageBase64, editPrompt, model, history = [] } = config
const modelName = model === 'pro'
? 'gemini-3-pro-image-preview'
: 'gemini-2.5-flash-image'
// Build conversation with image as first message
const contents = [
{ role: 'user', content: [
{ type: 'image', image: imageBase64 },
{ type: 'text', text: editPrompt }
]}
]
const result = await generateText({
model: google(modelName),
messages: [...history, ...contents],
providerOptions: {
google: {
responseModalities: ['IMAGE']
}
}
})
return {
url: `data:${result.files[0].mediaType};base64,${result.files[0].base64}`,
newHistory: [...history, ...contents, {
role: 'assistant',
content: result.files[0]
}]
}
}API Route with Streaming
// app/api/generate/route.ts
import { google } from '@ai-sdk/google'
import { streamText } from 'ai'
export const maxDuration = 30
export async function POST(req: Request) {
const { prompt, model = 'nano' } = await req.json()
const result = streamText({
model: google(model === 'pro' ? 'gemini-3-pro-image-preview' : 'gemini-2.5-flash-image'),
prompt,
providerOptions: {
google: {
responseModalities: ['IMAGE', 'TEXT']
}
}
})
return result.toDataStreamResponse()
}---
Client-Side Components
Complete Image Generator Component
// app/components/ImageGenerator.tsx
'use client'
import { useState } from 'react'
import { useChat } from '@ai-sdk/react'
type Model = 'nano' | 'pro'
export function ImageGenerator() {
const [selectedModel, setSelectedModel] = useState<Model>('nano')
const [prompt, setPrompt] = useState('')
const { messages, append, isLoading } = useChat({
api: '/api/generate',
body: { model: selectedModel }
})
const handleGenerate = (e: React.FormEvent) => {
e.preventDefault()
if (!prompt.trim()) return
append({
role: 'user',
content: prompt,
// @ts-ignore - custom body property
model: selectedModel
})
setPrompt('')
}
return (
<div className="max-w-2xl mx-auto p-6">
{/* Model Selector */}
<div className="flex gap-2 mb-4">
<button
onClick={() => setSelectedModel('nano')}
className={`px-4 py-2 rounded ${selectedModel === 'nano'
? 'bg-blue-500 text-white'
: 'bg-gray-200'}`}
>
Nano (Fast)
</button>
<button
onClick={() => setSelectedModel('pro')}
className={`px-4 py-2 rounded ${selectedModel === 'pro'
? 'bg-blue-500 text-white'
: 'bg-gray-200'}`}
>
Pro (Quality)
</button>
</div>
{/* Generated Images Gallery */}
<div className="grid grid-cols-2 gap-4 mb-6">
{messages.map((m, i) =>
m.parts?.map((part, j) =>
part.type === 'image' && (
<img
key={`${i}-${j}`}
src={part.url}
alt="Generated"
className="w-full rounded-lg shadow"
/>
)
)
)}
</div>
{/* Prompt Input */}
<form onSubmit={handleGenerate} className="flex gap-2">
<input
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe your image..."
className="flex-1 px-4 py-2 border rounded"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !prompt.trim()}
className="px-6 py-2 bg-purple-500 text-white rounded disabled:opacity-50"
>
{isLoading ? 'Generating...' : 'Generate'}
</button>
</form>
</div>
)
}Iterative Editor Component
// app/components/IterativeEditor.tsx
'use client'
import { useState } from 'react'
interface EditHistory {
role: string
content: any
}
export function IterativeEditor() {
const [currentImage, setCurrentImage] = useState<string>('')
const [editPrompt, setEditPrompt] = useState('')
const [history, setHistory] = useState<EditHistory[]>([])
const [isLoading, setIsLoading] = useState(false)
const handleEdit = async () => {
if (!editPrompt.trim() || !currentImage) return
setIsLoading(true)
const response = await fetch('/api/edit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
imageBase64: currentImage.split(',')[1],
editPrompt,
history
})
})
const data = await response.json()
setCurrentImage(data.url)
setHistory(data.newHistory)
setEditPrompt('')
setIsLoading(false)
}
return (
<div className="flex flex-col lg:flex-row gap-6">
{/* Image Display */}
<div className="flex-1">
{currentImage ? (
<img src={currentImage} alt="Current" className="w-full rounded" />
) : (
<div className="aspect-square bg-gray-100 rounded flex items-center justify-center">
Upload or generate an image to start
</div>
)}
</div>
{/* Edit Controls */}
<div className="flex-1">
<textarea
value={editPrompt}
onChange={(e) => setEditPrompt(e.target.value)}
placeholder="Describe your edit..."
className="w-full h-32 p-3 border rounded mb-4"
/>
<button
onClick={handleEdit}
disabled={isLoading || !editPrompt.trim()}
className="w-full py-2 bg-green-500 text-white rounded disabled:opacity-50"
>
{isLoading ? 'Editing...' : 'Apply Edit'}
</button>
{/* History */}
<div className="mt-4">
<h3 className="font-bold mb-2">Edit History</h3>
{history.slice(-5).map((h, i) => (
<div key={i} className="text-sm text-gray-600 py-1">
{h.role}: {typeof h.content === 'string'
? h.content
: JSON.stringify(h.content).substring(0, 50)}
</div>
))}
</div>
</div>
</div>
)
}---
Advanced Patterns
Multi-Image Composition
// Combine multiple images into one generation
export async function compositeImages(
images: string[],
prompt: string
) {
const imageParts = images.map(img => ({
inlineData: {
mimeType: 'image/png',
data: img.split(',')[1]
}
}))
const result = await generateText({
model: google('gemini-3-pro-image-preview'),
messages: [{
role: 'user',
content: [...imageParts, { text: prompt }]
}],
providerOptions: {
google: { responseModalities: ['IMAGE'] }
}
})
return result.files[0]
}Batch Generation with Progress
// app/actions/batch.ts
export async function generateBatch(
prompts: string[],
onProgress?: (current: number, total: number) => void
) {
const results = []
for (let i = 0; i < prompts.length; i++) {
const result = await generateImage({
prompt: prompts[i],
model: 'nano',
storeImage: true
})
results.push(result)
onProgress?.(i + 1, prompts.length)
}
return results
}Progressive Loading
// Generate low-res first, then high-res
export async function generateProgressive(prompt: string) {
// Fast preview
const preview = await generateImage({
prompt,
model: 'nano',
storeImage: false
})
// High-res final
const final = await generateImage({
prompt,
model: 'pro',
storeImage: true
})
return { preview, final }
}---
Usage Patterns
Gallery with Infinite Scroll
// app/components/ImageGallery.tsx
'use client'
import { useState, useEffect } from 'react'
import { useChat } from '@ai-sdk/react'
export function ImageGallery() {
const { messages, append, isLoading } = useChat({
api: '/api/generate'
})
const images = messages.flatMap(m =>
m.parts?.filter(p => p.type === 'image') ?? []
)
return (
<div className="grid grid-cols-3 gap-4">
{images.map((part, i) => (
<div key={i} className="aspect-square">
<img src={part.url} alt="" className="w-full h-full object-cover rounded" />
</div>
))}
</div>
)
}Error Handling with Retry
// app/actions/generate.ts
export async function generateImageWithRetry(
config: GenerateConfig,
maxRetries = 3
) {
for (let i = 0; i < maxRetries; i++) {
try {
return await generateImage(config)
} catch (error) {
if (i === maxRetries - 1) throw error
await new Promise(r => setTimeout(r, 1000 * (i + 1)))
}
}
}Configuration & Operations for Nano Banana Builder
Complete reference for provider options, storage, rate limiting, and cost management.
Provider Options Reference
Response Modalities
providerOptions: {
google: {
responseModalities: ['IMAGE'], // Images only (saves tokens)
responseModalities: ['TEXT', 'IMAGE'], // Both (default)
}
}When to use each:
['IMAGE']- When you only need images, saves token costs['TEXT', 'IMAGE']- When you want both descriptions and images
Image Configuration
providerOptions: {
google: {
imageConfig: {
aspectRatio: '1:1', // 1:1, 16:9, 21:9, 4:3, 3:4, 9:16, etc.
imageSize: '2K' // Pro only: 1K, 2K (default for Pro is 2K)
}
}
}Aspect Ratio Guide
| Ratio | Resolution | Best For | Token Cost |
|---|---|---|---|
| 1:1 | 1024×1024 | Icons, squares, Instagram | Lowest |
| 16:9 | 1344×768 | YouTube thumbnails, widescreen | Medium |
| 21:9 | 1536×672 | Cinematic, ultra-wide | Higher |
| 4:3 | 1184×864 | Presentations, standard | Medium |
| 9:16 | 768×1344 | TikTok, Reels, Stories | Medium |
Thinking Configuration (Pro Only)
providerOptions: {
google: {
thinkingConfig: {
thinkingBudget: 8192, // Controls reasoning depth (tokens)
includeThoughts: false // Return thinking process in response
}
}
}Thinking Budget Guidelines:
4096- Quick generations, simple prompts8192- Default balance16384- Complex compositions, detailed edits32768- Maximum reasoning for challenging requests
---
Storage Strategy
Vercel Blob (Recommended for Vercel Deploy)
// lib/storage/vercel-blob.ts
import { put } from '@vercel/blob'
export async function storeImage(
imageBase64: string,
userId: string
) {
const buffer = Buffer.from(imageBase64, 'base64')
const blob = await put(
`images/${userId}/${Date.now()}.png`,
buffer,
{
access: 'public',
token: process.env.BLOB_READ_WRITE_TOKEN
}
)
return blob.url
}
export async function storeImageWithMetadata(
imageBase64: string,
userId: string,
metadata: { prompt: string; model: string }
) {
const buffer = Buffer.from(imageBase64, 'base64')
const filename = `${Date.now()}-${metadata.model}.png`
const blob = await put(
`images/${userId}/${filename}`,
buffer,
{
access: 'public',
token: process.env.BLOB_READ_WRITE_TOKEN,
addMetadata: {
prompt: metadata.prompt,
model: metadata.model,
createdAt: new Date().toISOString()
}
}
)
return { url: blob.url, filename }
}S3 / R2 (Universal)
// lib/storage/s3.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
const s3 = new S3Client({
region: 'auto',
endpoint: process.env.S3_ENDPOINT,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID!,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!
}
})
export async function storeImageS3(
imageBase64: string,
userId: string
) {
const buffer = Buffer.from(imageBase64, 'base64')
const key = `${userId}/${Date.now()}.png`
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
Body: buffer,
ContentType: 'image/png',
Metadata: {
createdAt: new Date().toISOString()
}
}))
return `${process.env.S3_PUBLIC_URL}/${key}`
}
// With presigned URLs for private access
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { GetObjectCommand } from '@aws-sdk/client-s3'
export async function getPrivateImageURL(key: string) {
const command = new GetObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key
})
return await getSignedUrl(s3, command, { expiresIn: 3600 })
}Base64 Fallback (Development Only)
// Only for development - do not use in production
export function base64DataURL(base64: string, mediaType = 'image/png') {
return `data:${mediaType};base64,${base64}`
}---
Rate Limiting & Cost Management
Understanding Quotas
Free Tier:
- Nano Banana: ~100 RPD (requests per day), 15 RPM (requests per minute)
- Nano Banana Pro: ~10 RPD, 5-10 RPM
Paid Tier (varies by spend):
- Tier 1: 500+ RPM
- Tier 2: 1,000+ RPM
- Tier 3: 2,000+ RPM
Cost Estimates:
- ~1,290 tokens per image (varies by complexity)
- ~$0.039 per image (varies by model and region)
Rate Limit Handler (Upstash Redis)
// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit'
import { Redis } from '@upstash/redis'
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 requests per minute
analytics: true,
prefix: 'nano-banana'
})
export async function checkRateLimit(userId: string) {
const { success, limit, reset, remaining } = await ratelimit.limit(userId)
if (!success) {
throw new Error(
`Rate limited. Resets at ${new Date(reset).toISOString()}. ` +
`Please wait before generating more images.`
)
}
return { limit, remaining }
}
// Usage in server action
export async function generateImage(config: GenerateConfig) {
// Get user ID from session
const userId = await getUserId()
// Check rate limit
await checkRateLimit(userId)
// Proceed with generation...
}Rate Limit Handler (In-Memory)
// lib/rate-limit-memory.ts
// Simple in-memory rate limiter for development
interface RateLimitEntry {
count: number
resetAt: number
}
const limits = new Map<string, RateLimitEntry>()
export function checkRateLimitMemory(
userId: string,
maxRequests = 10,
windowMs = 60000
) {
const now = Date.now()
const entry = limits.get(userId)
if (!entry || now > entry.resetAt) {
limits.set(userId, { count: 1, resetAt: now + windowMs })
return { allowed: true, remaining: maxRequests - 1 }
}
if (entry.count >= maxRequests) {
return {
allowed: false,
resetAt: entry.resetAt,
remaining: 0
}
}
entry.count++
return { allowed: true, remaining: maxRequests - entry.count }
}Cost Optimization Strategies
1. Use Nano for iterations, Pro for final output
// Quick iteration
const draft = await generateImage({ prompt, model: 'nano' })
// Final quality
const final = await generateImage({ prompt, model: 'pro' })2. Set `responseModalities: ['IMAGE']` to save text tokens
providerOptions: {
google: { responseModalities: ['IMAGE'] }
}3. Cache similar prompts with deduplication
const cacheKey = `img:${hash(prompt + model + aspectRatio)}`
const cached = await redis.get(cacheKey)
if (cached) return { url: cached }
const result = await generateImage(config)
await redis.setex(cacheKey, 86400, result.url)4. Implement queue system for high-volume scenarios
// Background job queue
await queue.add('generate-image', { prompt, model, userId })5. Use appropriate aspect ratios (1:1 = fewest tokens)
---
Error Handling
Common Errors and Solutions
// lib/errors.ts
export class NanoBananaError extends Error {
constructor(
message: string,
public code: string,
public retryable: boolean = false
) {
super(message)
this.name = 'NanoBananaError'
}
}
export function handleNanoBananaError(error: any) {
// Rate limit exceeded
if (error.message?.includes('429')) {
throw new NanoBananaError(
'Too many requests. Please wait a moment.',
'RATE_LIMIT',
true
)
}
// Invalid API key
if (error.message?.includes('401')) {
throw new NanoBananaError(
'Invalid API key. Check your configuration.',
'INVALID_KEY',
false
)
}
// Content policy violation
if (error.message?.includes('400')) {
throw new NanoBananaError(
'Image generation blocked. Please modify your prompt.',
'CONTENT_POLICY',
false
)
}
// Network errors
if (error.code === 'ECONNREFUSED') {
throw new NanoBananaError(
'Connection failed. Please check your internet.',
'NETWORK_ERROR',
true
)
}
throw error
}Usage in Server Actions
export async function generateImage(config: GenerateConfig) {
try {
const result = await generateText({...})
return result.files[0]
} catch (error) {
handleNanoBananaError(error)
}
}---
Environment Variables
# .env.local
# Required
GEMINI_API_KEY=your_api_key_here
# Vercel Blob (if using Vercel Blob storage)
BLOB_READ_WRITE_TOKEN=vercel_blob_xxxxx
# S3 / R2 (if using S3-compatible storage)
S3_BUCKET=your-bucket-name
S3_ENDPOINT=https://your-endpoint.r2.cloudflarestorage.com
S3_ACCESS_KEY_ID=your_access_key
S3_SECRET_ACCESS_KEY=your_secret_key
S3_PUBLIC_URL=https://your-public-domain.com
# Upstash Redis (if using rate limiting)
UPSTASH_REDIS_REST_URL=https://your-redis-url.upstash.io
UPSTASH_REDIS_REST_TOKEN=your_redis_token
# Optional
NEXT_PUBLIC_APP_URL=https://your-app.com
IMAGE_CACHE_TTL=86400
MAX_IMAGE_SIZE=5242880