Tauri
Build desktop apps with a web frontend and Rust backend - lightweight shells, typed IPC, and OS integration.
Recipe
#[tauri::command]
fn read_text(path: String) -> Result<String, String> {
std::fs::read_to_string(path).map_err(|e| e.to_string())
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![read_text])
.run(tauri::generate_context!())
.expect("error running tauri");
}When to reach for this:
- Teams strong in React/Svelte/Vue wanting native packaging
- Apps needing filesystem, clipboard, or shell access from Rust
- Smaller download size vs Electron
- Security-sensitive operations kept out of JavaScript
Working Example
use serde::Serialize;
use tauri::State;
use std::sync::Mutex;
#[derive(Default)]
struct AppState {
counter: Mutex<i32>,
}
#[derive(Serialize)]
struct CounterResponse {
value: i32,
}
#[tauri::command]
fn increment(state: State<AppState>) -> CounterResponse {
let mut c = state.counter.lock().unwrap();
*c += 1;
CounterResponse { value: *c }
}
fn main() {
tauri::Builder::default()
.manage(AppState::default())
.invoke_handler(tauri::generate_handler![increment])
.run(tauri::generate_context!())
.expect("tauri run failed");
}What this demonstrates:
- Managed state across command invocations
- Serde JSON responses to frontend
- Minimal command registration in builder
Deep Dive
How It Works
- WebView renders UI assets from
dist/folder. - IPC serializes command args/responses with serde.
- Rust plugins add deep links, updater, and shell scopes.
- Capabilities (Tauri 2) replace blanket allow lists.
Tauri vs Electron
| Aspect | Tauri | Electron |
|---|---|---|
| Runtime size | System WebView | Bundled Chromium |
| Backend | Rust | Node.js |
| IPC | Commands/events | Node integration |
Gotchas
- Exposing raw paths from JS - path traversal if unchecked. Fix: canonicalize and verify under app dir.
- Blocking in sync commands - freezes UI WebView. Fix:
asynccommands for IO-heavy work. - Missing CSP - XSS in UI becomes RCE via IPC. Fix: strict Content-Security-Policy in config.
- Dev-only
unwrapin commands - panics kill app. Fix: returnResultto frontend. - Platform WebView differences - test Safari WebView on macOS and WebView2 on Windows separately.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Electron | Need Chromium-specific APIs | Binary size critical |
| egui | Pure Rust UI, no HTML | Rich marketing site UI required |
| iced | Native Rust widgets | Team only knows React |
| Flutter desktop | Design system in Dart | Rust backend mandate |
FAQs
Tauri 1 vs 2?
Tauri 2 uses capabilities-based permissions - migrate configs when upgrading templates.
Which frontend?
Any static build - React, Svelte, Vue, or plain TS. Rust does not dictate UI framework.
How to call commands from JS?
import { invoke } from '@tauri-apps/api/core' then invoke('increment').
Background tasks?
Spawn Tokio tasks in setup hook; emit events to frontend with app.emit.
Auto-update?
Built-in updater plugin with signed artifacts - see packaging page.
Mobile?
Tauri mobile targets exist - separate from desktop chapter but shares command model.
Testing?
Unit test Rust commands; Playwright on WebView for E2E where feasible.
Secrets in app?
Never embed API keys in frontend bundle - fetch from Rust command reading env or OS keychain.
State patterns?
See State & Architecture.
Native APIs?
Native Integration for dialogs and notifications.
Related
- GUI Basics - landscape
- State & Architecture - shared state
- Packaging & Distribution - installers
- Native Integration - OS APIs
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.