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 whenUse a plugin when
The feature is app-specificThe feature is reusable across apps
One app owns the API shapeYou need JavaScript API glue plus Rust implementation
Permissions are simpleYou need custom permissions and scopes
Desktop only is fineMobile 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?