Getting Started with Electron
This guide walks you through embedding a Zelt application in an Electron app.
Prerequisites
Additional requirements for Electron:
- Electron — see
@zeltjs/adapter-electronpeer dependency for the minimum version - electron-vite (recommended)
Installation
pnpm add @zeltjs/core @zeltjs/adapter-electron
pnpm add -D electron electron-vite
Architecture Overview
Unlike server-based adapters (Node.js, Bun), the Electron adapter communicates via IPC instead of HTTP sockets. Your Zelt app runs in the main process, and the renderer calls it through an IPC bridge:
Renderer ──ipcFetch()──▶ Preload ──IPC──▶ Main (Zelt app)
◀── Response ── ◀── Response ──
Three pieces are needed:
- Main process —
onElectron(app)starts Zelt and registers the IPC handler - Preload script —
exposeIpc()bridges IPC to the renderer - Renderer process —
ipcFetch()sends requests through the bridge
Project Structure
my-electron-app/
├── src/
│ ├── main/
│ │ ├── entry/
│ │ │ └── hello.controller.ts
│ │ ├── app.ts # Zelt app definition
│ │ └── index.ts # Electron main entry
│ ├── preload/
│ │ └── index.ts # IPC bridge setup
│ └── renderer/
│ └── src/
│ └── api/
│ └── zeltFetch.ts # IPC fetch wrapper
├── electron.vite.config.ts
├── package.json
└── tsconfig.json
Hello World
Step 1: Create the Controller
Create src/main/entry/hello.controller.ts:
@Controller('/hello')
export class HelloController {
@Get('/:name')
greet(name = pathParam('name')) {
return { message: `Hello, ${name}!` };
}
}
Step 2: Create the Application
Create src/main/app.ts:
export const app = createApp([http({
controllers: [HelloController],
})]);
Step 3: Initialize in Main Process
Create src/main/index.ts:
const bootstrap = async () => {
const electronZelt = await onElectron(app, {
ipcChannel: 'http://zelt-app',
});
const win = new BrowserWindow({
width: 900,
height: 670,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
contextIsolation: true,
sandbox: true,
},
});
win.loadFile(join(__dirname, '../renderer/index.html'));
};
void bootstrap();
onElectron() starts the Zelt runtime and automatically registers the IPC handler for the specified channel. It returns:
fetch(request)— Handles aRequestand returns aResponseshutdown()— Gracefully shuts down the applicationget<T>(Class)— Resolves a service from the DI container
The webPreferences.preload path must point to the compiled preload script. Without it, exposeIpc() never runs and the renderer cannot reach the Zelt app.
Step 4: Set Up the Preload Script
Create src/preload/index.ts:
exposeIpc({ channel: 'http://zelt-app' });
exposeIpc() uses Electron's contextBridge to safely expose the IPC sender to the renderer.
Step 5: Call API from Renderer
Create src/renderer/src/api/zeltFetch.ts:
export const zeltFetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> =>
ipcFetch(input, init, { channel: 'http://zelt-app' });
Use it like the standard fetch():
const response = await zeltFetch('http://zelt-app/hello/world');
const data = await response.json();
console.log(data.message); // "Hello, world!"
The channel string (e.g. 'http://zelt-app') must match across all three layers: main, preload, and renderer.
What's Next?
- Electron — IPC Bridge — How the IPC bridge works in detail
- Electron — Window Management — Managing BrowserWindows through Zelt DI
- Controllers — Route handling and HTTP methods
- Services — Business logic and dependency injection