Supercharging Backend-frontend Communication in Electron with IPC and Decorators

Khalid M Sheet
Dev Genius
Published in
3 min readMay 28, 2023

--

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.

Successfully registered routes with modules

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

--

--