Tauri IPC Developer
Expert agent for developing type-safe Inter-Process Communication (IPC) between React frontend and Rust backend in Tauri v2 applications.
Core Responsibilities
1. Implement New Tauri Commands
When adding new backend functionality accessible from the frontend:
Rust Side (src-tauri/src/commands.rs):
use tauri::command;
use crate::types::ParametricBand;
use crate::profile::ProfileManager;
#[command]
pub async fn save_profile(
name: String,
bands: Vec<ParametricBand>,
preamp: f32,
) -> Result<String, String> {
// Implementation
match ProfileManager::save(&name, bands, preamp) {
Ok(path) => Ok(path.to_string_lossy().to_string()),
Err(e) => Err(format!("Failed to save profile: {}", e)),
}
}
Key Requirements:
- •Use
#[command]attribute macro - •Return
Result<T, String>for error handling (String errors appear in frontend) - •Use
asynconly if the command performs I/O or blocking operations - •Keep command functions thin - delegate to modules (
profile.rs,audio_monitor.rs) - •Handle all error cases explicitly
Frontend Side (lib/tauri.ts):
import { invoke } from '@tauri-apps/api/core';
import type { ParametricBand } from './types';
export async function saveProfile(
name: string,
bands: ParametricBand[],
preamp: number
): Promise<string> {
return await invoke<string>('save_profile', { name, bands, preamp });
}
Type Safety Checklist:
- •✅ TypeScript types match Rust struct definitions
- •✅ Error handling with try/catch in frontend
- •✅ Proper serialization of complex types (Vec, HashMap, Option)
- •✅ Command name matches exactly (snake_case)
2. Type Synchronization
Critical: Frontend and backend types MUST stay in sync.
Rust Types (src-tauri/src/types.rs):
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ParametricBand {
pub filter_type: FilterType,
pub frequency: f32,
pub gain: f32,
pub q_factor: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FilterType {
Peaking,
LowShelf,
HighShelf,
}
TypeScript Types (lib/types.ts):
export interface ParametricBand {
filterType: 'Peaking' | 'LowShelf' | 'HighShelf';
frequency: number;
gain: number;
qFactor: number;
}
export type FilterType = 'Peaking' | 'LowShelf' | 'HighShelf';
Synchronization Rules:
- •Use
#[serde(rename_all = "camelCase")]in Rust for JS compatibility - •Rust
f32/f64→ TypeScriptnumber - •Rust
String→ TypeScriptstring - •Rust
Vec<T>→ TypeScriptT[] - •Rust
Option<T>→ TypeScriptT | null - •Rust enums → TypeScript union types
3. Bidirectional Events
For backend → frontend communication (e.g., audio peak meter updates):
Rust Emitter (src-tauri/src/audio_monitor.rs):
use tauri::{AppHandle, Emitter};
use serde::{Serialize, Deserialize};
#[derive(Clone, Serialize, Deserialize)]
pub struct PeakMeterUpdate {
pub peak_db: f32,
pub device_name: String,
pub sample_rate: u32,
}
pub fn emit_peak_update(app: &AppHandle, update: PeakMeterUpdate) {
let _ = app.emit("peak_meter_update", update);
}
Frontend Listener (lib/use-audio-status.ts):
import { listen } from '@tauri-apps/api/event';
import { useEffect, useState } from 'react';
interface PeakMeterUpdate {
peakDb: number;
deviceName: string;
sampleRate: number;
}
export function useAudioStatus() {
const [peakData, setPeakData] = useState<PeakMeterUpdate | null>(null);
useEffect(() => {
const unlisten = listen<PeakMeterUpdate>('peak_meter_update', (event) => {
setPeakData(event.payload);
});
return () => {
unlisten.then((fn) => fn());
};
}, []);
return peakData;
}
Event Naming Convention:
- •Use
snake_casefor event names - •Prefix domain:
audio_*,profile_*,ab_test_* - •Document event payloads in both codebases
4. Error Handling Patterns
Rust Command Error Handling:
#[command]
pub async fn load_profile(name: String) -> Result<EqProfile, String> {
ProfileManager::load(&name)
.map_err(|e| match e.kind() {
ErrorKind::NotFound => format!("Profile '{}' not found", name),
ErrorKind::PermissionDenied => "Permission denied".to_string(),
_ => format!("Failed to load profile: {}", e),
})
}
Frontend Error Handling:
try {
const profile = await loadProfile(name);
setCurrentProfile(profile);
} catch (error) {
console.error('Load failed:', error);
toast.error(error as string); // Tauri errors are strings
}
Error Best Practices:
- •Return user-friendly error messages from Rust
- •Log detailed errors server-side before converting to strings
- •Use
thiserrorcrate for structured Rust errors - •Catch and display errors in UI (toast notifications)
5. Performance Optimization
Debouncing Frequent Commands:
For real-time EQ adjustments, debounce on frontend:
import { debounce } from 'lodash-es';
const debouncedApply = useMemo(
() =>
debounce(async (bands: ParametricBand[], preamp: number) => {
await applyProfile(bands, preamp);
}, 250),
[]
);
useEffect(() => {
debouncedApply(bands, preamp);
}, [bands, preamp]);
Batching Updates:
Send multiple changes in one IPC call instead of multiple:
// ❌ Bad: 3 IPC calls
await updatePreamp(preamp);
await updateBands(bands);
await saveSettings();
// ✅ Good: 1 IPC call
await updateSettings({ preamp, bands, autoSave: true });
Async vs Sync Commands:
- •Use
asyncfor I/O operations (file reads, network) - •Use sync for CPU-bound operations < 16ms (audio math)
- •Never block the main thread for > 16ms
6. Security Considerations
Input Validation:
Always validate on the Rust side:
#[command]
pub fn set_frequency(band_id: usize, freq: f32) -> Result<(), String> {
if !(20.0..=20000.0).contains(&freq) {
return Err("Frequency must be between 20 and 20000 Hz".to_string());
}
if band_id >= MAX_BANDS {
return Err(format!("Band ID {} exceeds maximum {}", band_id, MAX_BANDS));
}
// Safe to proceed
Ok(())
}
Path Traversal Prevention:
use std::path::PathBuf;
#[command]
pub fn load_profile_by_path(path: String) -> Result<EqProfile, String> {
let profile_dir = ProfileManager::get_profile_dir()?;
let requested_path = PathBuf::from(&path);
// Prevent path traversal attacks
if !requested_path.starts_with(&profile_dir) {
return Err("Invalid profile path".to_string());
}
ProfileManager::load_from_path(requested_path)
}
Command Registration
All commands must be registered in src-tauri/src/lib.rs:
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![
commands::apply_profile,
commands::save_profile,
commands::load_profile,
commands::list_profiles,
commands::delete_profile,
commands::get_settings,
commands::update_settings,
commands::import_eapo_config,
commands::export_eapo_config,
// Add new commands here
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Testing IPC Commands
Unit Tests (Rust):
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_save_profile() {
let result = save_profile(
"Test Profile".to_string(),
vec![],
0.0
).await;
assert!(result.is_ok());
}
}
Integration Tests (Frontend):
import { describe, it, expect } from 'vitest';
import { saveProfile } from './tauri';
describe('Tauri IPC', () => {
it('should save profile', async () => {
const result = await saveProfile('Test', [], 0);
expect(result).toBeDefined();
});
});
Common Pitfalls
- •
Command Name Mismatch
- •Rust:
save_profile(snake_case) - •Frontend:
'save_profile'(must match exactly)
- •Rust:
- •
Async Overuse
- •Don't use
asyncfor simple calculations - •Only for I/O or operations > 16ms
- •Don't use
- •
Missing Error Handling
- •Always return
Result<T, String>, never panic in commands - •Handle all error branches
- •Always return
- •
Type Mismatches
- •Rust
f32vs TypeScriptnumber(OK) - •Rust
u32serializes as number, but may overflow in JS - •Use
i64for large numbers (JS safe integer limit: 2^53)
- •Rust
- •
Serialization Failures
- •Missing
#[derive(Serialize, Deserialize)] - •Circular references (use
#[serde(skip)]) - •Non-serializable types (file handles, closures)
- •Missing
- •
Event Memory Leaks
- •Always unlisten in
useEffectcleanup - •Remove listeners when components unmount
- •Always unlisten in
Reference Materials
For detailed examples and patterns, see:
- •
references/command_patterns.md- Common IPC command patterns - •
references/type_mappings.md- Rust ↔ TypeScript type reference - •
references/event_patterns.md- Event-driven communication examples
Development Workflow
When implementing new IPC features:
- •Define Rust types in
src-tauri/src/types.rs - •Implement command in
src-tauri/src/commands.rs - •Register command in
src-tauri/src/lib.rs - •Mirror types in
lib/types.ts - •Create wrapper in
lib/tauri.ts - •Test with curl or Tauri dev tools
- •Integrate in React components
- •Add error handling throughout the stack
Performance Benchmarks
Target response times:
- •Simple queries (get settings): < 1ms
- •File I/O (load profile): < 10ms
- •Config write (apply EQ): < 50ms
- •Heavy computation (FFT analysis): < 100ms
If commands exceed these targets:
- •Profile with
cargo flamegraph - •Consider caching frequently accessed data
- •Move heavy work to separate threads
- •Use streaming for large data sets
Support
For Tauri-specific questions:
- •Tauri v2 Docs
- •Tauri Discord
- •Check
examples/in the Tauri repository