This content originally appeared on DEV Community and was authored by Ali nazari
mongoose-reactions is a lightweight, TypeScript-first Mongoose plugin that adds Laravel-like polymorphic reactions (likes, loves, laughs, custom strings) to any model.
This post shows why you’ll want it in your stack, how it works, quickstart examples, scaling tips, and how you can help the project grow (⭐ GitHub!).
What this solves and why you should care
If you build social features (posts, comments, photos, products with likes), you need a fast, scalable, and developer-friendly way to attach reactions to any model.
mongoose-reactions:
- Stores reactions in a single, indexed collection for efficient counts and queries.
- Works polymorphically (one reactions collection — many models).
- Is TypeScript-ready and tested for common concurrency edge cases.
- Provides easy statics & instance helpers: react, unreact, toggleReaction, getReactionCounts, getUserReactions.
- Plays nicely with transactions and bulk aggregation queries.
If you want to ship social features fast, avoid custom ad-hoc arrays in your documents, and keep your code clean and maintainable — this plugin is for you.
Why not just store reactions on the parent document?
Many early-stage apps put likes
as an array on Post documents. That looks simple but quickly becomes painful:
- Large arrays grow MongoDB documents to awkward sizes and harm performance.
- Counting and listing across many posts (or across models) becomes expensive.
- Deleting a user’s reactions (GDPR) requires scanning many documents.
- Cross-model analytics (top-reacted posts vs comments) is hard.
By moving reactions into a small, indexed, dedicated collection you get:
- O(1) writes (append-only docs), efficient aggregation for counts, and simpler migrations.
- Single source of truth for reactions and easier features (undo, toggle, per-user lists).
- Ability to add adapters later (Redis for counters, Prisma adapter, etc.).
Features at a glance
- Polymorphic: store reactions for Post, Comment, Photo, etc., in one collection.
- Configurable: single-reaction-per-user (default) or multi-reaction-per-user mode.
- Validation: whitelist reaction types (e.g., ['like','love','haha']) with case-insensitive normalization.
- Transaction-friendly: write operations can accept a session.
- Bulk helpers: get counts for many reactables in one aggregation.
- GDPR helpers: utilities to delete all reactions for a user or a reactable.
- TypeScript types + Vitest testing scaffolding.
Quickstart — install & use
Install:
npm install mongoose mongoose-reactions
# or
pnpm add mongoose mongoose-reactions
Basic usage:
import mongoose from "mongoose";
import reactionsPlugin from "mongoose-reactions";
const PostSchema = new mongoose.Schema({ title: String });
PostSchema.plugin(reactionsPlugin, {
reactionTypes: ['like','love','haha','angry'], // optional whitelist
allowMultipleReactionsPerUser: false, // default
});
const Post = mongoose.model('Post', PostSchema);
// react
await Post.react(postId, userId, 'like'); // static call
await postDoc.react(userId, 'love'); // instance call
// counts (no need to pass model name; plugin infers it)
const counts = await Post.getReactionCounts(postId); // -> { like: 12, love: 3 }
Express route sample:
import express from 'express';
const router = express.Router();
router.post('/posts/:id/reactions', async (req, res, next) => {
try {
const userId = req.user._id; // assume auth middleware
const postId = req.params.id;
await Post.react(postId, userId, req.body.reaction);
const counts = await Post.getReactionCounts(postId);
res.json({ ok: true, counts });
} catch (err) { next(err) }
});
API (developer-friendly)
Core methods exposed on any model you apply the plugin to:
-
Model.react(reactableId, userId, reaction, meta?, { session? })
- create/update reaction -
Model.unreact(reactableId, userId, reaction?, { session? })
- remove reaction(s) -
Model.toggleReaction(reactableId, userId, reaction, meta?, { session? })
- toggle behavior -
Model.getReactionCounts(reactableId)
-{ like: 10, love: 2 }
(infers model) -
Model.getUserReactions(reactableId, userId)
-ReactionDoc[]
(useful when multiple allowed) -
Model.listReactors(reactableId , opts)
- list users who reacted with optional reaction filter, paginated
Instance helpers map to the statics, so you can use postDoc.react(userId, 'like')
.
Polymorphism & model names — the small but crucial detail
The plugin stores a reactableModel
(we recommend the Mongoose modelName
like 'Post') alongside reactableId
.
This avoids collisions when two models share the same id value and makes populate()
and permissions checks straightforward.
We infer this.modelName
inside statics — so you don’t need to pass the model name every time. Post.getReactionCounts(id)
is enough.
If you like the plugin, please consider:
⭐ Star the GitHub repo — stars help the project show up in searches and attract contributors.
This content originally appeared on DEV Community and was authored by Ali nazari

Ali nazari | Sciencx (2025-08-24T20:15:03+00:00) Build social features fast with mongoose-reactions — a tiny, production-ready Mongoose plugin for reactions. Retrieved from https://www.scien.cx/2025/08/24/build-social-features-fast-with-mongoose-reactions-a-tiny-production-ready-mongoose-plugin-for-reactions/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.