Why I Built the Project
I built a small counter app with vanilla JavaScript to practice three important concepts:
- Encapsulating state
- Simplifying DOM event handling
- Deploying a static app with Cloudflare Workers
Project Overview
Tech stack
- HTML
- CSS
- JavaScript
- Tailwind CSS via the CLI
- Cloudflare Workers
Constraints
- No frameworks. Plain HTML, CSS, and JavaScript only.
- Tailwind CSS via the CLI.
- The
countvariable needed to be private so it could only be accessed through the class methods. - The final app had to be deployed and publicly accessible.
The privacy requirement was especially useful because it pushed me to think about how JavaScript state can be protected from direct access.
1. Protecting State with a Closure
My first implementation used a constructor function. The count variable was declared inside the function, so it could not be accessed directly from outside.
function ProtectCount() {
let count = 0;
this.increaseCount = function () {
++count;
};
this.decreaseCount = function () {
--count;
};
this.resetCount = function () {
count = 0;
};
this.getCurrentCount = function () {
return count;
};
}
This achieved encapsulation. The only way to modify count is through the exposed methods.
However, I felt the implementation could be made more readable.
2. Refactoring to a Class with a Private Field
I refactored the constructor function into a class and replaced the closure-based variable with a private class field using the # syntax.
class Counter {
#count = 0;
increase() {
this.#count++;
}
decrease() {
this.#count--;
}
reset() {
this.#count = 0;
}
getCount() {
return this.#count;
}
}
This kept the encapsulation intact while making the code easier for other developers to scan and understand.
3. Replacing Multiple Event Listeners with Event Delegation
My first working version attached a separate click listener to each of the three buttons (increase, decrease, reset). It worked, but most of the code was nearly identical, and it would be difficult to add more buttons in the future.
Since all three buttons share the same parent, I replaced the three event listeners with a single one on the parent and used event.target to determine which button was actually clicked.
document.getElementById("btn").addEventListener("click", (e) => {
if (e.target.id === "minus") countObj.decrease();
else if (e.target.id === "add") countObj.increase();
else if (e.target.id === "setZero") countObj.reset();
else return;
updateUi();
});
This is event delegation: instead of attaching listeners to each button, I listen on their common ancestor.
For a small app, the performance difference is not important. The bigger benefit here was organization: related interactions were handled together, which made the code easier to extend.
4. Removing duplicate code for UI updates
Before consolidating the listeners, every handler repeated the same two lines to read the count and update the DOM.
const currentCount = countObj.getCount();
counterPlace.textContent = currentCount;
I pulled this into a single updateUi function to avoid code duplication.
const updateUi = function () {
const currentCount = countObj.getCount();
counterPlace.textContent = currentCount;
};
Debugging the Deployment: A Real Case Study
Deploying my app to Cloudflare Workers failed three or four times in a row with the same error.
β [ERROR] Asset too large. Cloudflare Workers supports assets with sizes of up to 25 MiB.
We found a file /opt/buildhome/repo/node_modules/workerd/bin/workerd with a size of 122 MiB.
Root cause: My Wrangler config pointed the assets directory at the project root.
"assets": { "directory": "." }
Cloudflare was treating my entire repository, including node_modules, as deployable static assets β that's why I got the error.
Fix: I pointed the assets directory at my actual build output folder instead of the repo root.
"assets": { "directory": "./dist" },
"compatibility_date": "2026-08-04"
After this, Cloudflare treated the dist folder as the assets directory, and the error was resolved.
Result
The final app is live at counter.yashmore2002bs.workers.dev.
Although the final application is small, the project helped me practice several patterns that I expect to reuse:
- Use private class fields when a class needs to protect its internal state.
- Use event delegation when related controls share a common parent.
- Keep UI update logic in one reusable function.
- Verify what a deployment configuration treats as an asset before shipping.
Final Thoughts
This project reminded me that even a small application can be a useful learning opportunity. The counter itself was easy to build, but improving its structure and deploying it successfully required me to think more carefully about state, events, rendering, and configuration.
I am planning to continue building small projects like this while moving from vanilla JavaScript toward larger full-stack applications.
Thanks for reading!
If you have suggestions for improving this project or the code, feel free to share them in the comments.
Top comments (0)