In this tutorial, we’ll walk through how to integrate an Electron app with a NestJS engine in a way that makes the most of both frameworks. The NestJS app will run as a standalone process, without listening on any HTTP ports, and will communicate with the Electron app via Inter-Process Communication (IPC). This approach allows the two parts of the application to work together smoothly, each focusing on what it does best—Electron managing the user interface, and NestJS handling complex tasks in its own process.
Before diving into the core setup, let's create a structured environment using NPM workspaces. This allows us to manage both the Electron and NestJS parts of our application within a single repository.
$ mkdir electron-demo
$ cd electron-demo
$ npm init --yes
Next, set up the workspace structure:
$ mkdir packages
Update your package.json
to configure the workspace:
{
"name": "electron-demo",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"workspaces": [
"packages/electron-app",
"packages/nest-engine"
]
}
With the workspace set up, let's create our Electron app:
$ cd packages
$ npx create-electron-app@latest electron-app --template=webpack-typescript
Test your newly created Electron app from the root of the repository:
$ cd ../
$ npm run start -w packages/electron-app
Next, we’ll create the NestJS engine, which will run as a standalone application. In this context, the NestJS app will not function as a typical HTTP server but instead act as a background process that communicates with the Electron app using Inter-Process Communication (IPC).
Normally, a NestJS application functions by listening for HTTP requests on a specific port, handling them through controllers, and responding with data. However, in this setup, the NestJS app will be running as a standalone application with no HTTP server. Instead, it listens for messages from the Electron main process, performs specific tasks, and then sends messages back as responses.
The NestJS app is initialized using NestFactory.createApplicationContext()
, which sets up the NestJS application without starting an HTTP server. This method is ideal for when you need NestJS to act as a background worker or utility, processing logic in the background and interacting with other parts of the application directly, without involving HTTP communication.
Here’s how to create the NestJS engine:
$ npm i -g @nestjs/cli
$ cd packages
$ nest new nest-engine
Use NPM as the package manager when prompted.
Remove the git folder for the Nest app, as we are tracking the repo at the workspace level
$ cd nest-engine
$ rm -rf .git
Now, modify the main.ts
file to set up the NestJS app as a standalone utility process that communicates with the Electron app:
// packages/nest-engine/src/main.ts
/// <reference types="electron" />
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
await NestFactory.createApplicationContext(AppModule);
process.parentPort?.on('message', (e) => {
process.parentPort.postMessage('Message received from Nest! ' + e.data);
});
}
bootstrap();
In this code, the NestJS app is initialized as a context, and it listens for messages from the parent process (the Electron app). Whenever a message is received, it responds with a confirmation message.
From the root of your repo, build the Nest code so it can be used by the Electron app.
$ npm run build -w packages/nest-engine
Or if you want to run it in watch mode so it auto-builds while you make changes:
npm run build -w packages/nest-engine -- --watch
Now that the NestJS engine is set up as a standalone process, we need to integrate it with the Electron app. The next steps will guide you through packaging the NestJS app and including it in your Electron project.
Create a new folder called resources
in your Electron app and add a .gitignore
file
// file: packages/electron-app/resources/.gitignore
./*.tgz
Update the package.json
of your electron-app folder to include the following scripts:
{
"name": "electron-app",
"productName": "electron",
...
"scripts": {
"start": "electron-forge start",
...
"pack:nest": "npm pack ../nest-engine --pack-destination=./resources",
"prepackage": "npm run pack:nest"
},
...
}
This will take the built Nest app, package it as a zipped NPM package and copy it into our Electron project so it can be included in the packaged app.
Next, update your forge configuration to include the resources
folder in the packaged app.
// forge.config.ts
...
const config: ForgeConfig = {
packagerConfig: {
asar: true,
extraResource: ["./resources"],
},
...
}
Update your main process code to launch the Nest app. Add the following function to the bottom of the file:
// file: packages/electron-app/src/index.ts
const startEngine = async () => {
// By default, we load the file from the source code itself
let nestPath = join(process.cwd(), "..", "nest-engine", "dist", "main.js");
// If the app is packaged, we load it from node_modules instead
if (app.isPackaged) {
nestPath = join(
app.getPath("userData"),
"node_modules",
"nest-engine",
"dist",
"main.js"
);
}
const nestProcess = utilityProcess.fork(nestPath, [], {
stdio: "pipe",
});
nestProcess.on("spawn", () => {
console.log(`Nest process spawned: ${nestProcess.pid}`);
nestProcess.postMessage("Hello from main process!");
});
nestProcess.on("message", (message) => {
console.log(`Main process received message from Nest: ${message}`);
});
};
Then look for the app.on("ready", createWindow)
line and replace it with this:
app.on("ready", () => {
createWindow();
startEngine();
});
Let's start the app and make sure everything is working!
$ npm run start -w packages/electron-app
Check your terminal logs. You should see the following lines if everything is working properly:
Nest process spawned: 53009
Main process received message from Nest: Message received from Nest! Hello from main process!
By following this guide, you’ve set up an Electron app that works seamlessly with a NestJS engine running as a separate process. Using IPC to pass messages between the two applications keeps the structure clean and ensures they can interact with each other efficiently. This setup can be particularly useful when you want to take advantage of the full capabilities of both frameworks, without overcomplicating the design or introducing unnecessary overhead.
In this example, we created a single NestJS process for the Electron app. But in some scenarios you may want to spawn separate NestJS processes for each window of your Electron app. This is exactly how Vast Studio works. If you want to learn more or talk to the engineers directly, join the Discord, or find us on Reddit.