Desktop Apps with Electron, Tauri & .NET MAUI — Complete Build Guide
In this tutorial, you'll learn about Desktop Apps with Electron, Tauri & .NET MAUI. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Desktop application development with Electron, Tauri, and .NET MAUI lets you build native-feeling applications for Windows, macOS, and Linux using web technologies, Rust, or C#, with trade-offs in bundle size, performance, and ecosystem maturity.
What You'll Learn & Why It Matters
In this tutorial you will learn how to build desktop apps using Electron (JavaScript), Tauri (Rust + web), and .NET MAUI (C#) — covering window management, file I/O, system tray, and native OS integration. Desktop apps remain essential for professional tools, creative software, and developer utilities that demand full OS access and offline capability.
Real-world use: DodaZIP's desktop file compression tool was originally built with Electron and is being rewritten in Tauri to reduce the installer size from 180 MB to under 8 MB while improving file extraction performance through native Rust bindings.
Prerequisites
- Familiarity with JavaScript, Rust, or C#
- Basic understanding of desktop OS concepts (file system, window management)
- Node.js installed (for Electron and Tauri)
Learning Path
flowchart LR A[Cross-Platform Tools] --> B[Desktop Apps] B --> C[Mobile CI/CD] B --> D[App Monetization] B:::current classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
Electron — Web Technologies for Desktop
Electron packages a Chromium browser and Node.js runtime into a desktop application, giving you full web APIs plus native OS access.
Basic Electron App
// main.js
const { app, BrowserWindow, ipcMain, dialog } = require("electron");
const path = require("path");
const fs = require("fs");
function createWindow() {
const win = new BrowserWindow({
width: 1024,
height: 768,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
win.loadFile("index.html");
}
app.whenReady().then(createWindow);
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});
// IPC handler: open file dialog and read a file
ipcMain.handle("open-file", async () => {
const result = await dialog.showOpenDialog({
properties: ["openFile"],
filters: [{ name: "Text Files", extensions: ["txt", "md"] }],
});
if (result.canceled) return null;
const content = fs.readFileSync(result.filePaths[0], "utf-8");
return { path: result.filePaths[0], content };
});
// preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
openFile: () => ipcRenderer.invoke("open-file"),
});
Expected behavior: The app opens a native file dialog. After selecting a file, the content loads into the renderer process. contextIsolation keeps the renderer secure from Node.js access.
Package.json for Electron
{
"name": "dodatext",
"version": "1.0.0",
"main": "main.js",
"scripts": {
"start": "electron .",
"build": "electron-builder --win --mac --linux"
},
"devDependencies": {
"electron": "^33.0.0",
"electron-builder": "^25.0.0"
},
"build": {
"appId": "com.dodatech.dodatext",
"productName": "DodaText",
"directories": { "output": "dist" },
"win": { "target": "nsis" },
"mac": { "target": "dmg" },
"linux": { "target": "AppImage" }
}
}
Expected behavior: Running npm run build produces platform-specific installers: .exe (Windows), .dmg (macOS), and .AppImage (Linux). The installer includes the Chromium runtime (~150 MB).
Tauri — Lightweight Rust + WebView
Tauri uses the OS-native WebView instead of Bundling Chromium. The backend is written in Rust, giving native performance and tiny binary sizes.
Tauri Project Structure
// src-tauri/src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::fs;
#[tauri::command]
fn list_directory(path: String) -> Result<Vec<String>, String> {
let entries = fs::read_dir(&path).map_err(|e| e.to_string())?;
let mut files = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| e.to_string())?;
files.push(entry.file_name().to_string_lossy().to_string());
}
Ok(files)
}
#[tauri::command]
fn compress_file(source: String, dest: String) -> Result<(), String> {
use std::process::Command;
let output = Command::new("zip")
.args(["-j", "&dest", &source])
.output()
.map_err(|e| e.to_string())?;
if output.status.success() {
Ok(())
} else {
Err(String::from_utf8_lossy(&output.stderr).to_string())
}
}
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![list_directory, compress_file])
.run(tauri::generate_context!())
.expect("error running tauri app");
}
// src-tauri/tauri.conf.json
{
"productName": "DodaZipQuick",
"version": "1.0.0",
"identifier": "com.dodatech.dodazipquick",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"app": {
"windows": [
{
"title": "DodaZipQuick",
"width": 900,
"height": 600,
"resizable": true
}
],
"security": {
"csp": "default-src 'self'; script-src 'self'"
}
}
}
Expected behavior: The Tauri app opens a native window with the WebView rendering the frontend. The Rust backend handles file system operations with near-zero overhead. The final binary is under 5 MB.
.NET MAUI — Native C# Desktop Apps
.NET MAUI (Multi-platform App UI) lets you build desktop apps using C# and XAML with native platform controls.
MAUI Desktop Window
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="DodaFileOps.MainPage"
Title="DodaFileOps">
<Grid Padding="20" RowDefinitions="Auto,Auto,*">
<Label Text="File Operations"
FontSize="24"
FontAttributes="Bold"
Grid.Row="0"
Margin="0,0,0,16"/>
<HorizontalStackLayout Grid.Row="1" Spacing="8">
<Button x:Name="PickFileBtn"
Text="Pick File"
Clicked="OnPickFile"
BackgroundColor="#006B5E"
TextColor="White"/>
<Button x:Name="CompressBtn"
Text="Compress"
Clicked="OnCompress"
BackgroundColor="#4A635C"
TextColor="White"/>
</HorizontalStackLayout>
<ListView x:Name="FileList" Grid.Row="2" Margin="0,16,0,0"/>
</Grid>
</ContentPage>
using Microsoft.Maui.Storage;
using System.IO.Compression;
using System.IO;
namespace DodaFileOps;
public partial class MainPage : ContentPage
{
private string _selectedFilePath;
public MainPage()
{
InitializeComponent();
}
private async void OnPickFile(object sender, EventArgs e)
{
var result = await FilePicker.PickAsync(new PickOptions
{
FileTypes = new FilePickerFileType(new Dictionary<DevicePlatform, IEnumerable<string>>
{
{ DevicePlatform.WinUI, new[] { ".txt", ".md", ".pdf" } },
{ DevicePlatform.macOS, new[] { "public.plain-text", "public.pdf" } },
})
});
if (result != null)
{
_selectedFilePath = result.FullPath;
FileList.ItemsSource = new[] {
$"Name: {result.FileName}",
$"Size: {FormatSize(result.FileSize)}",
$"Path: {result.FullPath}"
};
}
}
private async void OnCompress(object sender, EventArgs e)
{
if (string.IsNullOrEmpty(_selectedFilePath)) return;
var savePath = await FileSaver.SaveAsync("compressed.zip",
new MemoryStream());
if (savePath != null)
{
using var archive = ZipFile.Open(savePath, ZipArchiveMode.Create);
archive.CreateEntryFromFile(_selectedFilePath,
Path.GetFileName(_selectedFilePath));
await DisplayAlert("Done", $"Compressed to {savePath}", "OK");
}
}
private static string FormatSize(long bytes) => bytes switch
{
< 1024 => $"{bytes} B",
< 1048576 => $"{bytes / 1024.0:F1} KB",
_ => $"{bytes / 1048576.0:F1} MB"
};
}
Expected behavior: The MAUI app opens a native file picker dialog. After selecting a file, its details appear in the list. Clicking Compress creates a zip file using the system's native compression APIs.
Framework Comparison
flowchart TD
A[Desktop App Requirements] --> B{Bundle size constraint?}
B -->|Under 10 MB| C[Choose Tauri]
B -->|No constraint| D{Need web dev skills?}
D -->|Yes| E[Choose Electron]
D -->|No, C# team| F[Choose .NET MAUI]
C --> G[Best for: utilities, tools, security apps]
E --> H[Best for: complex web-based UIs, prototyping]
F --> I[Best for: enterprise LOB apps, Windows-first]
Common Errors & Mistakes
1. Context Isolation Disabled in Electron
Mistake: Setting contextIsolation: false, exposing Node.js and Electron APIs directly to the renderer, creating a major security vulnerability.
Fix: Always keep contextIsolation: true and expose specific APIs through contextBridge in the preload script.
2. Tauri CSP Blocking Local Assets
Mistake: Overly strict CSP in tauri.conf.json that blocks the frontend from loading local images or fonts.
Fix: Use "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost" to allow assets.
3. MAUI Windows-Specific API Calls on macOS
Mistake: Using <a href="/operating-systems/windows/">Windows</a>.System.UserProfile or other Windows-only APIs without platform checks, crashing on macOS.
Fix: Guard platform-specific code with #if <a href="/operating-systems/windows/">WINDOWS</a> or use DeviceInfo.Platform for runtime checks.
4. Not Handling Window Close on macOS
Mistake: Electron or Tauri apps that quit on window close, violating macOS convention where apps stay open until Cmd+Q.
Fix: On macOS, hide the window instead of quitting. Use app.on("window-all-closed", () => { if (process.platform !== "darwin") app.quit(); }).
5. Large Installer Size Due to Bundled Chromium
Mistake: Shipping a simple utility app as an Electron app with a 150 MB installer.
Fix: Use Tauri for lightweight utilities. Reserve Electron for apps that genuinely need Chromium's full rendering engine (e.g., collaborative editors, complex web apps).
Practice Questions
Question 1
What is the key architectural difference between Electron and Tauri?
Show answer
Electron bundles the full Chromium rendering engine (~150 MB). Tauri uses the OS-native WebView (already installed on the user's system), resulting in a binary 20-30 times smaller.Question 2
How does .NET MAUI achieve native UI rendering?
Show answer
MAUI maps XAML controls to native platform APIs. On Windows it uses WinUI 3, on macOS it uses AppKit, and on Linux it uses GTK. This provides native look and feel on each platform.Question 3
What is the purpose of contextBridge in Electron?
Show answer
contextBridge safely exposes selected Electron and Node.js APIs to the renderer process without disabling contextIsolation, maintaining security while providing needed functionality.Question 4
How does Tauri handle native OS APIs?
Show answer
Tauri provides a Rust-based command system. The frontend calls `invoke("command_name", args)` which executes Rust functions with full OS API access via the tauri and std modules.Challenge
Build the same simple file renamer application in all three frameworks: a UI that lists files in a selected directory, allows batch renaming with a prefix/suffix pattern, and shows a preview before applying changes. Compare implementation effort, binary size, and startup time.
Mini Project: System Tray Utility
Build a system tray application that monitors a folder for new files and displays a notification when a file matching a pattern arrives. In Electron use Tray and Notification APIs. In Tauri use the tray-icon plugin. In MAUI use platform-specific tray implementations. Include a preferences window to configure the watched folder and file pattern.
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Author: DodaTech | Last updated: June 22, 2026
DodaTech tutorials are built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro — security tools used by millions worldwide.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro