A TypeScript framework for building structured, type-safe Electron desktop applications.
Documentation: https://electrojs.myraxbyte.dev/
ElectroJS brings the architectural patterns found in server-side frameworks — modules, dependency injection, lifecycle hooks, typed IPC — to the Electron ecosystem. It is designed for applications where maintainability, type safety, and long-term scalability matter.
If you have worked with NestJS, the mental model will feel familiar. The key difference is that ElectroJS targets Electron desktop apps rather than HTTP servers, and the "API layer" is a typed IPC bridge between the main process and your renderer views.
Two execution environments, one typed contract. Your business logic runs in Node.js (the Runtime). Your UI runs in a sandboxed browser context (the Renderer). The Bridge connects them with a fully typed API that is automatically generated from your service code.
Modules enforce boundaries. Every feature belongs to a module. Modules declare what they import and what they export. Nothing leaks across boundaries accidentally.
Lifecycle-aware.
Every module, view, and window participates in a consistent onInit → onReady → onShutdown → onDispose lifecycle. Resource management is predictable and explicit.
Inject everything.
Dependencies are resolved synchronously through inject(). No constructors. No service locator antipatterns. The DI system is hierarchical and scoped per module.
| Introduction | Architecture overview, key concepts |
| Getting Started | Bootstrap a working application in minutes |
| Application Lifecycle | Phase order, hook contracts, error handling |
| Modules | Domain boundaries, imports/exports, inter-module communication |
| Services & Providers | @Injectable, @query, @command, state patterns |
| Dependency Injection | inject(), tokens, scopes, injector hierarchy |
| Windows | Electron windows, mounting views, layout |
| Views — Runtime Side | @View decorator, access control |
| Renderer — Frontend Side | Vite config, bridge, bootstrapping |
| Bridge API | Queries, commands, signals from the frontend |
| Signals | Typed events, cross-module and Runtime → Renderer |
| Jobs | Background tasks, cron scheduling, cancellation |
| Registry System | Runtime introspection, dynamic orchestration |
| Code Generation | How the typed Bridge is produced |
| Build Pipeline | Dev, preview, production builds |
A complete, minimal application:
// runtime/modules/app.module.ts
@Module({ imports: [AuthModule] })
export class AppModule {}
// runtime/modules/auth/auth.service.ts
@Injectable()
export class AuthService {
@query()
async getMe(): Promise<User | null> {
return inject(AuthState).getCurrentUser();
}
@command()
async login(email: string, password: string): Promise<void> {
const user = await inject(HttpService).post("/auth/login", { email, password });
inject(AuthState).setSession(user);
this.signals.publish("auth:user-logged-in", { user, isNew: false });
}
}
// runtime/views/main.view.ts
@View({
id: "main",
resource: "view:main",
access: ["auth:getMe", "auth:login"],
signals: ["auth:user-logged-in"],
})
export class MainView {}
// runtime/windows/main.window.ts
@Window({ id: "main" })
export class MainWindow {
register() {
this.create();
const view = inject(MainView);
this.mount(view);
view.webContents.once("did-finish-load", () => this.window.show());
}
}
// renderer/views/main/app.tsx
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
bridge.auth.getMe().then(setUser);
const sub = bridge.signals.subscribe("auth:user-logged-in", ({ user }) => setUser(user));
return () => sub.unsubscribe();
}, []);
return user ? <Dashboard user={user} /> : <LoginForm />;
}