DEV Community

Seif Ahmed
Seif Ahmed

Posted on

Building a Bulletproof Comment Reply System in Node.js & MongoDB 🚀

When building a nested reply system, most developers worry about deep tree complexity or messy data structures. For Vlox, I took a different approach: keeping things flat, fast, and secure by reusing a single Mongoose schema with smart atomic limits.

Here is a deep dive into how I engineered a production-ready, race-condition-safe reply mechanism using MongoDB transactions, strict type sanitization, and automated limits.

How It Works 🛠️

  • User Action: A user clicks the reply icon and submits their reply.
  • The Payload: Vlox's system sends 3 fields via the endpoint /api/v1/reply/comment/post/:id:
    • id: The post ID (passed as a URL parameter).
    • rootCommentId: The ID of the root comment being replied to.
    • reply: The raw text entered by the user.
  • Sanitization & Validation: The incoming reply is instantly converted to a trimmed string. It then passes through two critical validation checks:
    1. Existence Check: The reply must exist. (If a malicious actor sends a payload without a body, the string literally evaluates to "undefined" and gets blocked).
    2. Length Limit: The reply must be under 201 characters, enforcing the standard comment limit.
  • Atomic Transactions: If the validation checks pass, the system initiates a Mongoose transaction to execute the following steps safely:
    • Permission Check: It verifies if the user has permission to reply by checking the post's status via await schemas.Posts.findOne(hotQueries.find_user_post(id, req.session.userId));.
    • Creation: If permissions are valid, it creates a new reply. (Fun fact: It reuses the exact same schema as standard comments!)
  • The Reply Schema Structure: The reply object functions just like a normal comment, with two distinct exceptions:
    • It does not contain a repliesCount field.
    • It includes an extra rootId field, which explicitly points to the ID of the root comment being replied to.
  • Concurrency & Caps: To guarantee that a single comment never receives more than 10 replies while simultaneously incrementing the counter, the system runs this precise atomic query:
  {
      _id: rootCommentId,
      repliesCount: { $lt: 10 },
      rootId: null
  }
Enter fullscreen mode Exit fullscreen mode
  • Error Handling: To catch race conditions or instances where a user attempts to reply to a thread that is already full, the system evaluates the write operation with if (result.matchedCount === 0) throw new Error("COMMENT_UPDATE_FAILED");. Inside the catch block, it intercept this with if (txError.message === "COMMENT_UPDATE_FAILED") return res.status(400).json({ error: "You can't reply to this comment!" });.

The Production Code 💻

// In actions.js
router.post("/api/v1/reply/comment/post/:id", checkAuth, checkValidID, async (req, res) => {
    try {
        const id = req.params.id;
        let { reply, rootCommentId } = req.body;
        reply = String(reply).trim(); // You can change this to an explicit string check too
        if (!reply) return res.status(400).json({ error: "Reply can't be empty!" });
        if (reply.length > 200) return res.status(400).json({ error: "Reply cannot exceed 200 chars!" });
        const session = await mongoose.startSession();

        try {
            await session.withTransaction(async () => {
                // Find post
                const post = await schemas.Posts.findOne(hotQueries.find_user_post(id, req.session.userId));
                if (!post) throw new Error("POST_NOT_FOUND");

                // Add reply
                const newReply = new schemas.Comments({
                    content: reply,
                    rootId: rootCommentId,
                    for: id,
                    by: req.session.userId
                });

                await newReply.save({ session });

                // Inc comments
                const result = await schemas.Comments.updateOne({
                    _id: rootCommentId,
                    repliesCount: { $lt: 10 },
                    rootId: null
                }, {
                    $inc: {
                        repliesCount: 1
                    }
                }, { session });

                if (result.matchedCount === 0) throw new Error("COMMENT_UPDATE_FAILED");
            });
        } catch (txError) {
            if (txError.message === "POST_NOT_FOUND") return res.status(400).json({ error: "Post not found or you don't have permissions to see it!" });
            if (txError.message === "COMMENT_UPDATE_FAILED") return res.status(400).json({ error: "You can't reply to this comment!" });
            console.log("Error: " + txError.message);
            return res.status(400).json({ error: "Failed to reply. Try again." });
        } finally {
            session.endSession();
        }

        return res.status(200).json({ success: true });
    } catch (e) {
        console.log("Error: " + e.message);
        createErrorMessage(e, req.session.userId, req.originalUrl);
        return res.status(400).json({ error: "Server error" });
    }
});

// In server.js
app.post("/api/v1/get/post/replies/:id", checkAuth, checkValidID, async (req, res) => {
    try {
        const id = req.params.id;
        const { rootCommentId } = req.body;

        // Do you have permissions to access this post?
        const post = await schemas.Posts.find(hotQueries.find_user_post(id, req.session.userId));
        if (!post) return res.status(400).json({ error: "Post not found or you don't have permissions to see it!" });

        // Find replies (Note: You might want to add limit(10) here as a best practice, though it is optional)
        const replies = await schemas.Comments.find({
            for: id,
            rootId: rootCommentId
        })
            .populate("by", "-password -recoveryCodes -pinnedPosts -email -pinnedPostsCount"); // Removes secrets and non-essential data

        return res.status(200).json({ success: true, replies: replies });
    } catch (e) {
        console.error("Fetch Replies Break: ", e.message);
        return res.status(500).json({ error: "Could not retrieve replies." });
    }
});
Enter fullscreen mode Exit fullscreen mode

Links:

Found this guide helpful? Drop a like! 🌟

Top comments (0)