Rust Backend And Commands
Rust commands are the narrowest useful native boundary in a Tauri app. Use them before reaching for a plugin or sidecar.
Command Shape
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct SaveNoteRequest {
title: String,
body: String,
}
#[derive(Serialize)]
struct SaveNoteResponse {
id: String,
}
#[tauri::command]
async fn save_note(input: SaveNoteRequest) -> Result<SaveNoteResponse, String> {
if input.title.trim().is_empty() {
return Err("title is required".into());
}
Ok(SaveNoteResponse { id: "note_1".into() })
}
Register commands in one obvious place:
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![save_note])
.run(tauri::generate_context!())
.expect("error while running tauri application");
State
Use managed state when multiple commands need shared native state.
use std::sync::Mutex;
use tauri::State;
struct AppState {
active_project: Mutex<Option<String>>,
}
#[tauri::command]
fn active_project(state: State<'_, AppState>) -> Option<String> {
state.active_project.lock().ok().and_then(|value| value.clone())
}
The Tauri Rust API requires managed state to be Send + Sync + 'static. If you need async state, be explicit about locks, cancellation, and shutdown.
Channels And Events
Use a command return value for normal request/response. Use channels or events when Rust needs to stream progress back to the frontend, such as downloads, imports, model startup, or long sidecar work.
Do not fake streaming with polling until you know why channels/events are insufficient.
Command Versus Plugin
| Use commands when | Use a plugin when |
|---|---|
| The feature is app-specific | The feature is reusable across apps |
| One app owns the API shape | You need JavaScript API glue plus Rust implementation |
| Permissions are simple | You need custom permissions and scopes |
| Desktop only is fine | Mobile needs Kotlin/Swift platform code |
Command Review Checklist
Does the command validate all webview input?
Does it avoid leaking secrets in errors or logs?
Does it use app paths instead of raw user-provided paths where possible?
Does it return serializable errors the UI can handle?
Does it have Rust tests for allowed and denied inputs?
Does the capability file expose only the command/plugin authority needed?