I didn’t expect this to take almost three days, but yeah, everything was from scratch. I was only building the reactions feature—the one following you at the bottom 😁
I didn’t do any prior analysis—I jumped straight to the database, lots of trial & error, and refactoring. Maybe a bit lazy, but I just wanted to code 😂
In this article I’ll outline the features and the concept of how I built it—but it’s not a step-by-step tutorial.
If you want to build something similar, feel free to borrow some ideas—just take the useful parts.
Let’s go…
Features
First I thought about the rough idea of how this reactions feature should work.
Here’s what I came up with:
-
Multiple Reactions
There are claps, wow, etc., and it should be easy to add new reactions in the future.
-
Batch Reactions
Users can send multiple reactions of the same type, e.g., 20x claps, 10x wow, etc.
-
Section Reactions
Know where a user reacted, e.g., 10x claps in the section titled ‘Features’ like this one.
Concept
What took the most time was building the REST API, lots of refactoring, designing the database schema, and other backend things.
There are probably parts that are imperfect—bear with me, I’m not primarily a backend person ✌
Database
Honestly, it had been about three years since I last dealt with databases. The last time was for my final college project.
First, I decided where to host it. SQL vs NoSQL didn’t matter—I was experimenting—so I looked for a free tier and found a shared plan on MongoDB Atlas.
Since I used Prisma as the ORM, migrating to another database shouldn’t be too hard.
Let’s talk about the database schema. This focuses only on the reactions feature—there are also fields for views and shares, but I’m excluding those here.
First, I created a ContentMeta table to store post data.
No need to store title, date, content, etc., because the post content is built locally from MDX—so I only store the slug.
model ContentMeta {
id String
slug String
createdAt DateTime
}
Then the important one, the Reaction table—to store the reactions.
model Reaction {
id String
type ReactionType
section String
count Int
sessionId String
createdAt DateTime
contentId String
}
enum ReactionType {
CLAPPING
THINKING
AMAZED
}
Let's break it down:
-
typeCritical because we support multiple reaction types. I used a predefined enum
ReactionType, so it can only beCLAPPING,THINKING, orAMAZED.To add new reactions, add a new enum item. Alternatively, switch to
Stringto allow arbitrary emojis.Why not create a relation to a new table?
You can, but I figured queries would be more complicated. Enums felt sufficient and provide type safety ✌
-
sectionStores the active section title when a user clicks a reaction. If the user is reading ‘Database’, this becomes ‘database’.
This relies on IntersectionObserver—observe all section headings and use the last one that is 'isIntersecting'.
-
countThis is for batching. Clicking a reaction doesn’t immediately hit the API—it waits a few milliseconds (debounce).
Simply put: clicks within a short window are accumulated (e.g., 10 clicks), then sent as one request—and the number goes into
count.This makes more sense than sending 10 requests and creating 10 identical rows. It also saves database storage.
-
sessionIdUsed to identify “who” reacted. Not the user’s name/identity—I use the user’s IP address.
But I don’t store raw IP addresses since they’re sensitive.
I hash the IP address and add a salt for privacy. So no one (including me) knows the raw IP.
Roughly like this:
export const getSessionId = (req: NextApiRequest) => { const ipAddress = req.headers['x-forwarded-for'] || 'localhost'; // hashes the user's IP address and combines it with // a salt to create a unique session ID that preserves // the user's privacy by obscuring their IP address. const currentSessionId = createHash('md5') .update(ipAddress + process.env.SALT_IP_ADDRESS, 'utf-8') .digest('hex'); return currentSessionId; };Or inspect it directly here.
Why do we need a sessionId?
It’s used to limit the number of reactions. I group by
typewheresessionId, then compute thecount.If the result hits the max limit, the user can’t react anymore.
REST API
I only needed one endpoint: /reactions, accepting POST with slug, type, section, and count.
As explained earlier, the data is prepared on the frontend—except sessionId, which is generated server-side during the API request.
I added one more: GET /content. This returns current reaction counts for a slug, used by the component.
From the two tables, the /content response looks like this:
{
"meta": {
"reactionsDetail": {
"CLAPPING": 65,
"THINKING": 45,
"AMAZED": 48
}
},
"metaUser": {
"reactionsDetail": {
"CLAPPING": 15,
"THINKING": 15,
"AMAZED": 15
}
},
"metaSection": {
"fitur": {
"reactionsDetail": {
"CLAPPING": 0,
"THINKING": 4,
"AMAZED": 0
}
},
"database": {
"reactionsDetail": {
"CLAPPING": 0,
"THINKING": 0,
"AMAZED": 6
}
},
}
}
meta is the total reactions from all users, metaUser is the current user’s reactions, and metaSection is reactions for a specific section.
You might notice—why isn’t metaSection an array?
Honestly… I don’t know 😂
But since the frontend component logic reads one section at a time (no loops), I prefer metaSection[section] over filtering an array 👀
Component
This is the part that I enjoy the most.
First, to fetch data from the API, I used SWR. From the SWR site:
With SWR, components will get a stream of data updates constantly and automatically. And the UI will be always fast and reactive.
SWR is great—especially for mutations.
Now let’s build the reactions component 🥳
Component details:
- Display reactions for
claps,wow, andhmm. - Show emoji animation on hover.
- Trigger animation on batched or repeated clicks.
- Animate the reactions counter.
- Change the emoji once the limit is reached.
For emoji animations (point 2), I used assets from here.
For animations, I still use Framer Motion 😍
When a user clicks a reaction, an animation is shown. On onClick, I push a random x and y value which are rendered by a Framer Motion component.
With random x and y, repeated clicks don’t produce monotonous animations in the same direction.
Confused? Sorry—check the full code.
Final Result
Here’s the result—try giving reactions up to the limit and see the animations 😁
It still needs refinements (styles, images, layout, performance), but I’m happy with the current result.
Summary
Even though building this was challenging—especially the backend—I enjoyed the process. It still took almost three days 😁
Tech stack / libraries used:
- MongoDB Atlas for database hosting.
- Prisma as the ORM.
- SWR for data fetching.
- Framer Motion for animations.
- Animated Fluent Emoji
WAITTT, what about section reactions? The API already provides
metaSection, what’s it for?
Since the data is there, we’re good—even if implementation isn’t done yet. I have plans and a clear idea of how to use it. Stay tuned 😁
I’ll likely share updates on Twitter, not a separate post.
THANK YOU for reading to the end. See you next week in the next post 👋