The Gemini Developer API is a direct path to Google's model family through a single API key from AI Studio. This covers the full interaction surface: generateContent, streaming, tool use loops, the File API, Imagen image generation, rate tier boundaries, and where cost accumulates.
Google offers two ways to call Gemini models in production. The Developer API runs through a single API key you create in Google AI Studio. Calls go to generativelanguage.googleapis.com. The Vertex AI path uses Google Cloud service accounts and ADC and sits behind a different endpoint. The two are not interchangeable. This article covers the Developer API path only.
A Google Maps API key returns 403 API_KEY_SERVICE_BLOCKED on the Gemini endpoint. A Vertex AI service account JSON is a completely different credential type and won't work here either. The key must come from AI Studio, created against a Google Cloud project with the Generative Language API enabled.
The current unified SDK is @google/genai (JS/TS) and google-genai (Python). The older @google/generative-ai package is deprecated. Do not mix them in the same project.
npm install @google/genai
// lib/ai.ts (server-side only)
import { GoogleGenAI } from "@google/genai";
export const ai = new GoogleGenAI({
apiKey: process.env.GEMINI_API_KEY,
});
One singleton per process. The key reads from the environment. Never pass it to the browser or bundle it in a client component.
Gemini model IDs are versioned strings. A bare name like gemini-2.5-flash may resolve to a different underlying version over time. Pin to a dated alias for production workloads where behavior stability matters. List live models with a GET to /v1beta/models.
generateContentinlineData.data (base64 PNG)ai.models.generateImages()-fast-001, ultra: -ultra-001imageBytes (base64)A bare model alias like gemini-2.5-flash resolves to a specific snapshot on Google's side. That mapping changes when they release a new version under the same alias. If your production app depends on stable output, pin to a specific dated version ID (e.g. gemini-2.5-flash-001) and check the models list periodically.
The simplest path. One request, one response. Use this for short outputs where the client can wait.
const res = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: "Explain M-Pesa STK Push in two sentences.",
});
const text = res.text; // convenience accessor on first candidate
res.text is undefined when the response has no text part (safety block, tool call, empty content). Always check res.candidates?.[0]?.finishReason in production.
For long outputs or better perceived latency, stream the response chunk by chunk. Each chunk has a partial text accessor.
const stream = await ai.models.generateContentStream({
model: "gemini-2.5-flash",
contents: "Write a 500-word product description.",
});
for await (const chunk of stream) {
process.stdout.write(chunk.text ?? "");
}
In a Next.js API route, pipe chunks into a ReadableStream and return a streaming Response. The client reads them with the Fetch API's body reader.
Set responseMimeType: "application/json" and responseJsonSchema to force the model to return valid JSON matching your schema. No parsing failures; the SDK retries internally if the model drifts.
import { GoogleGenAI, Type } from "@google/genai";
const res = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: "List three Kenyan mobile payment providers.",
config: {
responseMimeType: "application/json",
responseJsonSchema: {
type: Type.ARRAY,
items: {
type: Type.OBJECT,
properties: {
name: { type: Type.STRING },
description: { type: Type.STRING },
},
propertyOrdering: ["name", "description"],
},
},
},
});
const providers = JSON.parse(res.text ?? "[]");
Pass inlineData parts alongside text to analyze an image, PDF page, or audio clip. For files under a few MB, encode as base64 and inline them. For larger or reusable assets, use the File API instead.
import { readFileSync } from "fs";
const imageBase64 = readFileSync("screenshot.png").toString("base64");
const res = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: {
parts: [
{ text: "Describe the UI issues you see in this screenshot." },
{ inlineData: { mimeType: "image/png", data: imageBase64 } },
],
},
});
Construct a contents array of parts. Each part is text, inlineData, or a fileData URI from the File API.
Flash for speed and cost. Pro for quality on hard tasks. Flash-image or Imagen for image generation.
Unary for short output. Stream for long content or better UX. Both share the same model and contents shape.
Check finishReason. STOP is normal. SAFETY means no content. TOOL_CALLS means a tool loop is in progress.
Tool use in Gemini follows the same ReAct pattern as other models: declare a set of function schemas, the model decides which to call, your code executes the function, and you return the result for another model turn. The loop continues until the model stops calling tools and produces a final text response.
Pass a tools array in the config. Each tool has a functionDeclarations list with JSON Schema-compatible parameter definitions.
const tools = [{
functionDeclarations: [{
name: "get_exchange_rate",
description: "Return the current KES to USD exchange rate.",
parameters: {
type: "OBJECT",
properties: {
base: { type: "STRING", description: "Base currency code" },
target: { type: "STRING", description: "Target currency code" },
},
required: ["base", "target"],
},
}],
}];
const res = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: "What is the current KES to USD rate?",
config: { tools },
});
If the model triggers a safety filter mid-loop, finishReason becomes SAFETY and the tool loop halts with no function call. Handle this explicitly or the calling code will spin waiting for a tool call that never arrives.
After calling the function locally, append the result to the contents array as a functionResponse part and call generateContent again. The model reads the result and produces its next action.
const call = res.candidates[0].content.parts[0].functionCall;
const result = await executeLocally(call.name, call.args);
const next = await ai.models.generateContent({
model: "gemini-2.5-flash",
contents: [
...originalContents,
{ role: "model", parts: [{ functionCall: call }] },
{ role: "user", parts: [{ functionResponse: {
name: call.name, response: result
}}]},
],
config: { tools },
});
Inlining large files as base64 in every request is wasteful. The File API lets you upload a file once and reference it by URI across multiple calls. Uploaded files persist for 48 hours. After that they're deleted automatically.
// Upload once const file = await ai.files.upload({ file: new Blob([pdfBytes], { type: "application/pdf" }), config: { displayName: "Q2 Financial Report" }, }); // Reference in multiple calls for 48h const res = await ai.models.generateContent({ model: "gemini-2.5-flash", contents: { parts: [ { text: "Summarize the revenue trends in this report." }, { fileData: { mimeType: "application/pdf", fileUri: file.uri } }, ], }, });
| Method | Best for | Size limit | Lifetime |
|---|---|---|---|
inlineData |
Small images, quick analysis | ~20 MB per request | Per-request only |
File API + fileData |
PDFs, large images, video, audio; reuse across calls | 2 GB per file | 48 hours then auto-deleted |
Files expire after 48 hours. If you cache a file URI for later use and call it after expiry, the API returns a 404. For long-lived assets, upload again before each batch of calls, or store the raw bytes in your own storage (R2, S3) and re-upload on demand.
Two paths exist for generating images via the Gemini Developer API. They have different quality profiles, latencies, and cost structures.
Highest-quality text-to-image. Three variants:
imagen-4.0-generate-001 (standard)imagen-4.0-fast-001 (faster, lower cost)imagen-4.0-ultra-001 (best quality, highest cost)Called via ai.models.generateImages(). Response: generatedImages[0].image.imageBytes (base64).
Multimodal model that emits image parts in a standard generateContent response. Useful for combined text + image output in one call.
Response: candidates[0].content.parts[].inlineData.data (base64 PNG).
Lower quality than Imagen 4 but works in the same request as reasoning.
const res = await ai.models.generateImages({
model: "imagen-4.0-generate-001",
prompt: "A dark-themed dashboard UI with cyan accents, isometric 3D view",
config: { numberOfImages: 1, outputMimeType: "image/png" },
});
const bytes = res.generatedImages?.[0]?.image?.imageBytes; // base64
// Decode and store in R2 / S3 to avoid regenerating on every request
The Gemini API returns standard HTTP codes with a canonical status name. Three codes are transient and worth retrying. The rest indicate a bad request or bad credentials: retrying just wastes quota.
| Code | Status | Meaning | Retry? |
|---|---|---|---|
| 400 | INVALID_ARGUMENT | Malformed request, bad field, unsupported MIME type | No: fix the call |
| 403 | PERMISSION_DENIED | Wrong key type (e.g. Maps key), API not enabled | No: fix the key |
| 429 | RESOURCE_EXHAUSTED | Rate limit or quota exceeded | Yes: exponential backoff |
| 500 | INTERNAL | Unexpected error on Google's side | Yes: exponential backoff |
| 503 | UNAVAILABLE | Service temporarily overloaded | Yes: exponential backoff |
| 504 | DEADLINE_EXCEEDED | Request timed out | Maybe: raise client timeout or shrink prompt |
The SDK throws an ApiError with a .status field (the HTTP code). Branch on status, not string messages. Cap retries at 5. Apply exponential backoff with jitter.
import { ApiError } from "@google/genai";
const RETRYABLE = new Set([429, 500, 503]);
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
async function withBackoff(fn, maxRetries = 5) {
for (let attempt = 0; ; attempt++) {
try {
return await fn();
} catch (err) {
const status = err instanceof ApiError ? err.status : undefined;
if (attempt >= maxRetries || !RETRYABLE.has(status)) throw err;
// 1s, 2s, 4s, 8s... capped at 30s, plus up to 1s jitter
await sleep(Math.min(2 ** attempt * 1000, 30_000) + Math.random() * 1000);
}
}
}
When the model blocks a response for safety reasons, the API returns HTTP 200 with finishReason: "SAFETY" and no text content. res.text is undefined. This is not a transient error: retrying the same prompt returns the same block. Handle it as a separate case from network errors.
The free tier has tight per-minute (RPM) and per-day (RPD) quotas. The specific numbers vary by model and change over time. Check your project's live limits in the AI Studio console. Once billing is enabled, limits are much higher but you pay per token (text) and per image (images).
On the free tier, Google may use request and response data for model improvement. For any production workload handling PII or confidential user data, enable billing and verify the current data-use terms apply the appropriate opt-out.
The GEMINI_API_KEY is server-side only. Call the API from Next.js Server Actions, API routes, or standalone scripts. Never reference the key in client components or let it land in the browser bundle. Restrict the key in the Google Cloud console to specific APIs and IP ranges where possible.
generateContent call.| Behavior | Cost Impact | Mitigation |
|---|---|---|
| Sending full chat history on every turn | Grows linearly with turns | Summarize old turns or use caching prefix |
| Inlining the same large PDF on every call | Repeated token cost | Upload once via File API, reuse URI for 48h |
| Regenerating images on each request | Billed per image every time | Generate once, cache bytes in R2/S3 |
| No prompt caching on fixed system prompt | Full system prompt billed every call | Enable prompt caching on static prefix |
| Using Pro model for simple tasks | 2-4x token cost vs Flash | Default to Flash; escalate to Pro only when quality needs it |
generativelanguage.googleapis.com. A Maps key returns 403. A Vertex credential won't work here at all.@google/genai, not @google/generative-ai. The old package is deprecated. Mixing both in the same project causes confusing import collisions.GEMINI_API_KEY in client components or anywhere that ends up in the browser bundle.New deep-dives on systems architecture, delivered when they ship.