Getting Started with Tauri Desktop Applications

For years, building cross-platform desktop applications meant relying on Electron. While Electron revolutionized desktop development by allowing web developers to use HTML, CSS, and JavaScript, it came with a heavy cost: every app shipped with its own complete copy of the Chromium browser and the Node.js runtime. This resulted in huge executable sizes (often over 100MB for a simple “Hello World”), high memory consumption, and sluggish launch times.

Tauri represents a paradigm shift. Instead of embedding Chromium, Tauri uses the operating system’s native webview (WebKit on macOS, WebView2 on Windows, and WebKitGTK on Linux). Instead of Node.js, Tauri uses Rust to handle system-level operations, file access, and native integration. The result is desktop applications that are incredibly lightweight—often under 10MB—launch almost instantly, and use a fraction of the memory that an equivalent Electron app would require.

In this chapter, we explore testUI-app, a Tauri v2 starter application built with Vite and vanilla TypeScript. It is the simplest possible demonstration of how Tauri bridges a web-based user interface with a native desktop backend. We will examine the project structure, walk through the HTML and TypeScript frontend, configure Vite and Tauri, write our first Rust command, and run the complete application.

The examples for this chapter are located in the testUI-app directory.

* * *

Project Structure

A Tauri application is split into two parts: the frontend (web technologies) and the backend (Rust). The frontend code lives in the root directory and the standard src/ directory, while the backend code lives under the src-tauri/ directory:

 1 testUI-app/
 2 ├── index.html              // Web UI entry point
 3 ├── package.json            // npm dependencies and dev scripts
 4 ├── tsconfig.json           // TypeScript compiler configuration
 5 ├── vite.config.ts          // Vite bundler and dev server config
 6 ├── src/
 7 │   ├── main.ts             // Frontend application logic (TypeScript)
 8 │   ├── styles.css          // Application styles (CSS)
 9 │   └── assets/             // SVG logos and static assets
10 └── src-tauri/
11     ├── Cargo.toml          // Rust package dependencies
12     ├── tauri.conf.json      // Tauri project configuration
13     └── src/
14         ├── lib.rs           // Rust shared application logic (commands)
15         └── main.rs          // Rust binary entry point

Let’s look at each of these files and see how they contribute to building our desktop application.

* * *

The HTML Entry Point

The index.html file defines the visual structure of the application. It looks like a standard webpage, but since it runs inside Tauri’s webview, it will render as a native desktop window.

Here is the complete source for index.html:

 1 <!doctype html>
 2 <html lang="en">
 3   <head>
 4     <meta charset="UTF-8" />
 5     <link rel="stylesheet" href="/src/styles.css" />
 6     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 7     <title>Tauri App</title>
 8     <script type="module" src="/src/main.ts" defer></script>
 9   </head>
10   <body>
11     <main class="container">
12       <h1>Welcome to Tauri</h1>
13       <div class="row">
14         <a href="https://vite.dev" target="_blank">
15           <img src="/src/assets/vite.svg" class="logo vite" alt="Vite logo" />
16         </a>
17         <a href="https://tauri.app" target="_blank">
18           <img src="/src/assets/tauri.svg" class="logo tauri" alt="Tauri logo" />
19         </a>
20         <a href="https://www.typescriptlang.org/docs" target="_blank">
21           <img src="/src/assets/typescript.svg" class="logo typescript" alt="typescript logo" />
22         </a>
23       </div>
24       <p>Click on the Tauri logo to learn more about the framework</p>
25       <form class="row" id="greet-form">
26         <input id="greet-input" placeholder="Enter a name..." />
27         <button type="submit">Greet</button>
28       </form>
29       <p id="greet-msg"></p>
30     </main>
31   </body>
32 </html>

Walkthrough

  • Line 5: We link to /src/styles.css to load our styling.
  • Line 8: We load /src/main.ts using type="module" and the defer attribute. During development, Vite intercepts this script request and delivers the compiled TypeScript directly to the browser view.
  • Lines 12–23: A layout of links and logo images showcasing the technologies used: Vite, Tauri, and TypeScript.
  • Lines 25–28: A simple <form> containing a text input field (#greet-input) and a submit button. Wrapping input fields in a form element is a best practice because it naturally handles the “Enter” keypress event to submit the value, rather than requiring us to write custom keydown event listeners.
  • Line 29: An empty paragraph element (#greet-msg) that will display the greeting text returned from the Rust backend once the form is submitted.
* * *

Frontend Application Logic

The frontend logic is written in TypeScript and lives in src/main.ts. Its job is to capture user input, communicate with the Rust backend via Tauri’s inter-process communication (IPC) channel, and update the DOM with the result.

Here is the complete source for src/main.ts:

 1 import { invoke } from "@tauri-apps/api/core";
 2 
 3 let greetInputEl: HTMLInputElement | null;
 4 let greetMsgEl: HTMLElement | null;
 5 
 6 async function greet() {
 7   if (greetMsgEl && greetInputEl) {
 8     greetMsgEl.textContent = await invoke("greet", {
 9       name: greetInputEl.value,
10     });
11   }
12 }
13 
14 window.addEventListener("DOMContentLoaded", () => {
15   greetInputEl = document.querySelector("#greet-input");
16   greetMsgEl = document.querySelector("#greet-msg");
17   document.querySelector("#greet-form")?.addEventListener("submit", (e) => {
18     e.preventDefault();
19     greet();
20   });
21 });

Walkthrough

  • Line 1: We import the invoke function from the Tauri API (@tauri-apps/api/core). This function is the IPC bridge. Under the hood, it serializes arguments into JSON, transmits them to the Rust backend, and returns a Promise that resolves when the Rust backend sends a response back.
  • Lines 3–4: We define variables to hold references to our DOM elements. We type them as HTMLInputElement and HTMLElement or null to satisfy the TypeScript compiler’s strict safety checks.
  • Lines 6–12: The greet() asynchronous function. If our DOM element references are not null, we call invoke("greet", { name: greetInputEl.value }). The first argument is the name of the Rust command we want to call. The second argument is a payload object mapping key-value arguments. The return value is awaited and assigned directly to the text content of greetMsgEl.
  • Lines 14–21: We listen for the DOMContentLoaded event to ensure the DOM tree is fully parsed before querying elements. Once loaded, we lookup #greet-input and #greet-msg, and add a submit listener to our form (#greet-form).
  • Line 18: Inside the form submit handler, we call e.preventDefault() to stop the browser from reloading the page when the form is submitted. We then trigger the asynchronous greet() function.
* * *

Configuring Vite and Tauri

Vite Configuration

Vite is a modern frontend build tool that is incredibly fast. Tauri uses it during development as a hot-reloading dev server, and during builds to bundle the HTML/TypeScript assets.

Here is vite.config.ts:

 1 import { defineConfig } from "vite";
 2 
 3 const host = process.env.TAURI_DEV_HOST;
 4 
 5 export default defineConfig(async () => ({
 6   clearScreen: false,
 7   server: {
 8     port: 1420,
 9     strictPort: true,
10     host: host || false,
11     hmr: host ? { protocol: "ws", host, port: 1421 } : undefined,
12     watch: { ignored: ["**/src-tauri/**"] },
13   },
14 }));

Walkthrough

  • Line 3: When running on mobile devices or network interfaces, Tauri sets the TAURI_DEV_HOST environment variable to notify Vite of the host address.
  • Line 6: We set clearScreen: false so that compilation output from both Vite and Cargo (Rust’s build tool) remains visible in the terminal.
  • Line 9: We lock the dev server to port 1420 so that Tauri’s backend knows exactly where to load the UI during development.
  • Line 10: We enforce strictPort: true so Vite will fail immediately instead of automatically choosing another port if 1420 is already in use.
  • Line 12: We instruct Vite’s file watcher to ignore changes inside src-tauri/. Without this, editing Rust backend files would cause Vite to rebuild the frontend, resulting in redundant compiler overhead.

Tauri Configuration

Tauri is configured via a JSON file at src-tauri/tauri.conf.json. This configuration file tells Tauri how to build the application, what size the window should be, and what security settings to apply.

Here is the key configuration chunk:

 1 {
 2   "$schema": "https://schema.tauri.app/config/2",
 3   "productName": "testui",
 4   "version": "0.1.0",
 5   "identifier": "com.markwatson.testui",
 6   "build": {
 7     "beforeDevCommand": "npm run dev",
 8     "devUrl": "http://localhost:1420",
 9     "beforeBuildCommand": "npm run build",
10     "frontendDist": "../dist"
11   },
12   "app": {
13     "withGlobalTauri": true,
14     "windows": [{ "title": "testui", "width": 800, "height": 600 }],
15     "security": { "csp": null }
16   },
17   "bundle": {
18     "active": true,
19     "targets": "all",
20     "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"]
21   }
22 }

Walkthrough

  • Lines 5–11: Under build, we specify the shell commands Tauri should run. When we launch Tauri in dev mode, it runs npm run dev (Vite) and connects the desktop window webview to http://localhost:1420. When bundling the final app, it runs npm run build and loads the resulting assets from the ../dist directory.
  • Lines 12–16: The app block configures the window. We create a window titled "testui" with dimensions 800x600.
  • Lines 17–21: The bundle configuration defines icons and packaging parameters. When building, Tauri packages our application into native installers (.app on macOS, .msi on Windows, and .deb on Linux).
* * *

The Rust Backend

The backend is written in Rust. It serves as our direct link to the operating system. In the testUI-app project, the Rust code is structured as a library (src-tauri/src/lib.rs) and an executable binary wrapper (src-tauri/src/main.rs). This separation is a Tauri v2 convention that simplifies compilation across desktop and mobile targets.

Let’s look at src-tauri/src/lib.rs first, where our commands are defined and registered:

 1 #[tauri::command]
 2 fn greet(name: &str) -> String {
 3     format!("Hello, {}! You've been greeted from Rust!", name)
 4 }
 5 
 6 #[cfg_attr(mobile, tauri::mobile_entry_point)]
 7 pub fn run() {
 8     tauri::Builder::default()
 9         .plugin(tauri_plugin_opener::init())
10         .invoke_handler(tauri::generate_handler![greet])
11         .run(tauri::generate_context!())
12         .expect("error while running tauri application");
13 }

Walkthrough

  • Line 1: The #[tauri::command] attribute is a Rust macro. It generates the necessary serialization boilerplate to expose our Rust function to the TypeScript frontend.
  • Lines 2–4: The greet function. It takes a string slice (&str) and returns a new String. We use Rust’s format! macro to construct a friendly greeting. Tauri automatically serializes this returned string into JSON and passes it back across the IPC bridge to our frontend promise.
  • Line 6: The #[cfg_attr(mobile, tauri::mobile_entry_point)] attribute ensures that if we build this project for mobile devices (iOS or Android), Rust knows how to launch the application using the correct entry point.
  • Lines 7–13: The run function initializes and starts our application window:
    • tauri::Builder::default() creates a new window builder.
    • .plugin(tauri_plugin_opener::init()) initializes a system plugin that lets our app open native browser links or files.
    • .invoke_handler(tauri::generate_handler![greet]) registers our greet command so that when invoke("greet", ...) is called on the frontend, Tauri knows to route it to this function.
    • .run(tauri::generate_context!()) compiles configuration variables and starts the window event loop.

Now, let’s examine src-tauri/src/main.rs which serves as the executable’s entry point:

1 // Prevents additional console window on Windows in release, DO NOT REMOVE!!
2 #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3 
4 fn main() {
5     testui_lib::run()
6 }

Walkthrough

  • Line 2: The #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] attribute tells the compiler that in release mode on Windows, the application should run silently in the background without spawning a command prompt window.
  • Lines 4–6: The standard Rust main function. All it does is delegate the execution to testui_lib::run(), which boots up our window and starts the event loop we defined in lib.rs.
* * *

Running the Example

To run this desktop application locally, follow these steps:

  1. Install Frontend Dependencies: Open a terminal in the testUI-app directory and install the necessary npm packages:

    1 npm install
    
  2. Run the Tauri Development Command: Launch the application in development mode:

    1 npm run tauri dev
    

What Happens Behind the Scenes?

When you run npm run tauri dev:

  1. Frontend dev server starts: Tauri executes npm run dev in the background, starting Vite.
  2. Vite compiles assets: Vite bundles the CSS and transpiles main.ts on-the-fly, serving them at http://localhost:1420.
  3. Rust compiles: Cargo parses Cargo.toml, compiles the Rust dependencies, and builds the backend binary.
  4. App launches: Tauri opens a native desktop window and configures the webview to load http://localhost:1420.
  5. IPC setup: Tauri links the frontend invoke("greet") calls to the Rust handler.

Expected Output

Once compilation finishes, a native desktop window will pop up. Type your name (for example, “Alice”) into the text input and click the Greet button or press Enter. The paragraph below the form will immediately display:

1 Hello, Alice! You've been greeted from Rust!

If you modify src/main.ts or src/styles.css while the application is running, the frontend webview will reload instantly to reflect your changes, thanks to Vite’s Hot Module Replacement (HMR). If you modify Rust files in src-tauri/, Tauri will automatically rebuild the Rust binary and relaunch the application window.

* * *

Wrap-up

The testUI-app starter project shows how lightweight and accessible modern desktop application development has become. By separating the UI logic from the system logic, Tauri gives us the best of both worlds:

  • A frontend built using web standards (TypeScript, CSS, and HTML) that is easy to write and style.
  • A secure, performance-oriented backend built in Rust for operations that require direct OS access.
  • An IPC layer that coordinates communication between these environments using a simple promise-based API.

With this simple scaffolding working, you are ready to construct more complex desktop interfaces, persistence layers, and local AI integrations.