DEV Community

Cover image for How to organize your Electron project with Modules
traeop
traeop

Posted on • Edited on

How to organize your Electron project with Modules

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;
  }
  ....
}
Enter fullscreen mode Exit fullscreen mode

Dependency injection token

const USER_REST_API_PROVIDER = Symbol("USER_REST_API_PROVIDER");
Enter fullscreen mode Exit fullscreen mode

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>;
};
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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!
  }
}
Enter fullscreen mode Exit fullscreen mode

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],
}
Enter fullscreen mode Exit fullscreen mode

Resources

Try It Out

npm install @devisfuture/electron-modular
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
trinhcuong-ast profile image
Kai Alder

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:

  1. How does this handle hot reload during development? One of my biggest pain points with Electron is having to restart the whole app when main process code changes.
  2. Any plans for supporting lazy-loaded modules? For larger apps, loading everything at startup can get slow.

Gonna star the repo and try it on a side project this weekend. Electron really needs more opinionated structure like this.

Collapse
 
traeop_8fe714e58a84498dc2 profile image
traeop

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

Collapse
 
traeop_8fe714e58a84498dc2 profile image
traeop

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.