Supercharging Backend-frontend Communication in Electron with IPC and Decorators
Introduction
Unleashing the full potential of my side project startup, I began the process of developing an Electron-React application. Seeking an efficient solution for seamless backend-frontend communication, I discovered the power of IPC (Inter-Process Communication).
The Challenge of Event Registration
Navigating the complexities of event registration posed a challenge. How could I ensure that events were properly registered before the front-end initialization?
Decorators
The magical JavaScript/TypeScript feature came to the rescue! Introducing the Route decorator, Accepting a path argument only, it effortlessly established IPC routes using “ipcMain.handle” and ensured the method descriptor received all the necessary arguments sent from the front end.
# route.decorator.ts
export function Route(path: string) {
return function (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) {
// The handle method will listen to the coming event and
// inject all necessary arguments to the function descriptor
ipcMain.handle(path, async (_, ...args: unknown[]) => {
return await descriptor.value(...args);
});
};
}
Streamlining Module Initialization
To maintain organization within my “backend” folder, housing modules like “product.ts”
Created a basic “initializeModules()” method. This seamlessly initialized all the modules dynamically.
export async function initializeModules() {
// getting every module that ends with .ts inside modules
// folder.
const modulePaths = await glob("src/backend/modules/*.module.ts");
for (const modulePath of modulePaths) {
// capitalize and getImportedModuleName are methods
// I created and will include them below this part
const caplitalizedModuleName = capitalize(
getImportedModuleName(modulePath)
);
try {
const moduleInstance = await import(`./../backend/modules/${getImportedModuleName(modulePath)}`)
if (moduleInstance) {
// The fun part! initializing the module so it can
// be reached from anywhere inside the application
new moduleInstance.default();
}
} catch (error) {
console.error(
`Failed to initialize module [${caplitalizedModuleName}]: ${error}`
);
}
}
}
GetImportedModuleName
function getImportedModuleName(modulePath: string): string {
return modulePath.replace(".ts", "").replace(/\\/g, "/").split("/").pop();
}
capitalize
export function capitalize(string: string): string {
return string.charAt(0).toUpperCase() + string.slice(1);
}
initializeModules() method must be called in the “index.ts” file of Electron just before the initialization of the app.
For this to work you must export all of your modules as the default export
Harnessing the Power of Decorators
Decorating methods within backend modules with the Route decorator made it so effortless. The decorator handled the setup process, effortlessly establishing IPC routes and guaranteeing the correct method arguments during execution.
# ./src/backend/modules/product.module.ts
export default class ProductModule {
@Route("get-products")
async getProducts(): Promise<Product[]> {
return await Product.find();
}
}
the @Route() decorator will handle the incoming event and inject every argument to the function descriptor if any. Then you can access each argument from within the function.
Achieving Harmony
With the combination of decorators and IPC, I unlocked a straightforward approach to foster seamless communication between the backend and frontend.
Conclusion
By embracing the capabilities of IPC and decorators, A route decorator, coupled with module initialization. With this approach, the possibilities are endless, setting the stage for a successful and impactful project.
Resources
For those who want to dig deeper and know more about ElectronJS and IPC