| name | electron |
| description | [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. |
| source | cursor_mdc |
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)
new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.mjs'),
sandbox: true
}
})
-
Safe API Exposure with contextBridge: Never mutate the global window object directly. Use contextBridge.exposeInMainWorld and filter arguments.
❌ BAD: Exposing ipcRenderer.send directly.
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('myAPI', {
send: ipcRenderer.send
});
✅ GOOD: Expose specific, argument-filtered functions.
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', {
loadPreferences: () => ipcRenderer.invoke('load-prefs'),
saveSettings: (settings) => {
if (typeof settings === 'object' && settings !== null) {
ipcRenderer.send('save-settings', settings);
} else {
console.error('Invalid settings object provided.');
}
}
});
declare global {
interface Window {
electronAPI: {
loadPreferences: <any>;
: ;
};
}
}
-
Content Security Policy (CSP): Implement a strict CSP in your index.html or via webRequest.onHeadersReceived.
✅ GOOD:
<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:
import './setup-paths.mjs';
app.whenReady().then(() => { });
✅ GOOD:
import { app } from 'electron';
await import('./setup-paths.mjs');
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.
ipcRenderer.send('get-data', someId);
ipcRenderer.on('data-response', (event, data) => { });
✅ GOOD:
ipcMain.handle('get-data', async (event, someId) => {
return await fetchData(someId);
});
const data = await window.electronAPI.getData(someId);
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"
}
-
Unit Tests: Add new tests for any changes or new features.
✅ GOOD: npm run test
"scripts": {
"test": "electron-mocha spec"
}
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.