Electron Best Practices
Electron development demands a disciplined approach to security, performance, and maintainability. This guide outlines the definitive best practices for our team, leveraging modern Electron features and tooling (targeting Electron 28+).
1. Project Setup & Structure
Always start with Electron Forge to standardize project structure, build pipelines, and stay aligned with the latest Electron APIs.
Scaffolding: Use a modern template like Vite + TypeScript.
❌ BAD: Manual setup, outdated CLIs.
✅ GOOD:
npx create-electron-app@latest my-app --template=vite-typescript
File Naming: Adhere to Electron's coding style for JavaScript files.
❌ BAD: my_module.js
✅ GOOD: my-module.js
2. Security Fundamentals (Non-Negotiable)
Security is paramount. Always enable context isolation and expose APIs safely.
Context Isolation (Mandatory): Keep contextIsolation enabled. It's on by default since Electron 12.
❌ BAD: new BrowserWindow({ webPreferences: { contextIsolation: false } })
✅ GOOD: (Default behavior, no explicit setting needed unless overriding)
// main.mjs
new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.mjs'),
sandbox: true // Strongly recommended
}
})
Safe API Exposure with contextBridge: Never mutate the global window object directly. Use contextBridge.exposeInMainWorld and filter arguments.
❌ BAD: Exposing ipcRenderer.send directly.
// preload.mjs
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('myAPI', {
send: ipcRenderer.send // ❌ Allows renderer to send arbitrary IPC messages
});
✅ GOOD: Expose specific, argument-filtered functions.
// preload.mjs
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
loadPreferences: () => ipcRenderer.invoke('load-prefs'),
saveSettings: (settings) => {
// ✅ Validate and filter arguments before sending
if (typeof settings === 'object' && settings !== null) {
ipcRenderer.send('save-settings', settings);
} else {
console.error('Invalid settings object provided.');
}
}
});
// interface.d.ts (for TypeScript)
declare global {
interface Window {
electronAPI: {
loadPreferences: () => Promise<any>;
saveSettings: (settings: object) => void;
};
}
}
Content Security Policy (CSP): Implement a strict CSP in your index.html or via webRequest.onHeadersReceived.
✅ GOOD:
<!-- index.html -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">
3. ES Modules (ESM) Adoption (Electron 28+)
Leverage native ESM for cleaner, more modern code.
Main Process: Use .mjs extension or "type": "module" in package.json.
await for Pre-Ready APIs: ESM imports are asynchronous. Ensure critical APIs (e.g., app.setPath) are awaited before app.whenReady().
❌ BAD:
// main.mjs
import './setup-paths.mjs'; // May resolve after app is ready
app.whenReady().then(() => { /* ... */ });
✅ GOOD:
// main.mjs
import { app } from 'electron';
await import('./setup-paths.mjs'); // Guarantees execution before ready
app.whenReady().then(() => { /* ... */ });
Preload Scripts: Always use the .mjs extension for ESM preload scripts.
- Sandboxed Preloads: Cannot use ESM imports. Bundle them if external modules are needed.
- Context Isolation: Required for dynamic Node.js ESM imports in unsandboxed preloads.
❌ BAD:
preload.js with import statements.
✅ GOOD: preload.mjs
4. IPC Communication
Use ipcMain.handle and ipcRenderer.invoke for explicit request-response patterns.
- Request-Response:
❌ BAD: Using
ipcRenderer.send for requests that expect a response.// renderer.js
ipcRenderer.send('get-data', someId);
ipcRenderer.on('data-response', (event, data) => { /* ... */ }); // Race condition prone
✅ GOOD:// main.mjs
ipcMain.handle('get-data', async (event, someId) => {
// ✅ Perform validation, access native APIs
return await fetchData(someId);
});
// renderer.js
const data = await window.electronAPI.getData(someId); // Assuming exposed via contextBridge
5. System Path Handling
Always use Node.js path and os modules for cross-platform compatibility.
File Paths: Use path.join() for concatenation.
❌ BAD: app.getPath('userData') + '/config.json'
✅ GOOD:
import path from 'node:path';
import { app } from 'electron';
const configPath = path.join(app.getPath('userData'), 'config.json');
Temporary Directories: Use os.tmpdir().
❌ BAD: '/tmp/my-app-data'
✅ GOOD:
import os from 'node:os';
const tempDir = os.tmpdir();
6. Testing & Linting
Integrate Electron's built-in tooling for consistent code quality.
Linting: Run npm run lint regularly and integrate into pre-commit hooks.
✅ GOOD: Ensure your package.json includes:
"scripts": {
"lint": "electron-builder lint" // Or specific linter like 'eslint .'
}
Unit Tests: Add new tests for any changes or new features.
✅ GOOD: npm run test
"scripts": {
"test": "electron-mocha spec" // Example with electron-mocha
}
7. Staying Current
Electron evolves rapidly. Proactively manage updates and breaking changes.
- Official Documentation: Always consult the version-specific official documentation.
- Breaking Changes: Regularly review the "Breaking Changes" page for each major Electron release to anticipate necessary updates.
1---2name: electron3description: "[Applies to: **/*.{js,jsx}] This guide provides opinionated, actionable best practices for building secure, performant, and maintainable Electron applications using modern patterns and consistent tooling."4source: "cursor_mdc"5---67# Electron Best Practices89Electron development demands a disciplined approach to security, performance, and maintainability. This guide outlines the definitive best practices for our team, leveraging modern Electron features and tooling (targeting Electron 28+).1011## 1. Project Setup & Structure1213Always start with **Electron Forge** to standardize project structure, build pipelines, and stay aligned with the latest Electron APIs.1415- **Scaffolding:** Use a modern template like Vite + TypeScript.16 ❌ BAD: Manual setup, outdated CLIs.17 ✅ GOOD:18 ```bash19 npx create-electron-app@latest my-app --template=vite-typescript20 ```2122- **File Naming:** Adhere to Electron's coding style for JavaScript files.23 ❌ BAD: `my_module.js`24 ✅ GOOD: `my-module.js`2526## 2. Security Fundamentals (Non-Negotiable)2728Security is paramount. Always enable context isolation and expose APIs safely.2930- **Context Isolation (Mandatory):** Keep `contextIsolation` enabled. It's on by default since Electron 12.31 ❌ BAD: `new BrowserWindow({ webPreferences: { contextIsolation: false } })`32 ✅ GOOD: (Default behavior, no explicit setting needed unless overriding)33 ```javascript34 // main.mjs35 new BrowserWindow({36 webPreferences: {37 preload: path.join(__dirname, 'preload.mjs'),38 sandbox: true // Strongly recommended39 }40 })41 ```4243- **Safe API Exposure with `contextBridge`:** Never mutate the global `window` object directly. Use `contextBridge.exposeInMainWorld` and filter arguments.44 ❌ BAD: Exposing `ipcRenderer.send` directly.45 ```javascript46 // preload.mjs47 const { contextBridge, ipcRenderer } = require('electron');48 contextBridge.exposeInMainWorld('myAPI', {49 send: ipcRenderer.send // ❌ Allows renderer to send arbitrary IPC messages50 });51 ```52 ✅ GOOD: Expose specific, argument-filtered functions.53 ```javascript54 // preload.mjs55 import { contextBridge, ipcRenderer } from 'electron';5657 contextBridge.exposeInMainWorld('electronAPI', {58 loadPreferences: () => ipcRenderer.invoke('load-prefs'),59 saveSettings: (settings) => {60 // ✅ Validate and filter arguments before sending61 if (typeof settings === 'object' && settings !== null) {62 ipcRenderer.send('save-settings', settings);63 } else {64 console.error('Invalid settings object provided.');65 }66 }67 });6869 // interface.d.ts (for TypeScript)70 declare global {71 interface Window {72 electronAPI: {73 loadPreferences: () => Promise<any>;74 saveSettings: (settings: object) => void;75 };76 }77 }78 ```7980- **Content Security Policy (CSP):** Implement a strict CSP in your `index.html` or via `webRequest.onHeadersReceived`.81 ✅ GOOD:82 ```html83 <!-- index.html -->84 <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">85 ```8687## 3. ES Modules (ESM) Adoption (Electron 28+)8889Leverage native ESM for cleaner, more modern code.9091- **Main Process:** Use `.mjs` extension or `"type": "module"` in `package.json`.92 - **`await` for Pre-Ready APIs:** ESM imports are asynchronous. Ensure critical APIs (e.g., `app.setPath`) are `await`ed before `app.whenReady()`.93 ❌ BAD:94 ```javascript95 // main.mjs96 import './setup-paths.mjs'; // May resolve after app is ready97 app.whenReady().then(() => { /* ... */ });98 ```99 ✅ GOOD:100 ```javascript101 // main.mjs102 import { app } from 'electron';103 await import('./setup-paths.mjs'); // Guarantees execution before ready104 app.whenReady().then(() => { /* ... */ });105 ```106107- **Preload Scripts:** Always use the `.mjs` extension for ESM preload scripts.108 - **Sandboxed Preloads:** Cannot use ESM imports. Bundle them if external modules are needed.109 - **Context Isolation:** Required for dynamic Node.js ESM imports in unsandboxed preloads.110 ❌ BAD: `preload.js` with `import` statements.111 ✅ GOOD: `preload.mjs`112113## 4. IPC Communication114115Use `ipcMain.handle` and `ipcRenderer.invoke` for explicit request-response patterns.116117- **Request-Response:**118 ❌ BAD: Using `ipcRenderer.send` for requests that expect a response.119 ```javascript120 // renderer.js121 ipcRenderer.send('get-data', someId);122 ipcRenderer.on('data-response', (event, data) => { /* ... */ }); // Race condition prone123 ```124 ✅ GOOD:125 ```javascript126 // main.mjs127 ipcMain.handle('get-data', async (event, someId) => {128 // ✅ Perform validation, access native APIs129 return await fetchData(someId);130 });131132 // renderer.js133 const data = await window.electronAPI.getData(someId); // Assuming exposed via contextBridge134 ```135136## 5. System Path Handling137138Always use Node.js `path` and `os` modules for cross-platform compatibility.139140- **File Paths:** Use `path.join()` for concatenation.141 ❌ BAD: `app.getPath('userData') + '/config.json'`142 ✅ GOOD:143 ```javascript144 import path from 'node:path';145 import { app } from 'electron';146 const configPath = path.join(app.getPath('userData'), 'config.json');147 ```148149- **Temporary Directories:** Use `os.tmpdir()`.150 ❌ BAD: `'/tmp/my-app-data'`151 ✅ GOOD:152 ```javascript153 import os from 'node:os';154 const tempDir = os.tmpdir();155 ```156157## 6. Testing & Linting158159Integrate Electron's built-in tooling for consistent code quality.160161- **Linting:** Run `npm run lint` regularly and integrate into pre-commit hooks.162 ✅ GOOD: Ensure your `package.json` includes:163 ```json164 "scripts": {165 "lint": "electron-builder lint" // Or specific linter like 'eslint .'166 }167 ```168169- **Unit Tests:** Add new tests for any changes or new features.170 ✅ GOOD: `npm run test`171 ```json172 "scripts": {173 "test": "electron-mocha spec" // Example with electron-mocha174 }175 ```176177## 7. Staying Current178179Electron evolves rapidly. Proactively manage updates and breaking changes.180181- **Official Documentation:** Always consult the version-specific official documentation.182- **Breaking Changes:** Regularly review the "Breaking Changes" page for each major Electron release to anticipate necessary updates.