Hello Counter

2025-09-29

The views and opinions expressed here are solely mine and do not necessarily reflect the views of any organization I have worked or I am working for.

Why Hello Counter?

Introduction

I have created a small web application and wanted to write down some of my thought process and my current view on web applications.

The web application is intended to be used to simulate a tax return in Canton Zurich, here is the link if you want to try it: https://zhteuern.giannijiang.vip/

(Please use the official sites and guides if you have to declare a tax return)

With my immense knowledge of the German language I came up with the name zhteuern:

Canton Zurich's website has a tax return demo (link) but I wanted a stripped down version with better user experience.

Do I think I achieved better UX with my app compared to their demo?

Yes, and the reason is simple: I don't understand German my app has keyboard navigation.

Web Application

Before continue reading here some of questions for you if you have to deal with web applications at your job:

  1. How many JavaScript resources do you think are being downloaded on a typical web application? estimate in KB or MB or GB

  2. If an application has roles "editor" and "admin" (each user can be only one of the two) and it has an admin page, how would you design the front page and data exchange with the backend so that the link to the Admin page is visible only to users with "admin" role? (assume you can already identify the user and the role)

The html of the web page can be generated by the client or by the server. I prefer the second.
One issue with client side rendering is that the html is created using JavaScript and DOM manipulation: the JavaScript content has to be parsed/compiled by the browser and then executed to create the html of the page. More UI elements will lead to more JavaScript, more time to parse/compile/execute etc...

Another issue, in my opinion, is the architectural complexity needed to support the CSR paradigm: in order to display some dynamic data there is a need for the browser to access the data, which is often accomplished by exposing some APIs by the backend.

If you tried to solve question 2 using a client side rendered solution you would most likely have thought about having the backend expose an http endpoint with a json response in the form of:

{
    "username": "foo-bar",
    "role": "editor"
}

and something like this in the frontend code (this is not valid JavaScript but most frontend frameworks will let you write something similar):

if (user.role == "admin") {
    <a href="/admin.html">Admin Page</a>
}

Do you see the problem?
Pause reading for a moment to think. Hover below to read what I see as an issue.

A user with "editor" role won't see the link in the page html, which is correct, but can still analyze the JavaScript code and discover of the link of the admin page.

With client side rendering the blueprint of the UI is sent to the browser with risks of leaking data that the users should/must not be aware of due to the logics involved in generating the html.

You could change the http endpoint with an extra admin_link field that is only filled for "admin" users:

{
    "username": "foo-bar",
    "role": "editor"
}
{
    "username": "moo-baz",
    "role": "admin",
    "admin_link": "/admin.html"
}

and replace the admin page link value in the frontend snippet but now the same role check has to be repeated by the backend as well.

Why is server side rendering better in my opinion?

hello-counter-dbtohtml.webp

For question 2 you only do the check once in the backend, omitting the admin link in the html sent as http response if the user is not "admin", with less risk of data leaking.

In an "interactive" web application the browser has to do mainly 3 things:

The difficulties with server side rendering is that you still need some JavaScript to accomplish sending data to the server and displaying the data received from the server, unless you want to limit yourself with the use of form elements and a forced page reload on any data update.
I'll come back to these points later.

Suggested read (medium.com): The Cost of JavaScript

Tools used for zhteuern

Honorable mentions

I tried these tools but decided not to use them at the end.
They are great but I preferred to limit the dependencies.

Why not JavaScript framework X?

There are JavaScript full-stack frameworks like NextJs, NuxtJs, Sveltekit etc... that can also support server side rendering.

I think they are the better options over their client side versions (React, Vue, Svelte) + having a separate backend.
You can use the same language JavaScript/TypeScript and re-use some of the code on both BE and FE (probably).

The issues I see with these JavaScript frameworks are:

On the "positive" side, I am pretty sure that you can spin up a production ready draft web application using AI very quickly since there are many examples out there with these frameworks.

Why not Datastar?

So, initially I was about to use htmx for the project but then I found Datastar.

It is a small JavaScript framework that is only about 11 KiB in size.

It simplifies how you write the interactions between the browser and the server by leveraging data-* html attributes to add functionalities to the html elements.

This means that you can use it with any backend languages, as long as the backend http response conforms to the format that is expected by Datastar.

Don't have space to explain everything (not like I would be able to...) so I'll only list what I think are the most relevant features of the framework.

Coming back to what the browser has to do in an interactive web app:

send data to the server

With Datastar you can define the triggering event, the http request method and request endpoint like this:

<button data-on-click="@put('/my-butt-on-fire')">Button</button>

You can use the browser default events (click, input, keydown...), Datastar defined events (intersect, interval, load...) or use your own custom named events.

Datastar's signals, which are reactive JavaScript variables, are also sent with the http request.

receive data from the server to be displayed

When you need to update the UI after a Datastar initiated http request (example button clicking like in the previous snippet), you can return html in the http response.

The default behavior is that the html elements in the http response will replace the current html elements in the page based on a match by html ID but you can define the target query selector and the strategy (prepend, append, before, after...) if you prefer.

The best part is that you can stream the html elements using Server Sent Event: check the "Hello World" example on their website https://data-star.dev/#hello

do some local stuff

An example of Counter component using signals with no server interaction would look like:

<div data-signals-count="0">
Count is <span data-text="$count"></span>
<button data-on-click="$count++">Increment</button>
</div>

When I tried Datastar it felt nice and fun to use:

But I wanted to experiment and try to have a smaller JavaScript bundle so in the end I copy-pasted cloned their SSE handling and part of html merging code, while leaving out the signals stuff: technically it is not Datastar but the zhteuern web app is using the SSE streaming approach to update the UI.

Reading Datastar's source code (Github Link) a noticeable part of the html merging code is given by the default morphing strategy that compares the new and the old html elements and their children: it applies the new attributes and value to the old matched elements.

If I understood the reasons correctly, the main benefits are:

Suggested watch (youtube.com): Real-time Hypermedia - Delaney Gillilan
Datastar has changed since then but it is still worth watching

How much JavaScript did I end up with?

In order to reduce the JavaScript bundle size I ditched Datastar's morphing code, while keeping the other strategies (replaceWith, prepend, append, after, before), though in the application I effectively only used replaceWith.

I also came up with the brilliant idea to have a new event mapping to drive the UI changes from the backend (other than sending just html).

I was thinking, if I have an html element like

<input id="my-id" value="hello"/>

what is the JavaScript code needed to update the "value" property to "world"?

It would look like

const myInput = document.getElementById("my-id");
myInput.value = "world";

If I replace getElementById("my-id") with querySelectorAll("#my-id") it would still work.

To make it more generic, what is needed to update a property of an html element is:

I don't have to limit the updates only to element's properties: I can apply the same concept to attributes, styling etc..

So the Go structure and TypeScript function I ended up with for the new event mapping are:

type Update struct {
	Selector  string            `json:"selector"`
	Text      map[string]string `json:"text,omitempty"`
	Integer   map[string]int    `json:"integer,omitempty"`
	Boolean   map[string]bool   `json:"boolean,omitempty"`
	Attribute map[string]string `json:"attribute,omitempty"`
	Style     map[string]string `json:"style,omitempty"`
	Remove    *bool             `json:"remove,omitempty"`
}
interface Update {
    selector: string;
    text: { [key: string]: string };
    integer: { [key: string]: number };
    boolean: { [key: string]: boolean };
    attribute: { [key: string]: string };
    style: { [key: string]: string };
    remove: boolean;
}

function applyJson(updates: Update[]) {
    for (let index = 0; index < updates.length; index++) {
        const update = updates[index];
        const targets = document.querySelectorAll(update.selector);
        if (targets == null || targets.length == 0) {
            console.error(`no target found for selector ${update.selector}`)
            return;
        }
        for (let index = 0; index < targets.length; index++) {
            const target = targets[index];
            for (const [key, value] of Object.entries(update.text ?? {})) {
                target[key] = value;
            }
            for (const [key, value] of Object.entries(update.integer ?? {})) {
                target[key] = value;
            }
            for (const [key, value] of Object.entries(update.boolean ?? {})) {
                target[key] = value;
            }
            for (const [key, value] of Object.entries(update.attribute ?? {})) {
                target.setAttribute(key, value);
            }
            for (const [key, value] of Object.entries(update.style ?? {})) {
                (target as HTMLElement).style.setProperty(key, value);
            }
            if (update.remove ?? false) {
                target.remove();
            }
        }
    }
};

with the idea that, since I have this TypeScript code in the same codebase (and using esbuild to transpile it to JavaScript), I can extend the mapping (if needed) for other stuff like dispatchEvent, scrollIntoView and so on.

PROs:

CONs:

I would say that it is fine only for few fine grained updates.

Interactivity in the frontend is usually event driven: you attach event listener the the html elements with a callback function to execute when the event happens.

There are many browser APIs that are available (see https://developer.mozilla.org/en-US/docs/Web/API) to be used to emit events to which we can add event listeners to trigger an http request to the server.

I liked the idea of having the endpoint of the http request defined in the html tag, so I defined some attributes for it like init-fetch, input-fetch and intersect-fetch that are triggered (respectively) on element being added to the dom, user input on input elements, element appearing in the viewport.

The most important Web APIs (in my opinion) are:

<div id="hot-reload" init-fetch="/hot" method={ http.MethodPatch } retry="10"></div>

Had to write some other functions for events like click (to scroll up-down), keydown (keyboard navigation and avoid input elements of type numeric to accept 'e', 'E', '+', '-', '.', ','), input (send input values to server when they change).

The final brotli-compressed JavaScript bundle is just a bit less than 2 KB.

curl -s -o /dev/null -w "%{size_download}\n" \
https://zhteuern.giannijiang.vip/brotli/zhteuern.js
# 1844

I think most CRUD web applications don't need much JavaScript once you solve how to dinamycally update the UI from the server.

Html and CSS can also help manage some of the UI interactions in the browser, like I'll mention later.

Server Sent Event

Needed a section for SSE. The usual approach to frontend is getting your application state and applying a transformation to it. The output is what you show the user.
UI becomes a function of state.

What bothers me a bit is that, in most web applications, the previous statement is only "guaranteed" to be true at the moment of page load: if you visit an "Amazing" shopping website on two different browser pages, you can try to add a product to the cart in one of the pages. Does the other browser page's cart product count also update without a page reload? When I tried it: no, it didn't update automatically.

What I appreciate about Datastar's approach is that with SSE you can decide to push the view to the browser after any state change detected by the backend.

In zhteuern, if you have the same tax simulation opened in two browser pages then you should see that they are always in sync after an input change on any of the two pages. You can check the Network tab of the browser page and analyze the PATCH request's response if you are interested to see what the server is sending to drive the update.

In the app I added an online count to remind myself that I am the best: I will always be the number 1.

The live update could be achieved by WebSocket and polling as well but SSE has some advantages:

The downside is that browsers have a limit of 6 open connections (the limit is web domain based), but this issue is not present for http 2 and above

CSS and HTML

My experience using CSS so far:

hello-counter-css.webp

Thank goodness we have:

because I really have no clue how CSS works.

Best I could do on my own for styling was adding the border color.

I think CSS pseudo selector and html can already do a lot of work for many web applications to replace many trivial state management usually done in JavaScript, like toggling elements with :checked, using details + summary for accordions, using the Popover API for modals.

MPA vs SPA

I tried an hybrid approach using the ancor links with the hash property (ex: #income, #investment etc...) and some css to control the visibility of the sections.

PROs:

CONs:

a:target {
    background: white;
    color: black;
    border-color: black;
}

section.page {
    display: none;
}

#main:has(#income:target) section.page[name='income'] {
    display: block;
}

#main:has(#investment:target) section.page[name='investment'] {
    display: block;
}

#main:has(#deduction:target) section.page[name='deduction'] {
    display: block;
}

#main:has(#result:target) section.page[name='result'] {
    display: block;
}

Conclusion

If the mentality to create web applications could be shifted to opt for a backend driven server rendered UI then all the points above just become positive elixir tradeoffs.

And yes, the application is probably pointless because you could do the same thing with an Excel file...

Thanks for reading.