The Answer in 2024As of this writing I'm on electron-28.2.5. Electron has made improvements in securing your app. It proposes a framework that you only expose Main process APIs you need to your Renderer process.
You define the handler for writing data to a file in the Main
// main/index.jsipcMain.handle('file:save', async (event, args) => { // As previous answer stated, Main has access to // app.getPath('appData') // // use node:fs to write data to file // data comes from args}
You then expose the API in the Preload
// preload/index.jsimport { contextBridge } from 'electron'import { electronAPI } from '@electron-toolkit/preload'// Custom APIs for rendererconst { ipcRenderer } = electronAPIconst api = { save: (data) => ipcRenderer.invoke('file:save', data)}// Use `contextBridge` APIs to expose Electron APIs to// renderer only if context isolation is enabled, otherwise// just add to the DOM global.if (process.contextIsolated) { try { contextBridge.exposeInMainWorld('electron', electronAPI) contextBridge.exposeInMainWorld('api', api) } catch (error) { console.error(error) }} else { window.electron = electronAPI window.api = api}
Finally in your Frontend code (Renderer) you call the exposed/bridged api
// event handler where you have collected the datafunction onDataUpdate(e) { // collect data ... window.api.save(data) ...}
This is cleaner. Notice you never directly reference ipcRenderer in your Frontend components. For reference, this tutorial describes additional scenarios.
I also highly recommend using electron-vite for building Electron Apps. It comes with structured Main, Preload and Renderer stubs.