Desktop Application Development with Tauri
Tauri is a framework for desktop applications where the backend is written in Rust and the frontend uses any web stack. Unlike Electron, Tauri doesn't ship its own Chromium: it uses the system WebView (WebKit on macOS/Linux, WebView2 on Windows). Result — distribution from 4 to 15 MB vs 80+ MB for Electron.
Architecture
Frontend (HTML/CSS/JS → any framework)
↕ invoke() / events
Tauri Core (Rust)
↕ system calls
OS (file system, notifications, tray)
All unsafe code runs in Rust. Frontend can only call explicitly exposed Rust commands via invoke.
Creating a Project
# Prerequisites: Rust + system dependencies
# macOS: xcode-select --install
# Windows: Visual Studio Build Tools + WebView2
# Linux: webkit2gtk, libayatana-appindicator
npm create tauri-app@latest
# Choose: TypeScript, React, Vite
Project structure:
my-app/
├── src/ # React/Vue/Svelte frontend
├── src-tauri/
│ ├── src/
│ │ ├── main.rs # Rust entry point
│ │ └── lib.rs # commands and plugins
│ ├── Cargo.toml
│ └── tauri.conf.json
├── package.json
└── vite.config.ts
Rust Commands: Foundation of Interaction
// src-tauri/src/lib.rs
use tauri::State;
use std::sync::Mutex;
struct AppState {
counter: Mutex<i32>,
}
// Simple command
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// Async command with state
#[tauri::command]
async fn increment(state: State<'_, AppState>) -> Result<i32, String> {
let mut counter = state.counter.lock().map_err(|e| e.to_string())?;
*counter += 1;
Ok(*counter)
}
// File system access command
#[tauri::command]
async fn read_file(path: String) -> Result<String, String> {
std::fs::read_to_string(&path).map_err(|e| e.to_string())
}
pub fn run() {
tauri::Builder::default()
.manage(AppState { counter: Mutex::new(0) })
.invoke_handler(tauri::generate_handler![greet, increment, read_file])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Calling Rust Commands from Frontend
// src/api/tauri.ts
import { invoke } from '@tauri-apps/api/core';
export async function greet(name: string): Promise<string> {
return invoke('greet', { name });
}
export async function increment(): Promise<number> {
return invoke('increment');
}
// Usage in React component
function App() {
const [count, setCount] = useState(0);
const handleIncrement = async () => {
const newCount = await increment();
setCount(newCount);
};
return <button onClick={handleIncrement}>Count: {count}</button>;
}
Event System
// Rust → Frontend
use tauri::{Manager, Emitter};
#[tauri::command]
async fn start_long_task(app: tauri::AppHandle) {
tokio::spawn(async move {
for i in 0..=100 {
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
app.emit("progress", i).unwrap();
}
app.emit("task-complete", "done").unwrap();
});
}
// Frontend — subscribe to events
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen<number>('progress', (event) => {
setProgress(event.payload);
});
await invoke('start_long_task');
Ecosystem Plugins
# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-fs = "2"
tauri-plugin-dialog = "2"
tauri-plugin-notification = "2"
tauri-plugin-shell = "2"
tauri-plugin-store = "2"
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.run(tauri::generate_context!())
.expect("error running app");
}
Build and Distribution
# Development
npm run tauri dev
# Build for current platform
npm run tauri build
Output:
- macOS:
.app+.dmg - Windows:
.exe(NSIS) +.msi - Linux:
.deb+.AppImage+.rpm
When to Choose Tauri over Electron
Tauri justified when: distribution size is critical, need maximum native code performance, team knows Rust or willing to learn.
Electron justified when: need identical runtime regardless of system WebView, team is JavaScript/TypeScript only, app actively uses npm ecosystem in main process.
Size difference: Tauri ~8 MB, Electron ~120 MB. Memory: Tauri 30-60 MB vs Electron 100-200 MB at startup.







