I’m developing an Electron app (using TypeScript, Vite and React) that allows users (employees of a company) to manage clients and documents, which are stored in a database on a server. The app should communicate with the server sending HTTP requests to the server API. My plan is to create a local API in the Electron app that sends the HTTP requests (with necessary auth token). For example, the API would have functions like signIn(email, password) and getClient(clientId).
My first question is, where do I put this API? In the main process or in the renderer process (browser window)? The app needs to be secure, but I also don’t want the code to get confusing.
With the goal of finding an answer, I looked at other Electron apps, and found out that GitHub Dekstop (which uses Electron) has an API (located at app/src/lib/api.ts) that communiucates to the GitHub API throught HTTP requests. That’s basically what I want to do. After analyzing the code, it seems to me that this API code is running on the renderer process. However, it’s also using Node packages in the renderer process and has nodeIntegration set to true. The Electron documentation states that:
It is paramount that you do not enable Node.js integration in any renderer (
BrowserWindow,WebContentsView, or<webview>) that loads remote content.
It specifically states to not use it for remotely loaded content. GitHub and my app both use local HTML files. So, is it safe to use Node.js integration (and consequently use the renderer process for the API) for my app? Or should I just make the API (with HTTP requests) on the main process and use IPC to communicate between the renderer and main processes?
>Solution :
Here is what I do if I have to communicate with and external server or make HTTP request.
I use the main process for API calls and IPC for communication.
If you enable Nodejs integration in the renderer process you will expose your app to vulnerabilities. For eg: Attacker can run scripts and can access nodejs api’s and steal tokens or any sensitive data.
If you use main process, you are not exposing your api to UI. And you can use IPC to safely communicate between renderer and main process.
For eg:
In my main.js file, I do this:
import { app, BrowserWindow, ipcMain } from 'electron';
import axios from 'axios';
//handle signIn
async function signIn(email, password) {
const response = await axios.post('https://api.com/signin', { email, password });
return response.data;
}
//handle API request with IPC
function createWindow() {
const win = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'), //IPC communication
}
});
ipcMain.handle('signIn', async (event, email, password) => {
return await signIn(email, password);
});
}
app.whenReady().then(createWindow);
I don’t exactly do this, but you get my point right?
In my preload.js file:
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', {
signIn: (email, password) => ipcRenderer.invoke('signIn', email, password),
//you can also use your other functions here
//for eg: getClient, checkIfUserExists, etc
});
And in signIn.js or renderer process:
import React, { useState } from 'react';
function SignIn() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const handleSignIn = async () => {
try {
const response = await window.api.signIn(email, password);
console.log(response);
} catch (error) {
console.error(error);
}
};
return (
<div>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button onClick={() => handleSignIn()}>Sign In</button>
</div>
);
}
export default SignIn;
I hope you get what I am trying to say.
Hope this helps.