Skip to main content
โšก Calmops

Building Desktop Apps with Tauri 2.0: Complete Guide

Introduction

In the world of desktop application development, developers have long faced a difficult choice: use Electron and accept larger bundle sizes, or use traditional frameworks like Qt or GTK and deal with complex tooling. Enter Tauri 2.0โ€”a framework that combines the best of both worlds by letting you build desktop apps with web technologies while generating incredibly small, fast, and secure executables.

With over 74,000 GitHub stars and production adoption by companies worldwide, Tauri has become the go-to choice for developers who value performance and security. This guide will walk you through building desktop applications with Tauri 2.0, from setup to deployment.


What Is Tauri?

The Basic Concept

Tauri is a framework for building small, fast, and secure desktop applications using web frontend technologies. Unlike Electron, which bundles a full Chromium browser, Tauri uses the operating system’s native webview, resulting in dramatically smaller app sizes (often under 10MB vs 150MB+ for Electron).

Key Terms

  • WebView: The browser component embedded in desktop applications
  • Rust Backend: The server-side logic written in Rust that handles system operations
  • IPC (Inter-Process Communication): The mechanism for frontend-backend communication
  • Tauri Commands: Functions exposed from Rust to the frontend
  • App Bundle: The final distributable package (EXE, DMG, AppImage, etc.)

Why Tauri Matters in 2025-2026

Feature Electron Tauri
Bundle Size 150-200 MB 5-15 MB
Memory Usage 300-500 MB 50-100 MB
Startup Time 2-5 seconds < 1 second
Security Good Excellent
Native API Access Good Excellent

Architecture

How Tauri Works

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚           Frontend (Web)                โ”‚
โ”‚   HTML / CSS / JavaScript / TypeScript  โ”‚
โ”‚         React, Vue, Svelte, etc.        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                     โ”‚ IPC (HTTP/WS)
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚         Tauri Core (Rust)               โ”‚
โ”‚  - Window Management                     โ”‚
โ”‚  - System Tray                          โ”‚
โ”‚  - File System Access                   โ”‚
โ”‚  - Native Dialogs                       โ”‚
โ”‚  - App Lifecycle                        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                     โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚        Operating System                  โ”‚
โ”‚   Windows / macOS / Linux               โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Core Components

  1. Tauri Core: The Rust runtime that manages the application
  2. WebView: Native browser component (WebView2 on Windows, WKWebView on macOS, WebKit on Linux)
  3. IPC Layer: Communication bridge between frontend and backend
  4. Plugin System: Extendable functionality through plugins
  5. Build Tools: CLI for development and bundling

Getting Started

Prerequisites

# Install Node.js (for frontend)
node --version  # v18+

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustc --version  # 1.70+

# Install Tauri CLI
npm install -g @tauri-apps/cli@latest
tauri --version

Creating a New Project

# Create a new Tauri app
npm create tauri-app@latest my-tauri-app

# Select options:
# - Package name: my-tauri-app
# - Frontend: React (or Vue/Svelte/Vanilla)
# - TypeScript: Yes
# - Install dependencies: Yes

cd my-tauri-app
npm install

Project Structure

my-tauri-app/
โ”œโ”€โ”€ src/                    # Frontend source
โ”‚   โ”œโ”€โ”€ App.tsx
โ”‚   โ”œโ”€โ”€ main.tsx
โ”‚   โ””โ”€โ”€ styles.css
โ”œโ”€โ”€ src-tauri/              # Rust backend
โ”‚   โ”œโ”€โ”€ src/
โ”‚   โ”‚   โ””โ”€โ”€ main.rs
โ”‚   โ”œโ”€โ”€ Cargo.toml
โ”‚   โ”œโ”€โ”€ tauri.conf.json
โ”‚   โ””โ”€โ”€ icons/
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ vite.config.ts

Running the Development Server

# Start development mode
npm run tauri dev

# This will:
# 1. Build the frontend (Vite)
# 2. Compile the Rust backend
# 3. Launch the desktop app with hot reload

Building a Complete Application

Step 1: Configure the Application

// src-tauri/tauri.conf.json
{
  "productName": "My Tauri App",
  "version": "1.0.0",
  "identifier": "com.myapp.tauri",
  "build": {
    "devtools": true
  },
  "app": {
    "windows": [
      {
        "title": "My Tauri App",
        "width": 800,
        "height": 600,
        "resizable": true,
        "fullscreen": false,
        "center": true
      }
    ],
    "security": {
      "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost; style-src 'self' 'unsafe-inline'"
    }
  }
}

Step 2: Create Rust Backend Commands

// src-tauri/src/main.rs

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use tauri::Manager;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct TodoItem {
    pub id: u32,
    pub title: String,
    pub completed: bool,
}

// Command exposed to frontend
#[tauri::command]
fn get_todos() -> Vec<TodoItem> {
    vec![
        TodoItem { id: 1, title: "Learn Tauri".to_string(), completed: true },
        TodoItem { id: 2, title: "Build an app".to_string(), completed: false },
    ]
}

#[tauri::command]
fn add_todo(title: String) -> TodoItem {
    TodoItem {
        id: rand::random(),
        title,
        completed: false,
    }
}

#[tauri::command]
fn read_file(path: String) -> Result<String, String> {
    std::fs::read_to_string(path)
        .map_err(|e| e.to_string())
}

#[tauri::command]
async fn async_operation(data: String) -> Result<String, String> {
    // Async Rust code
    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
    Ok(format!("Processed: {}", data))
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            get_todos,
            add_todo,
            read_file,
            async_operation
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Step 3: Build the Frontend

// src/App.tsx
import { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { open } from '@tauri-apps/plugin-dialog';
import './styles.css';

interface TodoItem {
  id: number;
  title: string;
  completed: boolean;
}

function App() {
  const [todos, setTodos] = useState<TodoItem[]>([]);
  const [newTodo, setNewTodo] = useState('');

  useEffect(() => {
    loadTodos();
  }, []);

  const loadTodos = async () => {
    const items = await invoke<TodoItem[]>('get_todos');
    setTodos(items);
  };

  const handleAddTodo = async () => {
    if (!newTodo.trim()) return;
    const item = await invoke<TodoItem>('add_todo', { title: newTodo });
    setTodos([...todos, item]);
    setNewTodo('');
  };

  const handleOpenFile = async () => {
    const file = await open({
      multiple: false,
      filters: [{ name: 'Text', extensions: ['txt', 'md'] }]
    });
    if (file) {
      const content = await invoke<string>('read_file', { path: file });
      console.log('File content:', content);
    }
  };

  return (
    <div className="container">
      <h1>Tauri Todo App</h1>
      
      <div className="input-group">
        <input
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="Add a new todo..."
        />
        <button onClick={handleAddTodo}>Add</button>
      </div>

      <ul className="todo-list">
        {todos.map(todo => (
          <li key={todo.id} className={todo.completed ? 'completed' : ''}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => {}}
            />
            <span>{todo.title}</span>
          </li>
        ))}
      </ul>

      <button onClick={handleOpenFile} className="secondary">
        Open File
      </button>
    </div>
  );
}

export default App;

Step 4: Add System Integration

// System tray example
use tauri::{
    menu::{Menu, MenuItem},
    tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
    Manager,
};

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
            let show = MenuItem::with_id(app, "show", "Show Window", true, None::<&str>)?;
            let menu = Menu::with_items(app, &[&show, &quit])?;

            TrayIconBuilder::new()
                .menu(&menu)
                .menu_on_left_click(false)
                .on_menu_event(|app, event| {
                    match event.id.as_ref() {
                        "quit" => {
                            app.exit(0);
                        }
                        "show" => {
                            if let Some(window) = app.get_webview_window("main") {
                                let _ = window.show();
                                let _ = window.set_focus();
                            }
                        }
                        _ => {}
                    }
                })
                .build(app)?;

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Best Practices

1. Use TypeScript for Frontend

// โœ… Good: Type-safe commands
import { invoke } from '@tauri-apps/api/core';

interface ApiResponse {
  success: boolean;
  data: string;
}

const result = await invoke<ApiResponse>('my_command', { arg: 'value' });

// โŒ Bad: Any type loses type safety
const result = await invoke('my_command', { arg: 'value' });

2. Handle Errors Properly

// โœ… Good: Proper error handling
try {
  const result = await invoke<string>('risky_operation');
  console.log(result);
} catch (error) {
  console.error('Operation failed:', error);
}

3. Minimize IPC Calls

// โœ… Good: Batch operations
const results = await invoke<number[]>('batch_calculate', { 
  items: data 
});

// โŒ Bad: Multiple IPC calls
const results = [];
for (const item of data) {
  results.push(await invoke<number>('calculate_single', { item }));
}

4. Use Plugins for Complex Features

# Install plugins
npm install @tauri-apps/plugin-dialog
npm install @tauri-apps/plugin-fs
npm install @tauri-apps/plugin-shell
npm install @tauri-apps/plugin-store

Common Pitfalls

1. Forgetting Permissions

Wrong: Trying to access file system without permission

// tauri.conf.json - Missing permissions!
{
  "plugins": {}
}

Correct:

// tauri.conf.json
{
  "plugins": {
    "fs": {
      "scope": {
        "allow": ["$APPDATA/**", "$HOME/**"],
        "deny": ["$HOME/.ssh/**"]
      }
    }
  }
}

2. Blocking the Main Thread

Wrong: Heavy computation in synchronous command

#[tauri::command]
fn heavy_computation() -> u64 {
    // This blocks the app!
    (0..1_000_000).sum()
}

Correct: Use async

#[tauri::command]
async fn heavy_computation() -> u64 {
    // Runs in background
    tokio::task::spawn_blocking(|| {
        (0..1_000_000).sum()
    }).await.unwrap()
}

3. Not Handling Window Events

// โœ… Good: Handle window close
fn main() {
    tauri::Builder::default()
        .on_window_event(|window, event| {
            if let tauri::WindowEvent::CloseRequested { api, .. } = event {
                // Save state before closing
                save_state();
                api.prevent_close();
                window.close();
            }
        })
        .run(tauri::generate_context!())
        .expect("error");
}

Building and Distribution

Development Build

# Development mode
npm run tauri dev

# Build for current platform
npm run tauri build

Cross-Platform Builds

# Windows (from any platform)
npm run tauri build -- --target x86_64-pc-windows-msvc

# macOS
npm run tauri build -- --target x86_64-apple-darwin
npm run tauri build -- --target aarch64-apple-darwin

# Linux
npm run tauri build -- --target x86_64-unknown-linux-gnu

Output

After building, you’ll find:

src-tauri/target/release/
โ”œโ”€โ”€ my-tauri-app.exe      # Windows executable
โ”œโ”€โ”€ my-tauri-app          # Linux binary
โ”œโ”€โ”€ my-tauri-app.dmg      # macOS disk image
โ””โ”€โ”€ bundle/
    โ”œโ”€โ”€ msi/              # Windows installer
    โ”œโ”€โ”€ dmg/              # macOS package
    โ””โ”€โ”€ appimage/         # Linux AppImage

External Resources

Official Documentation

GitHub & Examples

Learning Resources

Tools


Conclusion

Tauri 2.0 represents a significant advancement in desktop application development, offering an compelling alternative to Electron with dramatically smaller bundle sizes, better performance, and stronger security. Whether you’re building a simple utility or a complex application, Tauri provides the tools and flexibility needed to create professional desktop software.

The combination of Rust’s performance and safety with the familiarity of web frontend technologies makes Tauri an excellent choice for developers in 2025 and beyond.


Key Takeaways

  • Tauri 2.0 uses native webviews, resulting in 10-20x smaller apps than Electron
  • Frontend + Backend architecture provides flexibility and performance
  • IPC Commands bridge Rust backend and web frontend
  • Plugins extend functionality for dialogs, file system, and more
  • Cross-platform builds work from a single codebase
  • Best practices include TypeScript, async Rust, and proper error handling

Next Steps: Explore Deno: The Rust-Based JavaScript Runtime to see another area where Rust excels.

Comments