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.4---56# Electron Best Practices78Electron 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+).910## 1. Project Setup & Structure1112Always start with **Electron Forge** to standardize project structure, build pipelines, and stay aligned with the latest Electron APIs.1314- **Scaffolding:** Use a modern template like Vite + TypeScript.15 ❌ BAD: Manual setup, outdated CLIs.16 ✅ GOOD:17 ```bash18 npx create-electron-app@latest my-app --template=vite-typescript19 ```2021- **File Naming:** Adhere to Electron's coding style for JavaScript files.22 ❌ BAD: `my_module.js`23 ✅ GOOD: `my-module.js`2425## 2. Security Fundamentals (Non-Negotiable)2627Security is paramount. Always enable context isolation and expose APIs safely.2829- **Context Isolation (Mandatory):** Keep `contextIsolation` enabled. It's on by default since Electron 12.30 ❌ BAD: `new BrowserWindow({ webPreferences: { contextIsolation: false } })`31 ✅ GOOD: (Default behavior, no explicit setting needed unless overriding)32 ```javascript33 // main.mjs34 new BrowserWindow({35 webPreferences: {36 preload: path.join(__dirname, 'preload.mjs'),37 sandbox: true // Strongly recommended38 }39 })40 ```4142- **Safe API Exposure with `contextBridge`:** Never mutate the global `window` object directly. Use `contextBridge.exposeInMainWorld` and filter arguments.43 ❌ BAD: Exposing `ipcRenderer.send` directly.44 ```javascript45 // preload.mjs46 const { contextBridge, ipcRenderer } = require('electron');47 contextBridge.exposeInMainWorld('myAPI', {48 send: ipcRenderer.send // ❌ Allows renderer to send arbitrary IPC messages49 });50 ```51 ✅ GOOD: Expose specific, argument-filtered functions.52 ```javascript53 // preload.mjs54 import { contextBridge, ipcRenderer } from 'electron';5556 contextBridge.exposeInMainWorld('electronAPI', {57 loadPreferences: () => ipcRenderer.invoke('load-prefs'),58 saveSettings: (settings) => {59 // ✅ Validate and filter arguments before sending60 if (typeof settings === 'object' && settings !== null) {61 ipcRenderer.send('save-settings', settings);62 } else {63 console.error('Invalid settings object provided.');64 }65 }66 });6768 // interface.d.ts (for TypeScript)69 declare global {70 interface Window {71 electronAPI: {72 loadPreferences: () => Promise<any>;73 saveSettings: (settings: object) => void;74 };75 }76 }77 ```7879- **Content Security Policy (CSP):** Implement a strict CSP in your `index.html` or via `webRequest.onHeadersReceived`.80 ✅ GOOD:81 ```html82 <!-- index.html -->83 <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'">84 ```8586## 3. ES Modules (ESM) Adoption (Electron 28+)8788Leverage native ESM for cleaner, more modern code.8990- **Main Process:** Use `.mjs` extension or `"type": "module"` in `package.json`.91 - **`await` for Pre-Ready APIs:** ESM imports are asynchronous. Ensure critical APIs (e.g., `app.setPath`) are `await`ed before `app.whenReady()`.92 ❌ BAD:93 ```javascript94 // main.mjs95 import './setup-paths.mjs'; // May resolve after app is ready96 app.whenReady().then(() => { /* ... */ });97 ```98 ✅ GOOD:99 ```javascript100 // main.mjs101 import { app } from 'electron';102 await import('./setup-paths.mjs'); // Guarantees execution before ready103 app.whenReady().then(() => { /* ... */ });104 ```105106- **Preload Scripts:** Always use the `.mjs` extension for ESM preload scripts.107 - **Sandboxed Preloads:** Cannot use ESM imports. Bundle them if external modules are needed.108 - **Context Isolation:** Required for dynamic Node.js ESM imports in unsandboxed preloads.109 ❌ BAD: `preload.js` with `import` statements.110 ✅ GOOD: `preload.mjs`111112## 4. IPC Communication113114Use `ipcMain.handle` and `ipcRenderer.invoke` for explicit request-response patterns.115116- **Request-Response:**117 ❌ BAD: Using `ipcRenderer.send` for requests that expect a response.118 ```javascript119 // renderer.js120 ipcRenderer.send('get-data', someId);121 ipcRenderer.on('data-response', (event, data) => { /* ... */ }); // Race condition prone122 ```123 ✅ GOOD:124 ```javascript125 // main.mjs126 ipcMain.handle('get-data', async (event, someId) => {127 // ✅ Perform validation, access native APIs128 return await fetchData(someId);129 });130131 // renderer.js132 const data = await window.electronAPI.getData(someId); // Assuming exposed via contextBridge133 ```134135## 5. System Path Handling136137Always use Node.js `path` and `os` modules for cross-platform compatibility.138139- **File Paths:** Use `path.join()` for concatenation.140 ❌ BAD: `app.getPath('userData') + '/config.json'`141 ✅ GOOD:142 ```javascript143 import path from 'node:path';144 import { app } from 'electron';145 const configPath = path.join(app.getPath('userData'), 'config.json');146 ```147148- **Temporary Directories:** Use `os.tmpdir()`.149 ❌ BAD: `'/tmp/my-app-data'`150 ✅ GOOD:151 ```javascript152 import os from 'node:os';153 const tempDir = os.tmpdir();154 ```155156## 6. Testing & Linting157158Integrate Electron's built-in tooling for consistent code quality.159160- **Linting:** Run `npm run lint` regularly and integrate into pre-commit hooks.161 ✅ GOOD: Ensure your `package.json` includes:162 ```json163 "scripts": {164 "lint": "electron-builder lint" // Or specific linter like 'eslint .'165 }166 ```167168- **Unit Tests:** Add new tests for any changes or new features.169 ✅ GOOD: `npm run test`170 ```json171 "scripts": {172 "test": "electron-mocha spec" // Example with electron-mocha173 }174 ```175176## 7. Staying Current177178Electron evolves rapidly. Proactively manage updates and breaking changes.179180- **Official Documentation:** Always consult the version-specific official documentation.181- **Breaking Changes:** Regularly review the "Breaking Changes" page for each major Electron release to anticipate necessary updates.