Hey everyone!
I wanted to share a project I've been working on that might help building Electron applications.
What I Built
electron-modular is a simple DI lib for Electron's main process. Think NestJS/Angular style architecture but specifically designed for Electron and this is simple solution.
Why Does This Exist?
When developers building a Electron apps and the main process sometimes became a mess and I want to better organize architecture especially when the project grows and something like this happens:
- Services with tangled dependencies
- IPC handlers scattered everywhere
- Window management logic duplicated
- Many dependencies
- Sometimes hard to test anything
Of course, you can build it correctly if you have the right skills, but it would be nice to have a defined structure similar (NestJS/Angular).
So, what It Does
Organize code into feature modules:
RestApiService service from another module
import { Injectable } from "@devisfuture/electron-modular";
....
@Injectable()
export class RestApiService {
....
async get<T>(
endpoint: string,
options: RequestOptions = {},
): Promise<T | undefined> {
const response = await fetch(endpoint, options);
if (response.error !== undefined) {
console.error("Error fetching data:", response.error.message);
return;
}
return response.data;
}
....
}
Dependency injection token
const USER_REST_API_PROVIDER = Symbol("USER_REST_API_PROVIDER");
TUserRestApiProvider using for Factory Provider within the UserModule. This is like an abstraction between the UserModule and RestApiService
type TUserRestApiProvider = {
get: <TUser>(
endpoint: string,
options?: TRequestOptions,
) => Promise<TUser>;
};
UserModule is a feature module that encapsulates all user-related logic, including services, data fetching, IPC communication and so on (see detail in repo)
import { RgModule} from "@devisfuture/electron-modular";
....
@RgModule({
imports: [RestApiModule],
ipc: [UserIpc], // IPC communication handlers
windows: [UserProfileWindow],
providers: [
UserService,
{
provide: USER_REST_API_PROVIDER,
useFactory: (restApiService: RestApiService): TUserRestApiProvider => ({
get: (endpoint, options) => restApiService.get(endpoint, options),
}),
inject: [RestApiService],
},
],
})
export class UserModule {}
Automatic dependency injection
import { Injectable, Inject } from "@devisfuture/electron-modular";
....
@Injectable()
export class UserService {
constructor(
@Inject(USER_REST_API_PROVIDER) private auth: TUserAuthProvider,
private db: DatabaseService
) {} // Dependencies auto-injected!
}
Dedicated IPC handlers
import { IpcHandler } from "@devisfuture/electron-modular";
....
@IpcHandler()
export class UserIpc {
constructor(private userService: UserService) {}
onInit({ getWindow }) {
// Group all user-related IPC here
}
}
Window lifecycle management
import { WindowManager } from "@devisfuture/electron-modular";
....
@WindowManager({
hash: 'window:user-profile',
isCache: true,
options: { width: 600, height: 400 }
})
export class UserProfileWindow {
onWebContentsDidFinishLoad(window) {
// Lifecycle hooks!
}
}
Why Provider Pattern?
The provider pattern is useful when:
- You want to share only specific methods, not entire services
- You want better encapsulation and clear module boundaries
Instead of exposing a full service, you expose only what's needed:
{
provide: USER_REST_API_PROVIDER,
useFactory: (restApi: RestApiService): TUserRestApiProvider => ({
get: (endpoint, options) => restApi.get(endpoint, options),
}),
inject: [RestApiService],
}
Resources
- GitHub: https://github.com/trae-op/electron-modular
- NPM: https://www.npmjs.com/package/@devisfuture/electron-modular
- Simple Example App: https://github.com/trae-op/quick-start_react_electron-modular
- Full-featured starter: https://github.com/trae-op/electron-modular-boilerplate
Try It Out
npm install @devisfuture/electron-modular
The README has a full guide and examples.
I'd love your feedback! What are the alternatives, and are they easier to use?
I hope you guys like it! Positive feedback and GitHub stars would mean a lot to me )
I’ve been trying to create something that would be simple to use.
Top comments (3)
This is exactly the kind of structure I wish I had when I was building my first Electron app. The main process turned into a 2000-line monster before I knew what hit me.
The provider pattern with factory functions is a smart call - exposing only what each module needs instead of the whole service. That's something I see people get wrong even in NestJS projects where they just export everything.
Two questions:
Gonna star the repo and try it on a side project this weekend. Electron really needs more opinionated structure like this.
Hi! I’ve recently implemented Lazy Loading modules. You can now defer the initialization of specific modules until they are actually needed. You can check out the documentation here for implementation details: Lazy Loading modules section
I totally feel your pain — the "2000-line main.js" is exactly what pushed me to create this. I’m glad you noticed the provider pattern; I wanted to bring that NestJS-like discipline to Electron without the heavy overhead. And this is a simple package and I don't want to create big framework
About Hot Reload.
Currently, for the main process, I think you can use the package alongside electron-vite. Since the architecture is decoupled, restarting the main process is usually very fast and clean.
About Lazy-loading.
This is a brilliant suggestion. You're right — for massive apps, startup time is king. I am thinking about this. Maybe you can advise me how it would look good. I mean how it looks the options in the module.