Previous article: API Standardization and the Birth of Form Architecture
Opening Context
In the previous articles, I described three major stabilizations in sequence: TypeScript reduced fragile handling through type safety, ElementWrap improved DOM locality, and form classification gave API interactions a consistent boundary.
After that, a different question remained unresolved.
How should one page itself be represented?
Early Confusion: How to Bind Logic to a Page
At first, I treated page initialization as a wiring problem.
When a screen loaded, where should its logic begin? What should own the first event binding? What should hold references to key elements after that?
The most natural idea was one JavaScript file per page. I gave each endpoint its own script target and tried to keep boundaries that way. Conceptually, it sounded clean.
In practice, bundling and entry configuration were not clean at all for me at that stage. My Webpack understanding was still shallow, and each structural adjustment had side effects on build rules, path mapping, and output organization. I spent a long time in that unclear zone, where I could make things work but could not explain the structure with confidence.
I do not feel embarrassed about that period. It was a normal result of learning while shipping.
True clarity around bundling only arrived much later, when generative AI tools and autonomous agents made it easier to explore configuration patterns quickly and compare alternatives without burning whole days on trial-and-error setup.
Endpoint-Based Initialization
Before structure matured, I used the simplest reliable trigger.
If each screen had its own endpoint, then page-specific logic could run on DOMContentLoaded. That solved startup in a technical sense. Code executed at the right moment, and users could use the screen.
Also, because basic HTML was assembled on the server side, it was reasonable to treat target tags as existing when TypeScript searched and bound them. That assumption was stable in my environment and did not create irrational risk by itself.
I did generate some elements on the TypeScript side, but the scope was limited. Typical examples were message displays and panels for selecting specific entities. Those dynamic parts were useful to standardize, yet they were still bounded utilities rather than the main screen skeleton.
But complex screens exposed a different limitation immediately.
Those screens needed state ownership, reusable interactions, and predictable update paths. A procedural file with startup handlers was enough for small behavior, but it became brittle once a screen started accumulating modal coordination, list updates, and edit transitions.
The initialization problem was solved. The representation problem was not.
The Controller Realization
The turning point was practical, not theoretical.
In the early phase, I first tried to avoid a dedicated controller and rely on shared helpers plus a load-event registration method implemented as an ElementWrap static method. On the surface, that looked simple.
The cost appeared later. Instances that composed each screen were managed in scattered locations, ownership became ambiguous, and state changes were harder to follow during revisions.
For complex screens, it became more natural to define one controller class and let that instance own page references, mutable state, and behavior methods. Instead of scattering handlers across free functions and static helpers, I could keep interactions in one class boundary and initialize the screen by creating that class.
Most screens did not need anything more complicated than that.
I prioritized consistency over minimal code volume. Even when a very small screen could have worked with a few direct handlers, I often still used the same controller pattern so that moving between screens required less context switching.
That choice paid off repeatedly. List screens reused common structure more easily. CRUD flows became more uniform in naming and execution shape. Boilerplate shrank because repeated flow moved to shared methods. Refactoring became safer because responsibilities were visible in one place instead of hidden across many top-level callbacks.
This was not an attempt to follow textbook MVC purity.
It emerged from repeated implementation pressure: every time I avoided structure, maintenance cost returned. Every time I introduced a controller boundary, revisions became calmer.
The Birth of CotomyPageController
As this pattern repeated across projects, a base controller class gradually emerged.
Shared initialization steps moved into that base. Project-level page controllers began inheriting from a common parent by default. The page layer stopped being an informal collection of scripts and started behaving as a predictable inheritance chain.
During one period, I also experimented with a switchable-controller mechanism for complex SPA-style flows. It produced some measurable benefits, but it also pushed architecture in a direction where a large component framework would be the more rational choice.
Because Cotomy aimed at a narrower boundary, I removed controller switching before publishing and kept the model centered on one page controller structure per screen.
Even in legacy systems, migration followed the same direction over time. Earlier wrappers and custom setup code were not replaced all at once, but many screens moved incrementally toward Cotomy-style page control as they were touched for feature work.
In current systems, almost every new or refactored screen is built either directly on Cotomy or on transitional controller classes that are designed to converge into it.
For CRUD-heavy applications, it is often useful to define specialized subclasses of CotomyPageController by screen category and keep each category’s default behavior there.
Framework-Like Emergence
At that point, I started noticing a larger pattern.
What began as local utilities was turning into a set of boundaries.
The page layer had structure. The form layer had classification. The DOM layer had locality.
That combination made the TypeScript library feel framework-like in day-to-day use.
It was never intended to compete with large ecosystems, and I still do not frame it that way. The goal was narrower: provide exactly the boundaries I needed for business UI delivery, no more and no less.
Ongoing Uncertainty
Inside my own environment, I consider this evolution a success.
At the same time, I am still uncertain about how far this structure should be generalized. What works across my projects may not map directly to every team context, and I do not want to pretend the direction is fully closed.
The architecture is stable enough to trust, but still open enough to evolve.
Transition Forward
So far in this series, I have mostly described which recurring problems Cotomy solved for me and why those solutions became durable.
From the next articles onward, I will focus more on granular technical decisions inside those boundaries, where tradeoffs become sharper and implementation details matter more.
Development Backstory
This article is part of the Cotomy Development Backstory, which traces how Cotomy’s architecture emerged through real project constraints.
Series articles: Building Systems Alone , Early Architecture Attempts , The First DOM Wrapper and Scoped CSS , API Standardization and the Birth of Form Architecture , Page Representation and the Birth of a Controller Structure, The CotomyElement Constructor Is the Core , Dynamic HTML Boundaries in CotomyElement , Reaching Closures to Remove Event Handlers Later , and The Birth of the Page Controller .
Previous article: API Standardization and the Birth of Form Architecture Next article: The CotomyElement Constructor Is the Core