Skip to content
Learni
View all tutorials
Développement Desktop

How to Create a Desktop App with Tauri in 2026

Lire en français

Introduction

Tauri is a revolutionary open-source framework for building cross-platform desktop apps (Windows, macOS, Linux) that combine web frontend technologies (HTML, CSS, JS/TS) with a high-performance, secure Rust backend. Unlike Electron, which bundles Chromium (150+ MB per app), Tauri leverages the system's native WebView (10-20 MB), dramatically reducing size and memory usage. In 2026, Tauri v2's capabilities enable fine-grained permission control, preventing common security vulnerabilities. This expert tutorial walks you through creating a complete task manager app with file persistence, async Rust calls, and optimized builds. Ideal for senior devs prioritizing scalability, zero-trust security, and native performance. By the end, your app will be production-ready, signed, and distributable as AppImage, MSI, or DMG.

Prerequisites

  • Rust: Stable version (1.80+), install via rustup.rs
  • Node.js: 20+ with npm/yarn/pnpm
  • Build tools: Visual Studio (Windows), Xcode (macOS), or clang/gcc (Linux)
  • Optional tools: Git, an editor like VS Code with Rust/Tauri extensions
  • Knowledge: Intermediate Rust, TypeScript, Web APIs (Fetch, DOM)

Install Tauri CLI

terminal
cargo install tauri-cli --version "^2.0"
npm install -g @tauri-apps/cli

tauri --version
tauri doctor

This installs the Tauri v2 CLI via Cargo for Rust tools and npm for JS wrappers. tauri doctor checks your environment (WebView2 on Windows, etc.). Stick to stable releases for production; update with cargo update if needed.

Verification and First Command

Run tauri doctor to validate system dependencies. On Windows, install WebView2 Runtime if missing. Think of it as a pre-op checkup—it prevents 90% of build failures. Then create your project using the Vite + Vanilla template for expert simplicity.

Create the Tauri Project

terminal
npm create tauri-app@latest mon-app-tauri
cd mon-app-tauri
npm install
npm run tauri dev

Creates a boilerplate project with Vite frontend and Tauri backend. npm run tauri dev starts hot-reload: Rust compiles in ~2s, WebView opens. Customize templates (React/Svelte) via prompts. Pitfall: On macOS, grant Developer Tools permissions in Security settings.

Project Structure

The src-tauri folder holds the Rust backend (Cargo.toml, main.rs, tauri.conf.json). src manages the web frontend. v2 capabilities separate permissions into isolated JSON files, like RBAC roles. Edit src-tauri/capabilities/default.json to expose APIs without the outdated allowlist.

Configure tauri.conf.json

src-tauri/tauri.conf.json
{
  "productName": "MonAppTauri",
  "version": "1.0.0",
  "identifier": "com.exemple.monapp",
  "build": {
    "devPath": "../src",
    "distDir": "../dist"
  },
  "app": {
    "windows": [{
      "title": "Gestionnaire Tâches",
      "width": 800,
      "height": 600,
      "resizable": true
    }],
    "security": {
      "csp": null
    }
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": [
      "icons/32x32.png",
      "icons/128x128.png",
      "icons/128x128@2x.png",
      "icons/icon.icns",
      "icons/icon.ico"
    ]
  }
}

Sets app name, windows, multi-platform bundling, and icons (pre-generate them). identifier must be unique (reverse-domain style). In v2, null CSP allows eval() for better DX; tighten it in production. Pitfall: Forget distDir and Vite won't build.

Define Capabilities

src-tauri/capabilities/main.json
{
  "$schema": "https://schema.tauri.app/config/2.0.0/capability",
  "identifier": "main-capability",
  "description": "Capabilities principales",
  "local": true,
  "windows": ["main"],
  "permissions": [
    "core:default",
    {
      "identifier": "fs:allow-read",
      "allow": [
        { "path": "$APPDATA/../local/tasks.json" }
      ]
    },
    {
      "identifier": "shell:allow-execute",
      "allow": [{
        "name": "ls",
        "args": true,
        "sidecar": false
      }]
    }
  ]
}

Sets granular permissions: read JSON in AppData, execute shell commands. $APPDATA resolves natively. Link in tauri.conf.json via capabilities: ["main-capability"]. v2 advantage: window isolation and secure audits.

Implement the Rust Backend

Create JS-invokable commands with invoke. Use serde for JSON, std::fs for persistence. Analogy: Rust as a bodyguard—checks everything before DOM access.

Rust Commands (main.rs)

src-tauri/src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use serde::{Deserialize, Serialize};
use std::fs;
use tauri::{command, Manager};

#[derive(Serialize, Deserialize)]
struct Task {
    id: u32,
    title: String,
    done: bool,
}

type Tasks = Vec<Task>;

const TASKS_FILE: &str = "tasks.json";

#[command]
fn get_tasks() -> Result<Tasks, String> {
    let path = dirs::data_local_dir()
        .ok_or("Impossible de trouver data dir")?
        .join(TASKS_FILE);
    if !path.exists() {
        return Ok(vec![]);
    }
    let data = fs::read_to_string(&path).map_err(|e| e.to_string())?;
    let tasks: Tasks = serde_json::from_str(&data).unwrap_or(vec![]);
    Ok(tasks)
}

#[command]
fn add_task(title: String) -> Result<Tasks, String> {
    let mut tasks = get_tasks().unwrap_or(vec![]);
    let new_id = tasks.len() as u32 + 1;
    tasks.push(Task { id: new_id, title, done: false });
    let path = dirs::data_local_dir().unwrap().join(TASKS_FILE);
    fs::write(&path, serde_json::to_string_pretty(&tasks).unwrap()).map_err(|e| e.to_string())?;
    Ok(tasks)
}

#[command]
fn toggle_task(id: u32) -> Result<Tasks, String> {
    let mut tasks = get_tasks().map_err(|e| e)?;
    if let Some(task) = tasks.iter_mut().find(|t| t.id == id) {
        task.done = !task.done;
        let path = dirs::data_local_dir().unwrap().join(TASKS_FILE);
        fs::write(&path, serde_json::to_string_pretty(&tasks).unwrap()).map_err(|e| e.to_string())?;
    }
    Ok(tasks)
}

fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_fs::init())
        .invoke_handler(tauri::generate_handler![get_tasks, add_task, toggle_task])
        .run(tauri::generate_context!())
        .expect("Erreur lors du run Tauri");
}

Implements task CRUD with local JSON persistence. dirs handles OS-safe paths. Add tauri-plugin-fs to Cargo.toml. Common error: Forgetting windows_subsystem (shows console on Windows). Secured via capabilities.

TypeScript Frontend (main.tsx)

src/main.tsx
import { createRoot } from 'react-dom/client';
import React, { useState, useEffect } from 'react';
import { invoke } from '@tauri-apps/api/tauri';
import { listen } from '@tauri-apps/api/event';

type Task = {
  id: number;
  title: string;
  done: boolean;
};

const App: React.FC = () => {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [newTask, setNewTask] = useState('');

  const loadTasks = async () => {
    try {
      const loaded: Task[] = await invoke('get_tasks');
      setTasks(loaded);
    } catch (e) {
      console.error(e);
    }
  };

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

  const addTask = async () => {
    if (!newTask.trim()) return;
    try {
      const updated: Task[] = await invoke('add_task', { title: newTask });
      setTasks(updated);
      setNewTask('');
    } catch (e) {
      console.error(e);
    }
  };

  const toggleTask = async (id: number) => {
    try {
      const updated: Task[] = await invoke('toggle_task', { id });
      setTasks(updated);
    } catch (e) {
      console.error(e);
    }
  };

  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h1>Gestionnaire de Tâches Tauri</h1>
      <div>
        <input
          value={newTask}
          onChange={(e) => setNewTask(e.target.value)}
          placeholder="Nouvelle tâche"
          style={{ marginRight: '10px', padding: '8px' }}
        />
        <button onClick={addTask}>Ajouter</button>
      </div>
      <ul style={{ listStyle: 'none', padding: 0 }}>
        {tasks.map((task) => (
          <li key={task.id} style={{ margin: '10px 0' }}>
            <input
              type="checkbox"
              checked={task.done}
              onChange={() => toggleTask(task.id)}
            />
            <span style={{ textDecoration: task.done ? 'line-through' : 'none', marginLeft: '10px' }}>
              {task.title}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
};

const root = createRoot(document.getElementById('root') as HTMLElement);
root.render(<App />);

Simple React frontend using invoke to call Rust. Requires @tauri-apps/api. Vite enables hot-reload. Install with npm i react react-dom @types/react @types/react-dom @tauri-apps/api. Pitfall: Async without try/catch crashes the app.

Update Cargo.toml

src-tauri/Cargo.toml
[package]
name = "mon-app-tauri"
version = "1.0.0"
edition = "2021"

[build-dependencies]
 tauri-build = { version = "2.0", features = [] }

[dependencies]
 tauri = { version = "2.0", features = [ "shell-open", "protocol-asset" ] }
 tauri-plugin-fs = "2.0"
 serde = { version = "1.0", features = [ "derive" ] }
 serde_json = "1.0"
 tokio = { version = "1", features = [ "full" ] }
 dirs = "5.0"

Dependencies for Tauri v2, FS plugin, JSON, and Tokio async. tauri-build provides context. Use edition 2021 for modern features. Run cargo check after edits.

Production Build

terminal
npm run tauri build
# Ou pour un target spécifique
npm run tauri build -- --target x86_64-apple-darwin
# Signer sur macOS (exemple)
security find-identity -v -p codesigning

Generates native bundles (EXE/DMG/AppImage). Use --target for cross-compilation (install toolchains via rustup). Sign on macOS/Windows for stores. Final size ~15 MB vs 200+ MB for Electron.

Best Practices

  • Capabilities first: Always isolate permissions by window/role; audit with tauri audit.
  • Rust for core logic: Offload heavy computations (crypto, DB) to native; keep JS for UI only.
  • Official plugins: Use sql for SQLite, store for key-value instead of raw FS.
  • End-to-end tests: cargo test + Playwright for UI; CI with GitHub Actions.
  • Updater: Integrate tauri-plugin-updater for auto-updates via GitHub Releases.

Common Errors to Avoid

  • Missing permissions: invoke fails silently; check Rust logs with RUST_LOG=debug npm run tauri dev.
  • Absolute paths: Always use $APPDATA/$HOME; dirs crate for portability.
  • Slow hot-reload: Increase devServer timeout in tauri.conf; avoid watching node_modules.
  • Cross-platform builds: Install Rust targets (rustup target add); test on VMs before merging.

Next Steps

Dive into the Tauri v2 docs, explore plugins like tauri-plugin-sql for embedded DB or tauri-plugin-global-shortcut. For in-depth Rust/Tauri mastery, check our certified Learni trainings. Contribute on GitHub for custom plugins.