Published on April 26, 2025

Development

Emulating a Back-End Server for Electron Applications

When creating an Electron app, you might find yourself having to deal with interprocess communication (IPC). However, you can use the familiar client-server communication approach from web development. Without starting a local server.

This approach would give you a partial independence from the stack. You could simply run a server locally and access the user interface from the browser. Or port your app to be used in a browser, once all the needed features are available there.

You can find the source code in my repository. This solution is also available as a Node.js module on npm. There is also a short usage example at the end of the article.

Structure

  • Server: serves API and possibly your front-end code;

  • Electron: entry to your app, as usual;

  • Proxy: intercepts your “server” requests and emulates results.

Dependencies

I am going to use Express.js to create a server and light-my-request to emulate requests to the server:

npm install express light-my-request

Server

I am going to use Express. For the sake of simplicity, our server will have one API endpoint: /api/ping returning a string "Pong".

// server.js
import express from "express"

export const server = express()
server.get("/api/ping", (request, response) => {
    response.setHeader("Content-Type", "text/html")
    response.send("Pong")
})

You could also let the server handle your front-end files or even perform requests to your bundler’s development server.

Electron

We do not have the proxy yet, but we need to understand how it is going to be used. This is mostly boilerplate code, but the important part is the protocol.handle call. It allows us to intercept requests using a specified protocol (in our case HTTP).

// app.js
import { app, protocol, BrowserWindow } from "electron"
import { createProxy } from "./proxy.js"
import { server } from "./server.js"

const baseURL = new URL("http://example-app.local")

function createMainWindow() {
    const window = new BrowserWindow()
    const url = new URL("./api/ping", baseURL)
    window.loadURL(url.toString())
}

function onAppReady() {
    // Here we register our proxy
    protocol.handle(
        // We do not want the ‘:’ at the end
        baseURL.protocol.slice(0, -1),
        createProxy(baseURL, server)
    )

    createMainWindow()
}

function onAllWindowsClosed() {
    if (process.platform !== "darwin") {
        app.quit()
    }
}

function onActivated() {
    if (BrowserWindow.getAllWindows().length === 0) {
        createMainWindow()
    }
}

app.whenReady().then(onAppReady).catch(console.error)
app.on("window-all-closed", onAllWindowsClosed)
app.on("activate", onActivated)

Proxy

What we want the proxy to do is:

  • emulate a backend request if the host matches our base URL;

  • perform a normal request otherwise.

createProxy is going to return a function that will be executed on every HTTP request. That function has to return a Response object. Here is the skeleton for our code:

// proxy.js

// To perform a normal request
import { net } from "electron"
// To emulate a request to the back end
import inject from "light-my-request"

export function createProxy(baseURL, server) {
    return request => {
        const url = new URL(request.url)

        // If the host doesn’t match our base URL:
        if (url.hostname !== baseURL.hostname) {
            // Simply send the request.
            return net.fetch(request)
        }

        // Otherwise we want to emulate the request.
        return emulateRequest({ server, baseURL, url, request })
    }
}

Now we need to define the function that is going to emulate our request. It simply calls the inject function from the light-my-request library, adapting the argument and response types to the ones we need.

const cacheDisablingHeaders = {
    "Cache-Control": "no-cache, no-store, must-revalidate",
    "Pragma": "no-cache",
    "Expires": 0,
    "If-None-Match": null
}

async function emulateRequest({ server, baseURL, url, request }) {
    // This function call simply emulates the request.
    const response = await inject(server, {
        method: request.method,
        url: request.url,
        query: url.search,
        remoteAddress: new URL("/", baseURL).toString(),
        body: Buffer.from(await request.arrayBuffer()),
        // We need to convert request headers to the right format and add a few more to prevent errors.
        headers: {
            ...Object.fromEntries([...request.headers]),
            // We don’t want to cache the result
            ...cacheDisablingHeaders
        }
    }).end()

    // Convert the response to a Node.js’ Response object.
    return new Response(response.body, {
        headers: response.headers,
        status: response.statusCode,
        statusText: response.statusMessage
    })
}

As you might have noticed, we disable caching for these local requests. The reason is that Node.js throws an error when trying to create a Response object with status 304 Not Modified. I am not sure whether there are other ways to prevent this error from happening, but the current approach works.

The Result

If you run npx electron app.js, you should see the following:

loading-ag-132

Library

To make this approach reusable, I published a library that does all of this for you. First, add it as a dependency to your project:

npm install electron-client-server electron

Then import it in your entry file:

import { applyProxy } from "electron-client-server"

Define the URL that will be considered as the server address:

const baseURL = new URL("http://localhost:5173")

Last, in a callback after app.whenReady(), call:

applyProxy("http", baseURL, server)

I have only tested it with Express. Fastify should also work. And probably even other libraries.

If you find any bugs or suggestions, please consider opening an issue on the repository page.


Published on October 12, 2024

Development

Overcome Framework Lock-In

by migrating your UI library to the Web Components API.

Not long time ago, I rediscovered the Web Components API. While many developers are used to popular front-end frameworks, I would reconsider this built-in browser feature in new projects.

Why?

Framework Support

Each time a new framework comes out, we reimplement all the basic UI components like popups, dropdowns, and forms from scratch. In the best case we only have to create a wrapper around available libraries. Even though popular frameworks have many UI libraries to choose from, web components are universal. They might even work faster because of direct DOM manipulations.

Familiarity

Using custom web components is familiar, since it’s the same way we use all the other components. It uses the same approach as in vanilla web development: you write HTML code, style the elements with CSS, and add interactivity with JavaScript. You (almost) don’t have to learn anything if you already know the basics of web development.

Ease of Use

As a developer using a custom HTML element, you only need to import a script defining the element and use the element in your HTML markup. Bundlers aren’t required nor are any external libraries for rendering the component.

When?

I don’t think we should “spam” all our projects with web components. However, whenever we develop a UI component or a library that could be reused in another project, it would make sense to implement it as a custom HTML element. At least if you want to reach the widest audience and/or you aren’t sure whether you’re going to stick with one framework.

If you find it difficult to change back to imperative programming to implement a custom element, consider checking out Lit Element. It’s a library/framework that’s built on top of Web Components API, providing declarative syntax and additional features.

How?

In this article, two examples are provided. If you are willing to learn more, consider the guides from MDN, JavaScript.info, or web.dev.

Counter Element Example

First, create an HTML file index.html with the following layout:

<!-- index.html -->
<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>Web Components API Demo</title>

		<!-- Connect the script with our custom element to the page -->
		<script src="counter-element.js"></script>
	</head>
	<body>
		<!-- Here is how our custom element looks in the layout! -->
		<counter-element count="1"></counter-element>
	</body>
</html>

Then create a JavaScript file counter-element.js with the following code:

class CounterElement extends HTMLElement {
	// Change of these attributes triggers the ‘attributeChangedCallback’ call
	static observedAttributes = ["count"]

	// This method gets called whenever an observed attribute
	// changed its value (including when it gets set for the first time)
	attributeChangedCallback(attributeName, oldValue, newValue) {
		if (attributeName === "count") {
			this.innerText = `Count: ${newValue}`
		}
	}

	// This method gets called every time
	// the element gets appended to the DOM
	connectedCallback() { /* ... */ }

	// This method gets called every time
	// the element gets unmounted from the DOM
	disconnectedCallback() { /* ... */ }
}

// Before we are able to use the custom element on the page,
// we need to define it in the custom element registry.
// The element name has to have a ‘-’ in it,
// since one-word element names are reserved
customElements.define("counter-element", CounterElement)

If you wish to ensure the value gets updated on attribute change, create an interval that continuously updates the counter attribute.

Customizing built-in elements

In case you want to customize a built-in element, some features and advantages of the Web Components API disappear:

  • HTML syntax changes from <your-component-name> to <built-in-component-name is="your-component-name">

    • In CSS, your-component-name becomes built-in-component-name[is=your-component-name] accordingly
  • You cannot attach shadow DOM to customized built-in components

Still, there are ways to overcome these limitations, which won’t be covered in this article.

Here is an example for a custom button that prints to the console whenever it is being clicked:

class MyCustomButton extends HTMLElement {
	constructor() {
		super()
		this._onClick = () => console.log("Click!")
	}

	connectedCallback() {
		this.addEventListener("click", this._onClick)
	}

	disconnectedCallback() {
		this.removeEventListener("click", this._onClick)
	}
}

customElements.define("my-custom-button", MyCustomButton, {
	extends: "button"
})

// And now you can use it in HTML the following way:
// <button is="my-custom-button">Click me</button>

Summary

This article has covered reasons to use Web Components API more often, listed some situations when one should do that, and offered code samples and resources to deepen one’s knowledge.