Skip to main content
rule

tauri — Cursor rule: Rust backend patterns for Tauri

A Cursor rule that sets conventions for the Rust side of a Tauri app: commands, shared state, error types and permissions. Useful once the frontend is done and the Rust layer grows.

Install into Cursor · Manually: put the files in place All methods and platforms ↓
.cursor/rules/tauri.mdc
Open on GitHub

What tauri does

The rule attaches to **/*.rs files and reads as a set of agreements for the Rust half of a Tauri project. It asks you to group code by feature instead of by file kind, so structs, enums and their impl blocks stay in one module. Tauri commands should stay small and async, with the real work handed off to a service or manager.

Shared mutable state goes through tauri::State, wrapped in Arc<Mutex<T>> or Arc<RwLock<T>>. For errors the rule points at thiserror or anyhow, and commands return Result<T, E> where E serializes, so the frontend can show something readable. Anything blocking belongs in spawn_blocking; leave it on the async runtime and the UI stops responding.

Permissions get their own section. Capabilities are listed explicitly in src-tauri/tauri.conf.json, and a wildcard is only acceptable for named, audited plugins. Skip that and the allowlist stops meaning much. After edits, run cargo fmt and cargo clippy.

What it helps with

  • split Tauri Rust code by feature rather than by file type
  • keep Tauri commands async so the UI stays responsive
  • share mutable state through tauri::State without data races
  • return a typed, serializable error instead of a string or a panic
  • narrow the allowlist in src-tauri/tauri.conf.json instead of using a wildcard

Example prompts for the agent

  • Reorganize src-tauri/src into feature modules and move the command logic into a manager
  • Rewrite this command to return Result<T, AppError> with a serializable error type
  • Move the blocking file read into spawn_blocking and register the state with tauri::State

How to install tauri

Manually: put the files in place

.cursor/rules/tauri.mdc

Frontmatter: description, globs, alwaysApply

Step by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules

Questions about tauri

How do I install the tauri rule in Cursor?

Copy the rule file to .cursor/rules/tauri.mdc in your project root. Cursor picks it up for files matching the globs **/*.rs.

What does the tauri rule do?

It lays out how to write the Rust backend of a Tauri app: feature-based modules, async commands, state via tauri::State, errors via thiserror or anyhow, and blocking work moved to spawn_blocking.

Which files does it apply to?

Every .rs file — the rule's frontmatter sets globs to **/*.rs.

Source text

tauri.mdc · CC0-1.0
---
description: This guide provides definitive, actionable best practices for writing robust, performant, and secure Rust backend code in Tauri applications, focusing on modern patterns and common pitfalls.
globs: **/*.rs
---
# Tauri Best Practices (Rust Backend)

Tauri applications thrive on a lean, secure Rust backend. This guide outlines the essential patterns and anti-patterns for writing high-quality Rust code that integrates seamlessly with your frontend.

## 1. Code Organization and Structure

Organize your Rust code by feature, not by file type. Keep related structs, enums, and their `impl` blocks together within a module. Leverage Rust's module system for clear separation of concerns and encapsulation.

**✅ GOOD: Feature-based Modules**

```rust
// src-tauri/src/main.rs
mod user; // Defines the 'user' module

fn main() {
    // ...
}

// src-tauri/src/user/mod.rs
pub mod manager; // Exposes user::manager

// src-tauri/src/user/manager.rs
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct User {
    pub id: u32,
    pub name: String,
}

impl User {
    pub fn new(id: u32, name: String) -> Self {
        Self { id, name }
    }

    pub fn greet(&self) -> String {
        format!("Hello, {}!", self.name)
    }
}
```

**❌ BAD: Separating Types and Implementations**

```rust
// src-tauri/src/types.rs (AVOID)
pub struct User { /* ... */ }

// src-tauri/src/user_impls.rs (AVOID)
impl User { /* ... */ }
```

> **Action**: Always run `cargo fmt` and `cargo clippy` to enforce standard Rust style and catch common errors.

## 2. Tauri Command Fundamentals

Tauri commands are the bridge between your frontend and Rust backend. Keep them focused, asynchronous, and delegate complex logic.

**✅ GOOD: Lean, Async Commands**

```rust
// src-tauri/src/main.rs
#[tauri::command]
async fn greet(name: String) -> Result<String, String> {
    // Delegate complex logic to a dedicated service/manager
    let greeting = user::manager::User::new(0, name).greet();
    Ok(greeting)
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

**❌ BAD: Blocking or Overly Complex Commands**

```rust
// src-tauri/src/main.rs (AVOID)
#[tauri::command]
fn process_large_file_sync(path: String) -> Result<String, String> {
    // This blocks the main thread, making the UI unresponsive
    std::thread::sleep(std::time::Duration::from_secs(5));
    Ok(format!("Processed {}", path))
}
```

## 3. Manage Global State with `tauri::State`

For shared, mutable application state, `tauri::State` is the definitive solution. Wrap your state in `Arc<Mutex<T>>` or `Arc<RwLock<T>>` for thread-safe access.

**✅ GOOD: Thread-Safe Global State**

```rust
// src-tauri/src/main.rs
use std::sync::{Arc, Mutex};

struct AppState {
    counter: u32,
}

#[tauri::command]
fn increment_counter(state: tauri::State<'_, Arc<Mutex<AppState>>>) -> Result<u32, String> {
    let mut app_state = state.lock().map_err(|e| format!("State lock error: {}", e))?;
    app_state.counter += 1;
    Ok(app_state.counter)
}

fn main() {
    let app_state = Arc::new(Mutex::new(AppState { counter: 0 }));
    tauri::Builder::default()
        .manage(app_state) // Register the state
        .invoke_handler(tauri::generate_handler![increment_counter])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

**❌ BAD: Unmanaged Global Statics**

```rust
// src-tauri/src/main.rs (AVOID)
static mut GLOBAL_COUNTER: u32 = 0; // NOT thread-safe, prone to data races
#[tauri::command]
fn get_global_counter() -> u32 {
    unsafe { GLOBAL_COUNTER } // Unsafe code, difficult to reason about
}
```

## 4. Robust Error Handling

Use `thiserror` or `anyhow` for structured error handling. Always return `Result<T, E>` from commands, where `E` is a serializable error type. This allows the frontend to display meaningful error messages.

**✅ GOOD: Custom, Serializable Errors**

```rust
// src-tauri/src/error.rs
use thiserror::Error;
use serde::Serialize;

#[derive(Debug, Error, Serialize)]
pub enum AppError {
    #[error("Resource not found: {0}")]
    NotFound(String),
    #[error("Database operation failed: {0}")]
    Database(String),
    #[error("Internal server error: {0}")]
    Internal(String),
}

impl From<std::io::Error> for AppError {
    fn from(err: std::io::Error) -> Self {
        AppError::Internal(format!("IO error: {}", err))
    }
}

// src-tauri/src/main.rs
use tauri::async_runtime::spawn_blocking;
use crate::error::AppError; // Assuming error.rs is in src

#[tauri::command]
async fn read_config_file(path: String) -> Result<String, AppError> {
    // Use spawn_blocking for potentially blocking I/O
    spawn_blocking(move || {
        std::fs::read_to_string(&path).map_err(|e| AppError::from(e))
    }).await.map_err(|e| AppError::Internal(format!("Blocking task failed: {}", e)))?
}
```

**❌ BAD: Generic Error Strings or Panics**

```rust
// src-tauri/src/main.rs (AVOID)
#[tauri::command]
fn divide(a: u32, b: u32) -> Result<u32, String> {
    if b == 0 {
        return Err("Cannot divide by zero".to_string()); // Generic string
    }
    Ok(a / b)
}
#[tauri::command]
fn dangerous_operation() {
    panic!("Something went wrong!"); // Crashes the backend
}
```

## 5. Performance Considerations

Tauri's multi-process model means the Rust core can run heavy tasks without freezing the UI. Leverage `async/await` and `spawn_blocking` for optimal responsiveness.

**✅ GOOD: Offload Blocking Tasks**

```rust
// src-tauri/src/main.rs
use tauri::async_runtime::spawn_blocking;

#[tauri::command]
async fn perform_heavy_computation() -> Result<u64, String> {
    let result = spawn_blocking(|| {
        // Simulate a CPU-bound blocking task
        let mut sum = 0;
        for i in 0..1_000_000; {
            sum += i;
        }
        sum
    }).await.map_err(|e| e.to_string())?;
    Ok(result)
}
```

**❌ BAD: Blocking the Async Runtime**

```rust
// src-tauri/src/main.rs (AVOID)
#[tauri::command]
async fn sync_network_call() -> Result<String, String> {
    // This blocks the async runtime, impacting other async tasks
    let response = reqwest::blocking::get("http://example.com")
        .map_err(|e| e.to_string())?
        .text()
        .map_err(|e| e.to_string())?;
    Ok(response)
}
```

## 6. Common Pitfalls and Gotchas

### Security: Principle of Least Privilege

**Opinion**: Explicitly define capabilities in `src-tauri/tauri.conf.json`. Never grant wildcard access (`*`) unless absolutely necessary for specific, audited plugins.

**✅ GOOD: Granular Capabilities (`src-tauri/tauri.conf.json`)**

```json
{
  "tauri": {
    "allowlist": {
      "fs": {
        "readFile": true,
        "writeFile": true,
        "scope": ["$APPCONFIG/*", "$APPDATA/*"]
      },
      "shell": {
        "open": true
      }
    },
    "security": {
      "csp": "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline';"
    }
  }
}
```

**❌ BAD: Overly Permissive Capabilities (`src-tauri/tauri.conf.json`)**

```json
{
  "tauri": {
    "allowlist": {
      "all": true // Grants access to ALL APIs, highly insecure
    }
  }
}
```

### Frontend vs. Backend Logic

**Opinion**: Keep sensitive data and business logic exclusively in the Rust backend. The frontend (WebView) should only handle UI presentation and user interaction.

**✅ GOOD: Rust for Sensitive Logic**

```rust
// src-tauri/src/main.rs
#[tauri::command]
async fn process_payment(amount: f64, token: String) -> Result<String, AppError> {
    // Payment processing logic, interacts with secure APIs
    // ...
    Ok("Payment successful".into())
}
```

**❌ BAD: Handling Secrets in Frontend JavaScript**

```javascript
// src/main.js (AVOID)
async function sendPayment() {
  const secretApiKey = "YOUR_HARDCODED_API_KEY"; // EXPOSED!
  // ... send payment directly from frontend
}
```

## 7. Testing Approaches

Unit test your Rust modules independently. For commands, consider integration tests that simulate frontend calls.

**✅ GOOD: Unit Testing Rust Logic**

```rust
// src-tauri/src/user/manager.rs
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_user_greeting() {
        let user = User::new(1, "Alice".into());
        assert_eq!(user.greet(), "Hello, Alice!");
    }

    #[test]
    fn test_user_id() {
        let user = User::new(42, "Bob".into());
        assert_eq!(user.id, 42);
    }
}
```

© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc

Similar IDE rules

All IDE rules for Backend and APIs →

From the same set87

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor