GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-04-06Beginner

Using Gemini API with Rust: A Basics — Text Generation, Streaming & Multimodal Input

Learn how to call the Gemini API from Rust using the reqwest crate. This hands-on guide walks you through text generation, SSE streaming responses, multimodal image input, and multi-turn conversations with complete code examples.

gemini-api277rust2text-generationstreaming28reqwestbeginner13

Scan the list of official Gemini API SDKs — Python, JavaScript, Go, Java, Kotlin, Swift, Dart — and one name is conspicuously absent: Rust. If you reach for Rust when memory safety and throughput both matter, that gap stings a little.

It turns out not to matter much. The Gemini API speaks plain RESTful HTTP, so the reqwest crate gets you connected in a few dozen lines. Here is what we'll cover:

  • Set up a Cargo project with the necessary dependencies
  • Make asynchronous text generation requests to the Gemini API
  • Handle real-time Server-Sent Events (SSE) for streaming responses
  • Send Base64-encoded images for multimodal input
  • Manage multi-turn conversations
  • Troubleshoot common errors

This guide assumes basic familiarity with Rust syntax but no prior experience with AI APIs.


Prerequisites and Setup

Requirements

  • Rust 1.75 or later: rustup update stable
  • A Gemini API key (free from Google AI Studio)
  • A stable internet connection

Getting Your API Key

Head to Google AI Studio, sign in with your Google account, and click Get API key to create a new key. Store it as an environment variable:

export GEMINI_API_KEY="YOUR_GEMINI_API_KEY"

Security tip: Never hardcode your API key in source files. Use environment variables or the dotenvy crate for .env file support.

Creating the Cargo Project

cargo new gemini-rust-demo
cd gemini-rust-demo

Add the following dependencies to Cargo.toml:

[dependencies]
reqwest = { version = "0.12", features = ["json", "stream"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
dotenvy = "0.15"
base64 = "0.22"
futures-util = "0.3"

Text Generation — Your First API Call

Let's start with the simplest possible use case: sending a prompt and receiving a response. The Gemini API's /generateContent endpoint accepts a JSON payload and returns the model's reply.

use reqwest::Client;
use serde_json::{json, Value};
use std::env;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load API key from environment variable
    let api_key = env::var("GEMINI_API_KEY")
        .expect("GEMINI_API_KEY environment variable not set");
 
    let client = Client::new();
 
    // Build the request body
    let body = json!({
        "contents": [{
            "parts": [{
                "text": "Explain the three key features of Rust programming in simple terms."
            }]
        }]
    });
 
    // Send request to Gemini 2.5 Flash
    let url = format!(
        "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={}",
        api_key
    );
 
    let response = client
        .post(&url)
        .header("Content-Type", "application/json")
        .json(&body)
        .send()
        .await?;
 
    let json: Value = response.json().await?;
 
    // Extract the text from the response
    let text = &json["candidates"][0]["content"]["parts"][0]["text"];
    println!("Gemini says:\n{}", text.as_str().unwrap_or("No response received"));
 
    Ok(())
}

Expected output:

Gemini says:
Rust's three key features:

1. **Memory safety without a garbage collector**: Rust enforces ownership rules at compile time...
2. **Zero-cost abstractions**: High-level features compile down to efficient machine code...
3. **Fearless concurrency**: The type system prevents data races at compile time...

Adding System Instructions

System instructions let you define the model's role and response style before the conversation begins:

let body = json!({
    "system_instruction": {
        "parts": [{
            "text": "You are an expert Rust developer. Explain concepts clearly with code examples, suitable for developers who are new to Rust."
        }]
    },
    "contents": [{
        "role": "user",
        "parts": [{
            "text": "What is ownership in Rust?"
        }]
    }]
});

Streaming Responses with SSE

For longer responses, streaming lets you display output incrementally rather than waiting for the full reply. Use the streamGenerateContent endpoint with alt=sse.

use futures_util::StreamExt;
use reqwest::Client;
use serde_json::{json, Value};
use std::env;
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = env::var("GEMINI_API_KEY")?;
    let client = Client::new();
 
    let body = json!({
        "contents": [{
            "parts": [{
                "text": "Write a detailed explanation of async/await in Rust, with examples."
            }]
        }]
    });
 
    let url = format!(
        "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?key={}&alt=sse",
        api_key
    );
 
    let mut stream = client
        .post(&url)
        .header("Content-Type", "application/json")
        .json(&body)
        .send()
        .await?
        .bytes_stream();
 
    // Process each chunk as it arrives
    while let Some(chunk) = stream.next().await {
        let bytes = chunk?;
        let text = String::from_utf8_lossy(&bytes);
 
        for line in text.lines() {
            if let Some(json_str) = line.strip_prefix("data: ") {
                if json_str.trim() == "[DONE]" {
                    break;
                }
                if let Ok(json) = serde_json::from_str::<Value>(json_str) {
                    if let Some(part_text) = json["candidates"][0]["content"]["parts"][0]["text"].as_str() {
                        print!("{}", part_text); // Real-time output
                        use std::io::Write;
                        std::io::stdout().flush()?;
                    }
                }
            }
        }
    }
    println!();
    Ok(())
}

Multimodal Input — Analyzing Images

One of Gemini's standout capabilities is multimodal understanding. You can send images alongside text by encoding the file as Base64 and passing it as inlineData.

use base64::{engine::general_purpose, Engine as _};
use reqwest::Client;
use serde_json::{json, Value};
use std::{env, fs};
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = env::var("GEMINI_API_KEY")?;
    let client = Client::new();
 
    // Read and encode the image file
    let image_bytes = fs::read("sample.jpg")?;
    let base64_image = general_purpose::STANDARD.encode(&image_bytes);
 
    let body = json!({
        "contents": [{
            "parts": [
                {
                    "inline_data": {
                        "mime_type": "image/jpeg",
                        "data": base64_image
                    }
                },
                {
                    "text": "Describe what you see in this image in detail."
                }
            ]
        }]
    });
 
    let url = format!(
        "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={}",
        api_key
    );
 
    let response = client.post(&url).json(&body).send().await?;
    let json: Value = response.json().await?;
 
    println!(
        "{}",
        json["candidates"][0]["content"]["parts"][0]["text"]
            .as_str()
            .unwrap_or("Could not analyze image")
    );
 
    Ok(())
}

Multi-Turn Conversations

Maintaining conversation context requires accumulating the full contents array and sending it with each request.

use reqwest::Client;
use serde_json::{json, Value};
use std::env;
use std::io::{self, BufRead, Write};
 
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let api_key = env::var("GEMINI_API_KEY")?;
    let client = Client::new();
    let url = format!(
        "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={}",
        api_key
    );
 
    // Store conversation history
    let mut conversation: Vec<Value> = Vec::new();
    let stdin = io::stdin();
 
    println!("Chat started (type 'quit' to exit)");
    print!("You: ");
    io::stdout().flush()?;
 
    for line in stdin.lock().lines() {
        let user_input = line?;
        if user_input.trim() == "quit" {
            break;
        }
 
        // Append user message to history
        conversation.push(json!({
            "role": "user",
            "parts": [{ "text": user_input }]
        }));
 
        let body = json!({ "contents": conversation });
        let response = client.post(&url).json(&body).send().await?;
        let json: Value = response.json().await?;
 
        if let Some(reply) = json["candidates"][0]["content"]["parts"][0]["text"].as_str() {
            println!("Gemini: {}", reply);
 
            // Append model reply to history
            conversation.push(json!({
                "role": "model",
                "parts": [{ "text": reply }]
            }));
        }
 
        print!("You: ");
        io::stdout().flush()?;
    }
 
    Ok(())
}

Common Errors and How to Fix Them

400 Bad Request

This usually means your JSON payload is malformed. Debug by printing the request body with serde_json::to_string_pretty(&body).

403 API_KEY_INVALID

Your API key is missing or invalid. Verify with echo $GEMINI_API_KEY and make sure the variable is exported correctly.

429 RESOURCE_EXHAUSTED

You've hit the rate limit. The free tier allows 15 requests per minute for gemini-2.5-flash. Implement exponential backoff:

use tokio::time::{sleep, Duration};
 
for attempt in 0..3u32 {
    let response = client.post(&url).json(&body).send().await?;
    if response.status().as_u16() == 429 {
        let wait_secs = 2_u64.pow(attempt);
        eprintln!("Rate limited. Retrying in {}s...", wait_secs);
        sleep(Duration::from_secs(wait_secs)).await;
        continue;
    }
    // Handle successful response...
    break;
}

TLS errors on Linux

If you encounter TLS-related compilation errors, switch to rustls:

reqwest = { version = "0.12", features = ["json", "stream", "rustls-tls"], default-features = false }

For a deeper dive into production-grade API integrations — including Function Calling, tool use, and structured output patterns — check out Gemini API Function Calling Complete Guide.


Summary

In this guide, we covered the essentials of integrating the Gemini API into Rust applications:

  • The reqwest + serde_json combination provides a clean, idiomatic way to call the REST API
  • SSE streaming via streamGenerateContent enables real-time token-by-token output
  • inline_data with Base64 encoding unlocks multimodal image analysis
  • Accumulating the contents array enables stateful multi-turn conversations
  • Exponential backoff handles rate limiting gracefully

The combination of Rust's performance and memory safety with Gemini's AI capabilities is a powerful foundation for building reliable, high-throughput AI services. As a natural next step, consider wrapping these calls in an Axum or Actix-web HTTP server to expose your Gemini integration as a REST API.

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.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API / SDK2026-06-25
Gemini API × TypeScript Type-Safe AI Application Architecture — Integrating Zod Schemas, Structured Output, and Streaming
Type-safe AI applications with the Gemini API and TypeScript: Zod validation, Structured Output, streaming pipelines, and error handling that holds up in production.
API / SDK2026-05-23
When Gemini API Streaming Cuts Off Mid-Response in Production: The Diagnosis Order I Run
How I diagnose mid-response cutoffs in Gemini API streaming - the order I check network, SDK, and server-side suspects, with real cases from indie production.
API / SDK2026-05-20
Gemini API Streaming Works Locally but Buffers in Production — Fixing Cloud Run, Vercel, and Cloudflare
Streaming responses flow token-by-token in local dev, then arrive as one big blob in production. A walkthrough of the five most common causes — Cloud Run timeouts, Vercel runtime mismatch, Cloudflare Workers proxying, server-side text() pitfalls, and client-side decoding — with the fixes I use across Dolice Labs.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →