Why Desktop Apps in 2025? Because the Web Isn’t Everything

Let’s be honest—we all love JavaScript. It’s everywhere. You can build web apps, mobile apps, CLI tools, and even smart toasters with it (probably). But there’s one frontier that sometimes feels left behind: the desktop. Sure, you could argue that web apps are sufficient, but there’s something satisfying about shipping a native-looking application that works offline, has real file system access, and doesn’t require users to open yet another browser tab. Enter Electron: the framework that lets you build desktop applications using the same technologies you already know—HTML, CSS, and JavaScript. It’s the technology behind popular applications like VS Code, Slack, Discord, and Figma. If these companies trust Electron for their products, maybe it’s worth your attention too.

Understanding Electron’s Architecture

Before we dive into code, let’s understand what makes Electron tick. Think of it as a Swiss Army knife that combines two powerful tools: Chromium (the engine behind Chrome) and Node.js (server-side JavaScript runtime). By merging these two, Electron gives you the best of both worlds—a powerful rendering engine and full operating system access.

graph TB Main["Main Process
(Node.js Runtime)
Full OS Access"] Preload["Preload Script
(Security Bridge)"] Renderer["Renderer Process
(Chromium)
UI & User Interaction"] Main -->|IPC Messages| Preload Preload -->|Exposed APIs| Renderer Renderer -->|IPC Messages| Main Main -->|File System| OS["Operating System"] Main -->|System APIs| OS

An Electron application has two main processes: the main process (Node.js with full OS access) and renderer processes (Chromium instances that handle your UI). They communicate through IPC (Inter-Process Communication), with preload scripts serving as the security gateway between them.

Setting Up Your First Electron Project

Let’s get practical. I’ll walk you through creating a simple but complete desktop application—think of it as a “Hello Electron” on steroids.

Step 1: Project Initialization

Create a new directory and initialize your project:

mkdir my-awesome-app
cd my-awesome-app
npm init -y
npm install --save-dev electron

Why install Electron as a dev dependency? Because when you package your application, Electron becomes part of the bundle. Your users won’t need to install Node.js separately—it’s all baked in. Neat, right?

Step 2: Configure Your Entry Point

Update your package.json to point to your main process file and add a start script:

{
  "name": "my-awesome-app",
  "version": "1.0.0",
  "description": "My first Electron app",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "devDependencies": {
    "electron": "^31.0.0"
  }
}

Step 3: Create the Main Process

Create main.js in your project root. This is where the magic begins:

const { app, BrowserWindow } = require('electron');
const path = require('path');
let mainWindow;
// Create the browser window when the app is ready
function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      sandbox: true
    }
  });
  // Load the index.html file
  mainWindow.loadFile('index.html');
  // Open DevTools in development (optional)
  mainWindow.webContents.openDevTools();
}
// App event listeners
app.on('ready', createWindow);
app.on('window-all-closed', () => {
  // On macOS, apps typically stay open until explicitly closed
  if (process.platform !== 'darwin') {
    app.quit();
  }
});
app.on('activate', () => {
  // On macOS, re-create the window when the dock icon is clicked
  if (mainWindow === null) {
    createWindow();
  }
});

Notice the webPreferences object? The preload property points to a script that we’ll create next. The sandbox: true setting makes your app more secure by isolating the renderer process.

Step 4: Create Your HTML UI

Create index.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>My Awesome Electron App</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
            margin: 0;
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            color: #333;
        }
        .container {
            max-width: 600px;
            margin: 0 auto;
            background: white;
            padding: 40px;
            border-radius: 10px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.3);
        }
        h1 {
            color: #667eea;
            margin-top: 0;
        }
        button {
            background: #667eea;
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            transition: background 0.3s;
        }
        button:hover {
            background: #764ba2;
        }
        #output {
            margin-top: 20px;
            padding: 15px;
            background: #f5f5f5;
            border-radius: 5px;
            min-height: 50px;
            font-family: 'Courier New', monospace;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🚀 Welcome to Electron</h1>
        <p>Click the button below to see Electron in action:</p>
        <button id="btn">Get System Info</button>
        <div id="output">Click the button to see system information here...</div>
    </div>
    <script src="renderer.js"></script>
</body>
</html>

Step 5: The Preload Script (Security First!)

Create preload.js. This is crucial—it’s your security checkpoint:

const { contextBridge, ipcRenderer } = require('electron');
const os = require('os');
const path = require('path');
// Expose specific APIs to the renderer process
contextBridge.exposeInMainWorld('electronAPI', {
  getSystemInfo: () => ipcRenderer.invoke('get-system-info'),
  openFile: (filePath) => ipcRenderer.invoke('open-file', filePath),
  onSystemEvent: (callback) => ipcRenderer.on('system-event', callback)
});

Think of the preload script as a bouncer at an exclusive club. It carefully decides what from Node.js gets to talk to your UI. This prevents malicious code from hijacking your app’s capabilities.

Step 6: Handle IPC Messages in Main Process

Add this to your main.js (after the app setup):

const { ipcMain } = require('electron');
// Listen for IPC messages from the renderer
ipcMain.handle('get-system-info', async () => {
  return {
    platform: process.platform,
    arch: process.arch,
    cpuCount: require('os').cpus().length,
    totalMemory: Math.round(require('os').totalmem() / 1024 / 1024 / 1024) + ' GB',
    homeDir: require('os').homedir()
  };
});
ipcMain.handle('open-file', async (event, filePath) => {
  const { shell } = require('electron');
  await shell.openPath(filePath);
});

Step 7: Renderer Process (Your Frontend)

Create renderer.js:

document.getElementById('btn').addEventListener('click', async () => {
  try {
    const info = await window.electronAPI.getSystemInfo();
    document.getElementById('output').innerHTML = `
      <strong>System Information:</strong><br>
      Platform: ${info.platform}<br>
      Architecture: ${info.arch}<br>
      CPU Cores: ${info.cpuCount}<br>
      Total Memory: ${info.totalMemory}<br>
      Home Directory: ${info.homeDir}
    `;
  } catch (error) {
    document.getElementById('output').innerHTML = `<span style="color: red;">Error: ${error.message}</span>`;
  }
});

Step 8: Test Your App

Now for the moment of truth:

npm start

If everything went well, you should see a shiny desktop window with your app running. Click that button and marvel at the system information displayed. You just built a desktop app! 🎉

Adding Polish: Menus and Advanced Features

Your basic app works, but it feels a bit bare. Let’s add a native menu. Add this to your main.js:

const { Menu } = require('electron');
function createMenu() {
  const template = [
    {
      label: 'File',
      submenu: [
        {
          label: 'Exit',
          accelerator: 'CmdOrCtrl+Q',
          click: () => {
            app.quit();
          }
        }
      ]
    },
    {
      label: 'View',
      submenu: [
        {
          label: 'Reload',
          accelerator: 'CmdOrCtrl+R',
          click: () => mainWindow.reload()
        },
        {
          label: 'Toggle DevTools',
          accelerator: 'F12',
          click: () => mainWindow.webContents.toggleDevTools()
        }
      ]
    },
    {
      label: 'Help',
      submenu: [
        {
          label: 'About',
          click: () => {
            // You could create an about window here
            console.log('About clicked');
          }
        }
      ]
    }
  ];
  const menu = Menu.buildFromTemplate(template);
  Menu.setApplicationMenu(menu);
}
// Call this in your createWindow function:
app.on('ready', () => {
  createWindow();
  createMenu();
});

Packaging and Distribution

Here comes the exciting part—turning your development app into a distributable package. For this, we’ll use electron-builder:

npm install --save-dev electron-builder

Update your package.json:

{
  "build": {
    "appId": "com.mycompany.myapp",
    "productName": "My Awesome App",
    "files": [
      "main.js",
      "preload.js",
      "renderer.js",
      "index.html"
    ],
    "win": {
      "target": ["nsis", "portable"]
    },
    "mac": {
      "target": ["dmg", "zip"]
    },
    "linux": {
      "target": ["AppImage", "deb"]
    }
  },
  "scripts": {
    "start": "electron .",
    "build": "electron-builder"
  }
}

Then build your app:

npm run build

You’ll find your packaged application in the dist folder. For Windows, you get an installer. For Mac, a DMG file. For Linux, an AppImage or DEB package.

Pro Tips from the Trenches

1. Development vs. Production: In development, you probably want DevTools open. In production, definitely don’t. Use environment checks:

const isDev = require('electron-is-dev');
if (isDev) {
  mainWindow.webContents.openDevTools();
}

2. Auto-Updates: Users don’t like manually downloading new versions. Consider using electron-updater to push updates automatically. 3. Performance: Remember, you’re embedding Chromium. Your app’s memory footprint will be larger than a native application. Keep this in mind when designing features. 4. Code Signing: Before distributing on macOS, sign your app. Apple’s notarization process is strict but necessary for user trust. 5. Error Handling: The combination of Node.js and Chromium processes can lead to hard-to-debug issues. Implement proper error logging from day one.

Common Pitfalls to Avoid

  • Not using a preload script: Directly exposing Node.js APIs to your renderer is a security nightmare.
  • Blocking the main thread: Long-running operations in the main process freeze your entire app. Use ipcMain.handle() for async operations.
  • Forgetting platform differences: Windows, macOS, and Linux have different conventions. Test on all platforms if possible.
  • Shipping with DevTools: An embarrassing mistake that’s happened to bigger companies than you.

The Electron Ecosystem

Electron isn’t just a framework—it’s an ecosystem. Popular tools to enhance your development:

  • electron-builder: Package and distribute your app
  • electron-updater: Keep your app fresh automatically
  • electron-is-dev: Detect development vs. production environments
  • electron-log: Persistent application logging
  • electron-store: Simple data persistence

Final Thoughts

Electron democratized desktop application development. You no longer need to be a C++ wizard or sweat through Swift. If you can build a web app, you can build a desktop app. The learning curve is gentle, the community is supportive, and the possibilities are endless. Your next side project could be the next VS Code killer—or maybe just a useful utility that improves your workflow. The beauty of Electron is that it lets you focus on what matters: building great features and solving real problems. The platform takes care of the heavy lifting. So go forth, build something awesome, and remember—with great desktop power comes great responsibility for your users’ disk space. Keep it lean, keep it fast, and make it sing. Your users will thank you.