The Feature That Wasn't Simple
"It's just a clap counter. What could go wrong?"
Famous last words.
The request seemed harmless enough: add claps to bloqs. A little hands-clapping icon, a number that increments, maybe a nice scramble-text animation. I'd built similar things before. An hour, tops. Maybe two if I wanted to get fancy with the hover states.
Four hours later, I was staring at:
- A database migration
- Three new RPC functions
- A unified API route with type validation
- A custom hook with optimistic updates
- A shared component that works across two different content types
So much for "just a clap counter." The "simple" feature had touched every layer of the stack. I'd gone from "add an icon" to "redesign the data architecture."
And here's the embarrassing part: the code was the easy bit. The hard part was the 30 minutes I spent writing a spec before I wrote a single line of code.
The Wrong Way (Which I Almost Did)
My fingers were already on the keyboard. My brain had already decided:
This is how I used to work. It's also how I used to discover, halfway through implementation, that I'd made fundamental architectural decisions without realizing it. Decisions like:
- "Oh, this is for bloqs only? What about blips?"
- "Wait, how do I identify users without authentication?"
- "Should they be able to clap once or fifty times?"
- "What happens when someone writes a script and claps 10,000 times?"
Each of those questions was a landmine I was about to step on.
I almost started coding. Then I remembered the View Counter Incident of Last Month—a refactor that took three times longer than expected because I kept discovering requirements mid-build. I'd written the same API route three times. I'd rather not talk about it.
So I paused. Closed my editor. Opened a blank Markdown file.
And I started asking questions instead of answering them with code.
The Spec Document (AKA The Thing I Didn't Want To Write)
The spec wasn't formal. It wasn't even pretty. It looked like this:
That's it. Maybe 30 minutes of typing. But those 30 minutes contained decisions that would have taken me four hours of coding-and-backtracking to arrive at organically.
The spec was ugly. The spec was informal. But the spec had answers.
The Question That Ate My Afternoon (In A Good Way)
Here's the thing about specs: they surface questions you didn't know you were avoiding.
I wrote: "What content types need claps?"
My first answer: "Bloqs."
Then I paused. What about blips? The short-form posts? Should they get claps too?
Let's just do bloqs for now. Keep it simple. Wait. What happens when I want to add claps to blips next week? Do I create a whole separate table? A separate API? A separate hook?
This was the fork in the road:
Option A: Separate tables. bloq_claps here, blip_claps later. Clean separation. Simple queries. Two of everything.
Option B: Unified table. A single claps table with a post_type column. Slightly more complex queries. One of everything.
Option A felt simpler. Then I imagined explaining it to myself in six months: "Why do we have four different clap tables?" "Because Past Me didn't think ahead."
I chose Option B. Not because it was simpler now—honestly, it was more upfront work—but because it would be simpler forever. Adding a third content type would mean changing one validation function. Not creating three new files.
The spec made me have that argument with myself before I'd committed to any code.
The Architecture That Emerged
With the spec as a map, the implementation actually went smoothly:
Each layer has one job. The component renders and handles clicks. The hook manages state and lies optimistically (then apologizes if the server says no). The API validates. The database persists.
I didn't design this architecture. The questions did.
The Migration I Didn't Have To Debug
Plot twist: I'd already built claps for bloqs. With a bloq_claps table. Before I realized blips existed.
So now I had to migrate. And because I'd spec'd it out, the migration was... fine?
No guessing. No "wait, did I migrate the timestamps?" No 2 AM debugging session because I forgot a column. The spec had already told me what the target state looked like.
What I Actually Learned
-
Questions are specifications in disguise. Every "how do we handle X?" that I answered in the spec was a decision I didn't have to make while deep in implementation mode, when my brain was full of variable names and edge cases.
-
Design for the third case. When you're building for two of something, ask what happens when there's a third. The answer tells you whether to unify or separate. I learned this from a cauliflower at a vegetable shop, but that's a different article.
-
Specs age well. The spec I wrote before coding became the outline for this article. It's also the mental model I'll have when I touch this code in six months and forget how any of it works.
-
30 minutes of writing saves hours of refactoring. I know this now. Will I remember it next time?
Probably not.I'll try.
The Template (If You Want To Try This At Home)
I'm not sure I'll follow this perfectly every time. Sometimes the code just needs to flow. But having a template means I start with questions, not assumptions.
And between you and me? The questions are usually where the interesting stuff hides.
The Code (If You Want To Borrow It)
The component is a single button that takes a postId, postType, and optional interactive flag. It calls a hook that handles fingerprinting, fetching, and optimistic updates. The API is one route that validates the post type and calls a Supabase RPC function.
That's the whole architecture. The actual implementation is about 150 lines across three files—not worth pasting here. If you want to see it, the source is on GitHub or you can inspect it in your browser's dev tools.
Usage
Known Quirk: Multiple Counters Don't Sync
If you put two ClapsCounter components on the same page with the same postId—like I did on this article, where there's one in the sticky header and one inline as a demo—they won't update each other in real-time. Each component has its own state. Clap on one, the other stays stale until you refresh.
To be fair, this article might be the only place where that happens. The sticky header + inline demo is a bit of an edge case. On a normal bloq page, there's just one counter. On list pages, each card has its own post. Duplicating the same counter twice on one page is a self-inflicted problem that I created specifically for this article.
The fix would be a shared context or a global state store (Zustand, Jotai, React Query). I thought about it. Then I thought about:
- Adding another dependency
- Wrapping the article layout in a provider
- Managing cache invalidation across components
- Testing the whole thing
And I decided: no.
This is a claps feature. The stakes are approximately zero. If someone claps on the demo and doesn't see the header counter update instantly, the world will continue to spin. Refresh the page if you need that validation.
Is this lazy? Yes. Is this also a reasonable trade-off for a feature that exists purely for whimsy? Also yes. Not every quirk needs an architectural solution. Some quirks just need to be documented and left alone.
The spec for this article was written after the article. I'm not sure what that means.