$ cat /posts/tauri-20-http-client-making-api-requests-from-rust.md
[tags]Tauri 2.0

Tauri 2.0 HTTP Client Making API Requests from Rust

drwxr-xr-x2026-01-285 min0 views
Tauri 2.0 HTTP Client Making API Requests from Rust

HTTP client functionality in Tauri 2.0 enables desktop applications to communicate with web APIs, fetch remote data, upload files, and integrate with backend services leveraging Rust's powerful reqwest HTTP client for high-performance async requests with automatic serialization, connection pooling, timeout handling, and custom headers—essential for building connected desktop applications consuming REST APIs, GraphQL endpoints, WebSocket connections, and third-party services. Performing HTTP requests from Rust backend rather than JavaScript frontend provides security benefits including API key protection, CORS bypass for restricted APIs, certificate pinning, request signing, and rate limiting enforcement while maintaining performance through connection reuse, async operations, and efficient memory management. This comprehensive guide covers understanding HTTP client architecture and security implications, making GET requests with query parameters and headers, sending POST/PUT/DELETE requests with JSON bodies, handling authentication with tokens and API keys, uploading files with multipart form data, downloading files with progress tracking, implementing retry logic and error handling, managing timeouts and connection pooling, working with custom headers and cookies, and building real-world integrations including REST API clients, GraphQL queries, and file synchronization. Mastering HTTP patterns enables building sophisticated desktop applications syncing data with cloud services, fetching remote content, uploading telemetry, integrating third-party APIs, and implementing offline-first architectures with sync. Before proceeding, understand command creation and state management.

HTTP Client Setup

tomlCargo.toml
# Add reqwest to Cargo.toml
# src-tauri/Cargo.toml

[dependencies]
tauri = { version = "2.0", features = ["...your features..."] }
reqwest = { version = "0.11", features = ["json", "multipart"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }

# Optional: For better error handling
thiserror = "1.0"

# Optional: For async traits
async-trait = "0.1"

GET Requests

rustget_requests.rs
// Rust: GET requests
use serde::{Deserialize, Serialize};

// Simple GET request
#[tauri::command]
async fn fetch_data(url: String) -> Result<String, String> {
    let response = reqwest::get(&url)
        .await
        .map_err(|e| e.to_string())?
        .text()
        .await
        .map_err(|e| e.to_string())?;
    
    Ok(response)
}

// GET with JSON response
#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
    email: String,
}

#[tauri::command]
async fn get_user(id: u32) -> Result<User, String> {
    let url = format!("https://api.example.com/users/{}", id);
    let user = reqwest::get(&url)
        .await
        .map_err(|e| e.to_string())?
        .json::<User>()
        .await
        .map_err(|e| e.to_string())?;
    
    Ok(user)
}

// GET with query parameters
#[tauri::command]
async fn search_users(query: String, page: u32) -> Result<Vec<User>, String> {
    let client = reqwest::Client::new();
    let response = client
        .get("https://api.example.com/users/search")
        .query(&[("q", query), ("page", page.to_string())])
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    let users = response
        .json::<Vec<User>>()
        .await
        .map_err(|e| e.to_string())?;
    
    Ok(users)
}

// GET with custom headers
#[tauri::command]
async fn fetch_with_auth(url: String, token: String) -> Result<String, String> {
    let client = reqwest::Client::new();
    let response = client
        .get(&url)
        .header("Authorization", format!("Bearer {}", token))
        .header("Content-Type", "application/json")
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    let text = response.text().await.map_err(|e| e.to_string())?;
    Ok(text)
}

// GET with timeout
use std::time::Duration;

#[tauri::command]
async fn fetch_with_timeout(url: String, timeout_secs: u64) -> Result<String, String> {
    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(timeout_secs))
        .build()
        .map_err(|e| e.to_string())?;
    
    let response = client
        .get(&url)
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    response.text().await.map_err(|e| e.to_string())
}

// Frontend: Calling HTTP commands
import { invoke } from "@tauri-apps/api/core";

// Fetch data
const data = await invoke<string>("fetch_data", {
  url: "https://api.example.com/data",
});

// Get user
interface User {
  id: number;
  name: string;
  email: string;
}

const user = await invoke<User>("get_user", { id: 1 });

// Search with query
const results = await invoke<User[]>("search_users", {
  query: "john",
  page: 1,
});

// With authentication
const authData = await invoke<string>("fetch_with_auth", {
  url: "https://api.example.com/protected",
  token: "your-token-here",
});

POST, PUT, DELETE Requests

rustpost_requests.rs
// Rust: POST/PUT/DELETE requests
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct CreateUserRequest {
    name: String,
    email: String,
}

#[derive(Serialize, Deserialize)]
struct UserResponse {
    id: u32,
    name: String,
    email: String,
    created_at: String,
}

// POST with JSON body
#[tauri::command]
async fn create_user(name: String, email: String) -> Result<UserResponse, String> {
    let client = reqwest::Client::new();
    let body = CreateUserRequest { name, email };
    
    let response = client
        .post("https://api.example.com/users")
        .json(&body)
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    let user = response
        .json::<UserResponse>()
        .await
        .map_err(|e| e.to_string())?;
    
    Ok(user)
}

// PUT request
#[tauri::command]
async fn update_user(
    id: u32,
    name: String,
    email: String
) -> Result<UserResponse, String> {
    let client = reqwest::Client::new();
    let body = CreateUserRequest { name, email };
    let url = format!("https://api.example.com/users/{}", id);
    
    let response = client
        .put(&url)
        .json(&body)
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    response.json::<UserResponse>().await.map_err(|e| e.to_string())
}

// DELETE request
#[tauri::command]
async fn delete_user(id: u32) -> Result<(), String> {
    let client = reqwest::Client::new();
    let url = format!("https://api.example.com/users/{}", id);
    
    client
        .delete(&url)
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    Ok(())
}

// POST with authentication
#[tauri::command]
async fn create_post(
    token: String,
    title: String,
    content: String
) -> Result<serde_json::Value, String> {
    let client = reqwest::Client::new();
    let body = serde_json::json!({
        "title": title,
        "content": content,
    });
    
    let response = client
        .post("https://api.example.com/posts")
        .header("Authorization", format!("Bearer {}", token))
        .json(&body)
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    response.json().await.map_err(|e| e.to_string())
}

// POST with custom headers
#[tauri::command]
async fn api_request(
    method: String,
    url: String,
    headers: Vec<(String, String)>,
    body: Option<serde_json::Value>
) -> Result<serde_json::Value, String> {
    let client = reqwest::Client::new();
    let mut request = match method.as_str() {
        "GET" => client.get(&url),
        "POST" => client.post(&url),
        "PUT" => client.put(&url),
        "DELETE" => client.delete(&url),
        _ => return Err("Invalid HTTP method".to_string()),
    };
    
    // Add headers
    for (key, value) in headers {
        request = request.header(&key, &value);
    }
    
    // Add body if present
    if let Some(body) = body {
        request = request.json(&body);
    }
    
    let response = request.send().await.map_err(|e| e.to_string())?;
    response.json().await.map_err(|e| e.to_string())
}

// Frontend: POST/PUT/DELETE
import { invoke } from "@tauri-apps/api/core";

// Create user
const newUser = await invoke<UserResponse>("create_user", {
  name: "John Doe",
  email: "[email protected]",
});

// Update user
const updated = await invoke<UserResponse>("update_user", {
  id: 1,
  name: "John Smith",
  email: "[email protected]",
});

// Delete user
await invoke("delete_user", { id: 1 });

// Create post with auth
const post = await invoke("create_post", {
  token: "your-token",
  title: "My Post",
  content: "Post content...",
});

File Upload and Download

rustfile_operations.rs
// Rust: File upload with multipart
use reqwest::multipart;

#[tauri::command]
async fn upload_file(path: String, field_name: String) -> Result<String, String> {
    // Read file
    let file_content = tokio::fs::read(&path)
        .await
        .map_err(|e| e.to_string())?;
    
    // Get filename
    let filename = std::path::Path::new(&path)
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("file")
        .to_string();
    
    // Create multipart form
    let part = multipart::Part::bytes(file_content)
        .file_name(filename);
    
    let form = multipart::Form::new()
        .part(field_name, part);
    
    // Upload
    let client = reqwest::Client::new();
    let response = client
        .post("https://api.example.com/upload")
        .multipart(form)
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    response.text().await.map_err(|e| e.to_string())
}

// Upload with progress tracking
use tauri::{AppHandle, Manager};

#[tauri::command]
async fn upload_with_progress(
    app: AppHandle,
    path: String
) -> Result<String, String> {
    let file_content = tokio::fs::read(&path)
        .await
        .map_err(|e| e.to_string())?;
    
    let total_size = file_content.len() as u64;
    let filename = std::path::Path::new(&path)
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("file")
        .to_string();
    
    // Emit start event
    app.emit_all("upload-started", total_size).ok();
    
    let part = multipart::Part::bytes(file_content)
        .file_name(filename);
    
    let form = multipart::Form::new().part("file", part);
    
    let client = reqwest::Client::new();
    let response = client
        .post("https://api.example.com/upload")
        .multipart(form)
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    // Emit complete event
    app.emit_all("upload-complete", ()).ok();
    
    response.text().await.map_err(|e| e.to_string())
}

// Download file
#[tauri::command]
async fn download_file(url: String, save_path: String) -> Result<(), String> {
    let response = reqwest::get(&url)
        .await
        .map_err(|e| e.to_string())?;
    
    let bytes = response.bytes().await.map_err(|e| e.to_string())?;
    
    tokio::fs::write(&save_path, bytes)
        .await
        .map_err(|e| e.to_string())?;
    
    Ok(())
}

// Download with progress
#[tauri::command]
async fn download_with_progress(
    app: AppHandle,
    url: String,
    save_path: String
) -> Result<(), String> {
    let response = reqwest::get(&url)
        .await
        .map_err(|e| e.to_string())?;
    
    let total_size = response.content_length().unwrap_or(0);
    app.emit_all("download-started", total_size).ok();
    
    let bytes = response.bytes().await.map_err(|e| e.to_string())?;
    
    tokio::fs::write(&save_path, &bytes)
        .await
        .map_err(|e| e.to_string())?;
    
    app.emit_all("download-complete", bytes.len()).ok();
    
    Ok(())
}

// Frontend: File operations
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";

// Upload file
const response = await invoke<string>("upload_file", {
  path: "/path/to/file.jpg",
  field_name: "file",
});

// Upload with progress tracking
const unlistenStart = await listen<number>("upload-started", ({ payload }) => {
  console.log(`Upload started: ${payload} bytes`);
});

const unlistenComplete = await listen("upload-complete", () => {
  console.log("Upload complete!");
});

await invoke("upload_with_progress", {
  path: "/path/to/large-file.zip",
});

// Download file
await invoke("download_file", {
  url: "https://example.com/file.pdf",
  save_path: "/path/to/save/file.pdf",
});

Error Handling and Retry Logic

rusterror_handling.rs
// Rust: Advanced error handling
use thiserror::Error;

#[derive(Error, Debug)]
enum ApiError {
    #[error("Network error: {0}")]
    Network(String),
    
    #[error("API error: {status} - {message}")]
    Api { status: u16, message: String },
    
    #[error("Timeout error")]
    Timeout,
    
    #[error("Parse error: {0}")]
    Parse(String),
}

impl serde::Serialize for ApiError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

// Request with retry logic
#[tauri::command]
async fn fetch_with_retry(
    url: String,
    max_retries: u32
) -> Result<String, ApiError> {
    let client = reqwest::Client::new();
    let mut retries = 0;
    
    loop {
        match client.get(&url).send().await {
            Ok(response) => {
                if response.status().is_success() {
                    return response
                        .text()
                        .await
                        .map_err(|e| ApiError::Parse(e.to_string()));
                } else {
                    return Err(ApiError::Api {
                        status: response.status().as_u16(),
                        message: response.text().await.unwrap_or_default(),
                    });
                }
            }
            Err(e) => {
                retries += 1;
                if retries >= max_retries {
                    return Err(ApiError::Network(e.to_string()));
                }
                
                // Exponential backoff
                tokio::time::sleep(tokio::time::Duration::from_secs(
                    2u64.pow(retries)
                )).await;
            }
        }
    }
}

// Handle different status codes
#[tauri::command]
async fn api_call(url: String) -> Result<serde_json::Value, ApiError> {
    let client = reqwest::Client::new();
    let response = client
        .get(&url)
        .send()
        .await
        .map_err(|e| ApiError::Network(e.to_string()))?;
    
    let status = response.status();
    
    match status.as_u16() {
        200..=299 => {
            response
                .json()
                .await
                .map_err(|e| ApiError::Parse(e.to_string()))
        }
        401 => Err(ApiError::Api {
            status: 401,
            message: "Unauthorized".to_string(),
        }),
        404 => Err(ApiError::Api {
            status: 404,
            message: "Not found".to_string(),
        }),
        _ => Err(ApiError::Api {
            status: status.as_u16(),
            message: response.text().await.unwrap_or_default(),
        }),
    }
}

// Timeout handling
use tokio::time::{timeout, Duration};

#[tauri::command]
async fn fetch_with_timeout_handling(
    url: String,
    timeout_secs: u64
) -> Result<String, ApiError> {
    let fetch_future = async {
        reqwest::get(&url)
            .await
            .map_err(|e| ApiError::Network(e.to_string()))?
            .text()
            .await
            .map_err(|e| ApiError::Parse(e.to_string()))
    };
    
    timeout(Duration::from_secs(timeout_secs), fetch_future)
        .await
        .map_err(|_| ApiError::Timeout)?
}

// Frontend: Error handling
import { invoke } from "@tauri-apps/api/core";

async function fetchData(url: string) {
  try {
    const data = await invoke<string>("fetch_with_retry", {
      url,
      max_retries: 3,
    });
    return data;
  } catch (error) {
    console.error("API Error:", error);
    
    if (typeof error === "string") {
      if (error.includes("Timeout")) {
        alert("Request timed out. Please try again.");
      } else if (error.includes("401")) {
        alert("Unauthorized. Please log in.");
      } else if (error.includes("404")) {
        alert("Resource not found.");
      } else {
        alert(`Error: ${error}`);
      }
    }
    
    throw error;
  }
}

HTTP Client Configuration

rustclient_configuration.rs
// Rust: Configurable HTTP client
use std::sync::Arc;
use std::time::Duration;
use reqwest::{Client, ClientBuilder};
use tauri::State;

// Shared HTTP client
struct HttpClient {
    client: Arc<Client>,
}

impl HttpClient {
    fn new() -> Self {
        let client = ClientBuilder::new()
            .timeout(Duration::from_secs(30))
            .connect_timeout(Duration::from_secs(10))
            .user_agent("TauriApp/1.0")
            .pool_max_idle_per_host(10)
            .build()
            .expect("Failed to create HTTP client");
        
        HttpClient {
            client: Arc::new(client),
        }
    }
}

// Use shared client in commands
#[tauri::command]
async fn fetch_with_client(
    state: State<'_, HttpClient>,
    url: String
) -> Result<String, String> {
    let response = state.client
        .get(&url)
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    response.text().await.map_err(|e| e.to_string())
}

// API client with base URL
struct ApiClient {
    client: Client,
    base_url: String,
}

impl ApiClient {
    fn new(base_url: String) -> Self {
        let client = ClientBuilder::new()
            .timeout(Duration::from_secs(30))
            .build()
            .expect("Failed to create client");
        
        ApiClient { client, base_url }
    }
    
    async fn get(&self, path: &str) -> Result<serde_json::Value, String> {
        let url = format!("{}{}", self.base_url, path);
        self.client
            .get(&url)
            .send()
            .await
            .map_err(|e| e.to_string())?
            .json()
            .await
            .map_err(|e| e.to_string())
    }
    
    async fn post(
        &self,
        path: &str,
        body: &serde_json::Value
    ) -> Result<serde_json::Value, String> {
        let url = format!("{}{}", self.base_url, path);
        self.client
            .post(&url)
            .json(body)
            .send()
            .await
            .map_err(|e| e.to_string())?
            .json()
            .await
            .map_err(|e| e.to_string())
    }
}

// Initialize in main
fn main() {
    let http_client = HttpClient::new();
    
    tauri::Builder::default()
        .manage(http_client)
        .invoke_handler(tauri::generate_handler![
            fetch_with_client,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

HTTP Client Best Practices

  • Use Rust for Sensitive APIs: Keep API keys and secrets in Rust backend never exposing to frontend
  • Implement Retry Logic: Handle transient failures with exponential backoff
  • Set Timeouts: Always configure connection and request timeouts preventing hangs
  • Reuse Client: Create client once and share across requests for connection pooling
  • Handle Status Codes: Check response status and handle errors appropriately
  • Use Async Operations: Always use async HTTP requests preventing UI blocking
  • Validate Responses: Parse and validate JSON responses handling malformed data
  • Log Errors: Log HTTP errors for debugging including status codes and messages
  • Progress Tracking: Emit events for long-running uploads/downloads
  • Secure Headers: Use HTTPS and proper authentication headers
Security Warning: Never expose API keys or secrets in frontend code! Always make authenticated API calls from Rust backend where credentials are protected. Use environment variables or encrypted storage for sensitive data. Learn more about security best practices.

Real-World Example: REST API Integration

rustapi_client.rs
// Complete REST API client
use reqwest::{Client, ClientBuilder};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use tauri::State;

#[derive(Clone)]
struct ApiConfig {
    base_url: String,
    api_key: String,
}

struct ApiState {
    client: Client,
    config: ApiConfig,
}

impl ApiState {
    fn new(config: ApiConfig) -> Self {
        let client = ClientBuilder::new()
            .timeout(Duration::from_secs(30))
            .build()
            .expect("Failed to create HTTP client");
        
        ApiState { client, config }
    }
}

#[derive(Serialize, Deserialize)]
struct Post {
    id: Option<u32>,
    title: String,
    content: String,
    author: String,
}

#[tauri::command]
async fn get_posts(state: State<'_, ApiState>) -> Result<Vec<Post>, String> {
    let url = format!("{}/posts", state.config.base_url);
    
    let response = state.client
        .get(&url)
        .header("Authorization", format!("Bearer {}", state.config.api_key))
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    response.json().await.map_err(|e| e.to_string())
}

#[tauri::command]
async fn get_post(state: State<'_, ApiState>, id: u32) -> Result<Post, String> {
    let url = format!("{}/posts/{}", state.config.base_url, id);
    
    let response = state.client
        .get(&url)
        .header("Authorization", format!("Bearer {}", state.config.api_key))
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    response.json().await.map_err(|e| e.to_string())
}

#[tauri::command]
async fn create_post(
    state: State<'_, ApiState>,
    title: String,
    content: String,
    author: String
) -> Result<Post, String> {
    let url = format!("{}/posts", state.config.base_url);
    let post = Post {
        id: None,
        title,
        content,
        author,
    };
    
    let response = state.client
        .post(&url)
        .header("Authorization", format!("Bearer {}", state.config.api_key))
        .json(&post)
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    response.json().await.map_err(|e| e.to_string())
}

#[tauri::command]
async fn update_post(
    state: State<'_, ApiState>,
    id: u32,
    title: String,
    content: String
) -> Result<Post, String> {
    let url = format!("{}/posts/{}", state.config.base_url, id);
    
    let response = state.client
        .put(&url)
        .header("Authorization", format!("Bearer {}", state.config.api_key))
        .json(&serde_json::json!({
            "title": title,
            "content": content,
        }))
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    response.json().await.map_err(|e| e.to_string())
}

#[tauri::command]
async fn delete_post(state: State<'_, ApiState>, id: u32) -> Result<(), String> {
    let url = format!("{}/posts/{}", state.config.base_url, id);
    
    state.client
        .delete(&url)
        .header("Authorization", format!("Bearer {}", state.config.api_key))
        .send()
        .await
        .map_err(|e| e.to_string())?;
    
    Ok(())
}

// Initialize in main
fn main() {
    let config = ApiConfig {
        base_url: "https://api.example.com".to_string(),
        api_key: std::env::var("API_KEY").expect("API_KEY not set"),
    };
    
    let api_state = ApiState::new(config);
    
    tauri::Builder::default()
        .manage(api_state)
        .invoke_handler(tauri::generate_handler![
            get_posts,
            get_post,
            create_post,
            update_post,
            delete_post,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Next Steps

Conclusion

Mastering HTTP client operations in Tauri 2.0 enables building connected desktop applications integrating with REST APIs, GraphQL endpoints, file upload services, and third-party platforms while maintaining security through backend API calls protecting credentials, bypassing CORS restrictions, and enforcing rate limiting unavailable in pure frontend implementations. Rust's reqwest library provides powerful async HTTP capabilities including automatic JSON serialization, connection pooling for performance, timeout handling preventing hangs, multipart uploads for files, and flexible configuration maintaining consistent behavior across all requests. Understanding HTTP patterns including proper error handling with retries, status code checking, timeout configuration, client reuse for connection pooling, progress tracking for long operations, and authentication management establishes foundation for professional desktop application development creating reliable API integrations serving diverse use cases from simple data fetching to complex file synchronization. Your Tauri applications now possess powerful HTTP capabilities enabling features like cloud sync, remote content loading, file uploads, API authentication, and offline-first architectures delivering professional desktop experiences with robust network communication!

$ cat /comments/ (0)

new_comment.sh

// Email hidden from public

>_

$ cat /comments/

// No comments found. Be the first!

[session] guest@{codershandbook}[timestamp] 2026

Navigation

Categories

Connect

Subscribe

// 2026 {Coders Handbook}. EOF.