In programming, a bad decision is like a node_modules directory — it never stops growing.
Sometimes you have to step back and realize you’ve been solving problems that never needed to exist.
Have you ever doubted choosing a "popular" stack for your next important project? Maybe, but growing complexity and frustrating developer experience were not enough to outweigh the "safe" choice argument. But you’ve likely heard the news regarding its literal safety.
You can probably sense that the industry is in need of a next generation of tools, just as React brought us one in the past.
I wrote a full-stack framework for dynamic web applications in the Go ecosystem. It has reactive state, components, lifecycle control, SSR by default, and utilizes an HTTP API-free architecture.
You can find what led me to this decision and details about the architecture in my previous articles.
This article provides a general overview of the framework's API and some thoughts behind its design, and at the end, I will share a link to the tutorial and GitHub repo.
So, you can ship dynamic, secure, and fast web apps with Go. And you will have a better developer experience while doing so.
Fix The Web Dev: Part 4, The Framework.
Fundamentals
Philosophy: Deconstruct the modern front-end framework concept into orthogonal, composable primitives that developers can use independently or combine into purpose-built abstractions.
DOM updates: control > abstraction.
Modern web UI, at its core, is dynamic HTML. How to approach it — first decision I needed to make.
Reactive frameworks abstract DOM manipulation: the component's render function reruns "virtually" when the state changes, allowing the framework to calculate the difference with the previous output and apply it.
Vanilla JS, jQuery, and HTMX-like solutions, on the other hand, work directly: take the element in a hard way by selector and insert new content.
The first one goes against my philosophy (similar behavior should be possible, not enforced). The second one is essentially just string manipulation with no semantics.
Meet Door
A dynamic container in the DOM tree that can be updated, replaced, or removed at runtime in a type-safe way.
Door's methods:
-
Update
- changes its content (like innerHTML) -
Replace
- replace entirely (like outerHTML) -
Clear
- removes content -
Remove
- removes entirely -
Reload
- re-renders (useful if you query data during render and want new or modified data to appear)
Internally, Doors form a tree structure, where each branch has its own lifecycle. Also, Door provides local context that can be used as an unmount hook.
Door is a straightforward tool for DOM manipulation and a foundation for higher abstractions. However, many things can be done in a simple way just by using it directly.
Reactive State: communication > side effects.
Conceptually, state in modern UI is a combination of two roles:
- Argument to the render function, determining the resulting output.
- Trigger to initiate re-rendering.
But when you look at the code of a sophisticated React component, this concept breaks down. State is often used as a communication primitive or intermediary data, leading to a chain of useEffect
hooks that produce the actual render-driving state.
I don’t think developers are following bad practices; the tool itself naturally pushes real-world code toward that abuse. That is the insight.
Meet Beam
Communication primitive first, render driver by choice.
A changing value stream that can be read, subscribed to, watched, or derived.
Let's combine Beam
and Door
:
Or use the helper component (if you don't need precise control) to reduce boilerplate:
- Can hold any type of value
- Triggers subscribers upon value change (
==
or custom distinction function) - Origin (
SourceBeam
) can be updated or mutated - Not bound to a specific fragment or container
- Respects the dynamic container tree, guaranteeing render consistency, beam shines through doors
- In practice, you derive element-scoped state so each DOM node depends only on what it needs (diff data, not DOM)
Routing: freedom > automation.
In the PHP era, the path portion of a URI often was an actual filesystem path to a script, while the query parameters (the key-value map after ‘?’) functioned much like React component props. Item IDs, pagination, and filters - every page variation was expressed through query values.
Today, the path itself has become an abstraction, and means whatever the developer wants… or does it?
Front-end frameworks rely heavily on the path (less flexible than a key–value map) while enforcing some coupling between path structure and the component/file tree. It feels rudimentary and underpowered. And it's a shame, because the ability to encode some state directly in the URI is incredibly powerful.
Meet Path Model.
Route is changing data, and changing data is state.
Each page route is an annotated struct used to match, decode, and encode the URI.
You can:
- Declare multiple path variants (the matched field becomes true)
- Use type-safe parameter capturing
- Use splat parameter to capture the remaining path tail
- Use almost any types for query parameters (go-playground/form under the hood)
How to work with it
The path model comes through Beam into the page render function:
Derive a specific piece from the path model:
Render dynamic content based on its value by subscribing to it:
Also, you can generate links and navigate programmatically via SourceBeam
value mutation.
This approach requires more code than usual. But its type-safe, deterministic, and declarative nature gives a solid, confident experience and unmatched freedom in route modeling.
Toolkit
Philosophy: It’s better to deal with a comprehensive system than with a set of shortcuts. Adding your own wrapper once is far easier than fighting missing features.
Straightforward and type-safe event binding.
To attach an event listener, render the "magic" attribute in front of the target element:
This creates a protected HTTP endpoint and adds event-binding attributes to the element.
The attribute structure provides fields for event flow control, concurrency rules, pre-actions, error handling, and more.
Flexible indication API.
To communicate processing, use any combination of indicators and selectors with the Indication API:
Temporary indications
- Add class
- Remove class
- Set attribute
- Set content
Selectors:
- Event target element
- Global document query selector
- Closest ancestor query selector
Indications are automatically cleared once all triggered state and UI changes are applied.
Nuanced concurrency management.
Occasionally, UIs are hit with overlapping actions — rapid form resubmission, double clicks, and conflicting actions. This issue becomes especially pronounced when event processing happens remotely.
The framework offers basic guarantees at a single hook level. Processing occurs sequentially, and if the hook function returns true, subsequent invocations are ignored. That’s too relaxed for some cases, so precise control can be enabled on the client side via the Scopes API.
- Debounce: delays handling of rapid bursts of events.
- Blocking: cancels new events while one is processing.
- Serial: queues events on the client side and processes them sequentially.
- Priority: cancels lower-priority events when a higher-priority event occurs.
- Frame: creates a boundary between pending events and new ones
Scope value can be shared between handlers to coordinate behavior across them. Additionally, you can combine multiple scopes to form a pipeline.
Combining Scopes may feel complex, but it enables highly dynamic UI without artifacts.
Straightforward JS integration.
There is nothing wrong with using JS for small UI tasks, and in many cases, a client-side solution is more suitable (e.g. drawing).”
The framework allows you to:
- declare custom hooks (request handlers) and trigger them in JS like a private API
- register JS handlers and call them from Go.
- pass Go data to JavaScript
Extras
- File hosting (private and public)
- CSP header automation
- Import map generation
- JS/TS bundling and building
- Routing options
- Dynamic attributes
- Active link highlighting
- Session and instance lifecycle control
- Performance and resource management tweaks
Concerns Addressed
Processing events on the server causes delays and hurts UX.
Response:
With a ping of less than 100ms, the response feels almost instant. With 200ms+, it's pronounced, but acceptable. For better UX, you need servers closer to the customer regardless of stack.
Also, in a "traditional" front-end framework, if an event triggers an HTTP request anyway, there is essentially no difference.
Lack of animations.
Response:
Some transitions can be achieved via the Indication API + CSS.
If there really is a demand for more, let me know, and I will figure it out.
Some APIs are verbose.
Response:
Intentionally. My approach is to give you more control. You can always write a small wrapper and use it everywhere — better than hacking missing functionality.
LSP support?
Response:
There are LSP, highlighting and IDE plugins for templ, and it does its job.
However, not perfect. Also, it's nice to have some framework-specific support. If it succeeds, that is a first priority alongside the LLM-specific docs.
Learning curve?
Response:
No doubt, it's different from the tools you're familiar with.
It took me about two weeks to build my approaches and figure out the best practices. But once I got there... I didn't expect it to turn out so rewarding. It has a solid, almost mechanical feel, while being naked to your eye, because of its elementary nature. And the absence of client-server friction makes it flow; I couldn't imagine myself using the traditional stack again.
Licensing
As mentioned in the previous article, the framework is a paid product (lifetime license, no subscriptions). However, it's free for development and non-commercial production use.
Additionally, it's licensed under BUSL-1.1, so each version becomes open source after 4 years of release.
I want it to be sustainable, supported, and continually improved.
Conclusion
It's called doors.
In the two months since my last article, I’ve improved developer experience, performance, and completed documentation and tests to an appropriate level.
The documentation is still not the best, the tutorial is not comprehensive, and there are probably some bugs left. But I am very satisfied with the framework itself and its internals.
I need your input to push it further. Please try it out, open GitHub issues with any questions, and share your feedback.
Stay tuned.
The Link
- GitHub
- Docs and tutorial (works on the framework itself)
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.