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.cssto load our styling. - Line 8: We load
/src/main.tsusingtype="module"and thedeferattribute. 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
invokefunction 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
HTMLInputElementandHTMLElementornullto satisfy the TypeScript compiler’s strict safety checks. - Lines 6–12: The
greet()asynchronous function. If our DOM element references are not null, we callinvoke("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 ofgreetMsgEl. - Lines 14–21: We listen for the
DOMContentLoadedevent to ensure the DOM tree is fully parsed before querying elements. Once loaded, we lookup#greet-inputand#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 asynchronousgreet()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_HOSTenvironment variable to notify Vite of the host address. - Line 6: We set
clearScreen: falseso 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
1420so that Tauri’s backend knows exactly where to load the UI during development. - Line 10: We enforce
strictPort: trueso Vite will fail immediately instead of automatically choosing another port if1420is 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 runsnpm run dev(Vite) and connects the desktop window webview tohttp://localhost:1420. When bundling the final app, it runsnpm run buildand loads the resulting assets from the../distdirectory. - Lines 12–16: The
appblock configures the window. We create a window titled"testui"with dimensions800x600. - Lines 17–21: The
bundleconfiguration defines icons and packaging parameters. When building, Tauri packages our application into native installers (.appon macOS,.msion Windows, and.debon 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
greetfunction. It takes a string slice (&str) and returns a newString. We use Rust’sformat!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
runfunction 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 ourgreetcommand so that wheninvoke("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 inlib.rs.
Running the Example
To run this desktop application locally, follow these steps:
-
Install Frontend Dependencies: Open a terminal in the
testUI-appdirectory and install the necessary npm packages:1 npm install -
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:
- Frontend dev server starts: Tauri executes
npm run devin the background, starting Vite. - Vite compiles assets: Vite bundles the CSS and transpiles
main.tson-the-fly, serving them athttp://localhost:1420. - Rust compiles: Cargo parses
Cargo.toml, compiles the Rust dependencies, and builds the backend binary. - App launches: Tauri opens a native desktop window and configures the webview to load
http://localhost:1420. - 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.