Electron IPC Response/Request architecture with TypeScript
Tags: typescript, electron
Published: 2019-12-30 16:42:08

---
title: Electron IPC Response/Request architecture with TypeScript
published: true
date: 2019-12-20 17:00:47 UTC
tags: typescript,electron
canonical_url: https://blog.logrocket.com/electron-ipc-response-request-architecture-with-typescript/
cover_image: https://thepracticaldev.s3.amazonaws.com/i/2i13ebyho9psn4ol2glc.png
---
**Written by [Kevin Hirczy](https://blog.logrocket.com/author/kevinhirczy/)**✏️

The way Electron works is pretty simple. There are two different layers – the **main process** and the **renderer process(es)**. There’s always only one main process, which is the entry point of your Electron application. There can be any number of renderer processes, which are responsible for rendering your application.

Communication between these layers is usually done via IPC (interprocess communication). That may sound complicated, but is just a fancy name for an asynchronous request-response pattern.

![ipc pattern](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2019/12/ipcresponserequest.png?resize=674%2C176&ssl=1)

What happens behind the scenes for the communication between the renderer and main process is basically just event dispatching. For example, let’s say your application should show information regarding the system it is run on. This can be done with a simple command, `uname -a`, which shows your kernel version. But your application itself cannot execute commands, so it needs the main process. Within Electron applications, your application has access to the renderer process ([ipcRenderer](https://electronjs.org/docs/api/ipc-renderer)). Here’s what’s going to happen:

1. Your application will make use of the `ipcRenderer` to emit an event to the main process. These events are called _channels_ within Electron
2. If the main process registered a proper event listener (which listens for the event which was just dispatched) it’s capable of running proper code for this event
3. After everything is done the main process can emit yet another event for the result (in our case, the kernel version)
4. Now the entire workflow happens the other way around, the renderer process needs to implement a listener for the event dispatched in the main process
5. When the renderer process receives the proper event containing our desired information the UI can now show the information

Ultimately this entire process can just be seen as a simple request-response pattern, a bit like HTTP – just asynchronous. We’re going to request something via a certain channel and receive the response to that on a certain channel.

Thanks to TypeScript we can abstract this entire logic into a cleanly separated and properly encapsulated application, where we dedicate entire classes for single channels within the main process and utilize promises for making easier asynchronous requests. Again, this sounds a lot more complicated than it actually is!

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

## Bootstrapping an Electron application with TypeScript

The first thing we need to do is to bootstrap our Electron application with TypeScript. Our `package.json` is just:

```jsx
{
  "name": "electron-ts",
  "version": "1.0.0",
  "description": "Yet another Electron application",
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "start": "npm run build && electron ./dist/electron/main.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Kevin Hirczy <https://nehalist.io>",
  "license": "MIT",
  "devDependencies": {
    "electron": "^7.1.5",
    "typescript": "^3.7.3"
  }
}
```

The next thing we’re going to add is our Typescript configuration, `tsconfig.json`:

```jsx
{
  "compilerOptions": {
    "target": "es5",
    "noImplicitAny": true,
    "sourceMap": true,
    "moduleResolution": "node",
    "outDir": "dist",
    "baseUrl": "."
  },
  "include": [
    "src/**/*"
  ]
}
```

Our source files will live within the `src` directory, everything will be built into a `dist` directory. We’re going to split the `src` directory into two separate directories, one for Electron and one for our application. The entire directory structure will look something like this:

```jsx
src/
  app/
  electron/
  shared/
index.html
package.json
tsconfig.json
```

Our `index.html` will be the file loaded by Electron and is pretty simple (for now):

```jsx
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Hello World!</title>
  <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
</head>
<body>
  Hello there!
</body>
</html>
```

The first file we’re going to implement is the main file for Electron. This file will implement a `Main` class which is responsible for initializing our Electron application:

```jsx
// src/electron/main.ts

import {app, BrowserWindow, ipcMain} from 'electron';

class Main {
  private mainWindow: BrowserWindow;

  public init() {
    app.on('ready', this.createWindow);
    app.on('window-all-closed', this.onWindowAllClosed);
    app.on('activate', this.onActivate);
  }

  private onWindowAllClosed() {
    if (process.platform !== 'darwin') {
      app.quit();
    }
  }

  private onActivate() {
    if (!this.mainWindow) {
      this.createWindow();
    }
  }

  private createWindow() {
    this.mainWindow = new BrowserWindow({
      height: 600,
      width: 800,
      title: `Yet another Electron Application`,
      webPreferences: {
        nodeIntegration: true // makes it possible to use `require` within our index.html
      }
    });

    this.mainWindow.webContents.openDevTools();
    this.mainWindow.loadFile('../../index.html');
  }
}

// Here we go!
(new Main()).init();
```

Running `npm start` should now start your Electron application and show your `index.html`:

![hello world](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2019/12/helloworld.png?resize=800%2C628&ssl=1)

The next thing we’re going to implement is how our IPC channels are handled.

## Channel handling

Following [SoC](https://en.wikipedia.org/wiki/Separation_of_concerns) we’re going to implement one class per channel. These classes will be responsible for incoming requests. In the example above we’d have a `SystemInfoChannel` which is responsible for gathering system data. If you’d like to work with certain tools, let’s say control virtual machines with Vagrant, you’d have a `VagrantChannel`, and so on.

Every channel is going to have a name and a method for handling incoming requests – so we create an interface for that:

```jsx
// src/electron/IPC/IpcChannelInterface.ts

import {IpcMainEvent} from 'electron';

export interface IpcChannelInterface {
  getName(): string;

  handle(event: IpcMainEvent, request: any): void;
}
```

There’s one thing that stands out, `any`. Type-hinting `any` is a design flaw in many cases – and we’re not going to live with a design flaw. So let’s take a few moments to think about what type `request` really is.

Requests are sent from our renderer process. There are two things that might be relevant to know when sending requests:

1. We need to know that our channel may accept some parameters
2. We need to know which channel to use for the response

Both of them are optional – but we can now create an interface for sending requests. This interface will be **shared** between Electron and our application:

```jsx
export interface IpcRequest {
  responseChannel?: string;

  params?: string[];
}
```

Now we can go back to our `IpcChannelInterface` and add a proper type for our `request`:

```jsx
handle(event: IpcMainEvent, request: IpcRequest): void;
```

The next thing we need to take care of is how channels are added to our main process. The easiest way is to add an array of channels to our `init` method of our `Main` class. These channels will then be registered by our `ipcMain` process:

```jsx
public init(ipcChannels: IpcChannelInterface[]) {
  app.on('ready', this.createWindow);
  app.on('window-all-closed', this.onWindowAllClosed);
  app.on('activate', this.onActivate);

  this.registerIpcChannels(ipcChannels);
}
```

While the `registerIpcChannels` method is just one line:

```jsx
private registerIpcChannels(ipcChannels: IpcChannelInterface[]) {
  ipcChannels.forEach(channel => ipcMain.on(channel.getName(), (event, request) => channel.handle(event, request)));
}
```

What’s happening here is that channels passed to our `init` method will be registered to our main process and handled by their responding channel classes. To make that easier to follow let’s quickly implement a class for our system info from the example above:

```jsx
// src/electron/IPC/SystemInfoChannel.ts
import {IpcChannelInterface} from "./IpcChannelInterface";
import {IpcMainEvent} from 'electron';
import {IpcRequest} from "../../shared/IpcRequest";
import {execSync} from "child_process";

export class SystemInfoChannel implements IpcChannelInterface {
  getName(): string {
    return 'system-info';
  }

  handle(event: IpcMainEvent, request: IpcRequest): void {
    if (!request.responseChannel) {
      request.responseChannel = `${this.getName()}_response`;
    }
    event.sender.send(request.responseChannel, { kernel: execSync('uname -a').toString() });
  }
}
```

By adding an instance of this class to our `init` call of our `Main` class we have now registered our first channel handler:

```jsx
(new Main()).init([
  new SystemInfoChannel()
]);
```

Now every time a request happens on the `system-info` channel the `SystemInfoChannel` will take care of it and handle it properly by responding (on the `responseChannel`) with the kernel version.

Here is what we’ve done so far visualized:

![renderer process](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2019/12/rendererprocess.png?resize=724%2C165&ssl=1)

Looks good so far, but we’re still missing the part where our application actually does stuff – like sending a request for gathering our kernel version.

## Sending requests from our application

To make use of our clean main process’ IPC architecture we need to implement some logic within our application. For the sake of simplicity, our user interface will simply have a button for sending a request to the main process which will return our kernel version.

All of our IPC-related logic will be placed within a simple service – the `IpcService` class:

```jsx
// src/app/IpcService.ts

export class IpcService {
}
```

The first thing we need to do when using this class is to make sure we can access the `ipcRenderer`.

In case you’re wondering why we need to do that, it’s because if someone opens the `index.html` file directly there’s no `ipcRenderer` available.

Let’s add a method which properly initializes our `ipcRenderer`:

```jsx
private ipcRenderer?: IpcRenderer;

private initializeIpcRenderer() {
  if (!window || !window.process || !window.require) {
    throw new Error(`Unable to require renderer process`);
  }
  this.ipcRenderer = window.require('electron').ipcRenderer;
}
```

This method will be called when we try to request something from our main process – which is the next method we need to implement:

```jsx
public send<T>(channel: string, request: IpcRequest = {}): Promise<T> {
  // If the ipcRenderer is not available try to initialize it
  if (!this.ipcRenderer) {
    this.initializeIpcRenderer();
  }
  // If there's no responseChannel let's auto-generate it
  if (!request.responseChannel) {
    request.responseChannel = `${channel}_response_${new Date().getTime()}`
  }

  const ipcRenderer = this.ipcRenderer;
  ipcRenderer.send(channel, request);

  // This method returns a promise which will be resolved when the response has arrived.
  return new Promise(resolve => {
    ipcRenderer.once(request.responseChannel, (event, response) => resolve(response));
  });
}
```

Using generics makes it possible for us to get information about what we’re going to get back from our request – otherwise, it would be unknown and we would have to be a wizard in terms of casting to get proper information about what types we’re really dealing with. Don’t get me wrong here; being a wizard is awesome – but having no type information is not.

Resolving the promise from our `send` method when the response arrives makes it possible to make use of the `async/await` syntax. By using `once` instead of `on` on our `ipcRenderer` we make sure to not listen for additional events on this specific channel.

Our entire `IpcService` should look like this by now:

```jsx
// src/app/IpcService.ts
import {IpcRenderer} from 'electron';
import {IpcRequest} from "../shared/IpcRequest";

export class IpcService {
  private ipcRenderer?: IpcRenderer;

  public send<T>(channel: string, request: IpcRequest): Promise<T> {
    // If the ipcRenderer is not available try to initialize it
    if (!this.ipcRenderer) {
      this.initializeIpcRenderer();
    }
    // If there's no responseChannel let's auto-generate it
    if (!request.responseChannel) {
      request.responseChannel = `${channel}_response_${new Date().getTime()}`
    }

    const ipcRenderer = this.ipcRenderer;
    ipcRenderer.send(channel, request);

    // This method returns a promise which will be resolved when the response has arrived.
    return new Promise(resolve => {
      ipcRenderer.once(request.responseChannel, (event, response) => resolve(response));
    });
  }

  private initializeIpcRenderer() {
    if (!window || !window.process || !window.require) {
      throw new Error(`Unable to require renderer process`);
    }
    this.ipcRenderer = window.require('electron').ipcRenderer;
  }
}
```

## Putting everything together

Now that we have created an architecture within our main process for handling incoming requests and implemented a service to send such services we are now ready to put everything together!

The first thing we want to do is to extend our `index.html` to include a button for requesting our information and a place to show it:

```jsx
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Hello World!</title>
  <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';"/>
</head>
<body>
<button id="request-os-info">Request OS Info</button>
<div id="os-info"></div>
<script>
  require('./dist/app/app.js');
</script>
</body>
</html>
```

The `app.js` required doesn’t exist yet – so let’s create it. Keep in mind that the referenced path is the built file – but we’re going to implement the TypeScript file (which lives in `src/app/`)!

```jsx
// src/app/app.ts

import {IpcService} from "./IpcService";

const ipc = new IpcService();

document.getElementById('request-os-info').addEventListener('click', async () => {
  const t = await ipc.send<{ kernel: string }>('system-info');
  document.getElementById('os-info').innerHTML = t.kernel;
});
```

And et voilà – we’re done! It might seem unimpressive at first, but by clicking on the button now a request is sent from our renderer process to our main process, which delegates the request to the responsible channel class and ultimately responds with our kernel version.

![main ui](https://i1.wp.com/blog.logrocket.com/wp-content/uploads/2019/12/requestosinfo.gif?resize=730%2C363&ssl=1)

Of course, things like error handling and such needs to be done here – but this concept allows for a very clean and easy-to-follow communication strategy for Electron apps.

The entire source code for this approach can be found on [GitHub](https://github.com/nehalist/electron-ips-ts).

* * * 
**Editor's note:** Seeing something wrong with this post? You can find the correct version [here](https://blog.logrocket.com/electron-ipc-response-request-architecture-with-typescript/).

## Plug: [LogRocket](https://logrocket.com/signup/), a DVR for web apps
 
![LogRocket Dashboard Free Trial Banner](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png?resize=1200%2C677&ssl=1)
 
[LogRocket](https://logrocket.com/signup/) is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
[Try it for free](https://logrocket.com/signup/).
* * *
The post [Electron IPC Response/Request architecture with TypeScript](https://blog.logrocket.com/electron-ipc-response-request-architecture-with-typescript/) appeared first on [LogRocket Blog](https://blog.logrocket.com).