Skip to main content

Getting Started with Electron

This guide walks you through embedding a Zelt application in an Electron app.

Prerequisites​

  • Node.js v20 or higher (or Bun v1.0 or higher)
  • A package manager: pnpm (recommended), npm, or bun

Additional requirements for Electron:

  • Electron — see @zeltjs/adapter-electron peer 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:

Three pieces are needed:

  1. Main process — onElectron(app) starts Zelt; runtime.electronIpc.listen(runtime.http) registers the IPC handler
  2. Preload script — exposeIpc() bridges IPC to the renderer
  3. 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(req = request()) {
    const name = req.pathParam('name');
    return { message: `Hello, ${name}!` };
  }
}

Step 2: Create the Application​

Create src/main/app.ts:

export const app = createApp([
  http({ controllers: [HelloController] }),
  electronIpc(),
]);

Step 3: Initialize in Main Process​

Create src/main/index.ts:

const bootstrap = async () => {
  const electronZelt = await onElectron(app);
  await electronZelt.electronIpc.listen(electronZelt.http, { channel: '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 from the declared features. Pass the target HTTP capabilities and channel to electronIpc.listen() to register the IPC handler. It returns:

  • http.fetch(request) — Handles a Request and returns a Response (each configured feature is namespaced by its key; http is the default)
  • electronIpc.listen(http, options) — Exposes the selected HTTP target over IPC
  • shutdown() — Gracefully shuts down the application
  • get<T>(Class) — Resolves a service from the DI container
important

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!"
important

The channel string (e.g. 'http://zelt-app') must match across all three layers: main, preload, and renderer.

What's Next?​