DEV Community

Tsabary
Tsabary

Posted on • Updated on

Effortlessly Integrate Engaging Comment Sections in React with Replyke: Part 2

Welcome to Part 2 of our series on integrating the Replyke comment system into your React application. In Part 1, we focused on how to implement Replyke in your front-end environment. Now, we'll delve into creating the necessary backend API routes, enabling Replyke to communicate seamlessly with your own API.

Key Points to Understand Before We Begin:

  1. Privacy and Independence: Replyke is designed to respect user privacy and your application's independence. It does not communicate with any external services apart from your own API, and it only makes essential calls required for its functionality. We assure you that Replyke neither collects nor sends any data to services outside of your API. This principle is central to the integrity and reliability of Replyke.

  2. Technology Agnostic Backend: While this tutorial uses Node.js, Express.js, Mongoose, and TypeScript to demonstrate the setup, the core of Replyke's integration lies in the proper implementation of API routes and logic. You are free to use any technology stack for your backend, as long as you create the necessary routes and adhere to the logic outlined in this guide. By following our examples, you'll understand the expected routes, parameters, and responses, enabling you to adapt this knowledge to your preferred backend framework.

Setting Up Your API Routes:

Replyke's interaction with your API is straightforward. For instance, if you've set apiBaseUrl as https://www.my-base-api.com, then to fetch an article, Replyke would make a GET request to https://www.my-base-api.com/replyke-articles/the_article_id. It's crucial to implement these routes precisely as demonstrated to ensure seamless communication between your front-end and back-end.

For those who prefer a hands-on approach, feel free to explore the routes in our GitHub repository. The routes are simple and intuitive. If you're using the same technologies, you can even copy and paste the code directly.
In this article we will cover the same routes, but in greater detail.

Building Our Mongoose Models:

We'll start by creating two Mongoose models: Article and Comment. You can rename them if necessary, but remember, the exact implementation of the routes is what matters most.

models/article.ts

mongoose.Promise = global.Promise;
export const articleSchema = new mongoose.Schema({
  article_id: {
    type: String,
    trim: true,
    required: true,
  },
  likes: {
    type: [String],
    default: [],
    required: true,
  },
  likes_count: {
    type: Number,
    default: 0,
    required: true,
  },
  comments_count: {
    type: Number,
    default: 0,
    required: true,
  },
  replies_count: {
    type: Number,
    default: 0,
    required: true,
  },
});

export const Article =
  mongoose.models.Article ?? mongoose.model("Article", articleSchema);
Enter fullscreen mode Exit fullscreen mode

models/comment.ts

mongoose.Promise = global.Promise;
export const commentSchema = new mongoose.Schema({
  article_id: {
    type: String,
    trim: true,
    required: true,
  },
  body: {
    type: String,
    trim: true,
    required: true,
  },
  parent: {
    type: String,
    trim: true,
    required: false,
  },
  likes: {
    type: [String],
    required: true,
  },
  likes_count: {
    type: Number,
    default: 0,
    required: true,
  },
  replies_count: {
    type: Number,
    default: 0,
    required: true,
  },
  created_at: {
    type: String,
    trim: true,
    required: true,
  },
  author: {
    _id: {
      type: String,
      trim: true,
      required: true,
    },
    name: {
      type: String,
      trim: true,
    },
    img: {
      type: String,
      trim: true,
    },
  },
});

export const Comment =
  mongoose.models.Comment ?? mongoose.model("Comment", commentSchema);
Enter fullscreen mode Exit fullscreen mode

Defining TypeScript Interfaces:

Next, we'll define two interfaces corresponding to our models.

interfaces/IArticle.ts

export default interface IArticle {
  _id: string;
  article_id: string;
  likes: string[];
  likes_count: number;
  comments_count: number;
  replies_count: number;
}
Enter fullscreen mode Exit fullscreen mode

interfaces/IComment.ts

export default interface IComment {
  _id: string;
  article_id: string;
  body: string;
  parent?: string;
  likes: string[];
  likes_count: number;
  replies_count: number;
  created_at: string;
  author: {
    _id: string;
    name: string;
    img?: string;
  };
}
Enter fullscreen mode Exit fullscreen mode

Route for Fetching an Article

Endpoint: /replyke-articles/:article_id
Method: GET

This route retrieves a single article using its ID, passed as a parameter in the URL. It's a straightforward GET request to our Express router.

Extracting the ID: We get the article_id from req.params to identify which article to fetch.

Database Query: Using Mongoose, we search for the article with Article.findOne({ article_id }). The .lean() method ensures faster performance by returning a plain JavaScript object.

Handling Responses:

  • If the article is found, we return it with a 200 (OK) status.

  • If no article is found, we send a 404 (Not Found) status to indicate the absence of the requested resource.

  • For server errors, we return a 500 (Internal Server Error) with an error message.

This route is a key part of ensuring Replyke can retrieve specific article data, crucial for displaying the corresponding comments.

// Route to fetch a single article by its ID.
router.get("/replyke-articles/:article_id", async (req: ExReq, res: ExRes) => {
  try {
    // Extract article_id from the path parameters.
    const { article_id } = req.params;

    // Search for the article using Mongoose's findOne method.
    const article: IArticle | null = await Article.findOne({
      article_id,
    }).lean();

    // If no article is found, return a 404 (Not Found) status.
    if (!article) return res.status(404).send();

    // If an article is found, return it with a 200 (OK) status.
    return res.status(200).send(article);
  } catch (err: any) {
    // In case of any server errors, return a 500 (Internal Server Error) status.
    return res.status(500).send({ error: "Server error" });
  }
});
Enter fullscreen mode Exit fullscreen mode

Route for Liking an Article

Endpoint: /replyke-articles/like
Method: POST

This route allows users to like an article. It receives the article_id and user_id in the request body.

Validating Inputs: The route first checks if both article_id and user_id are present. If not, it returns a 400 (Bad Request) status, indicating missing data.
Fetching the Article: It attempts to find the article using Article.findOne({ article_id }). If the article isn't found, it creates a new one.

Like Logic:

  • Prevent Duplicate Likes: Checks if the user has already liked the article. If so, it returns a 409 (Conflict) status to avoid duplicate likes.
  • Updating Likes: If the user hasn't liked the article yet, it increments the likes_count and adds the user_id to the likes array.
  • Creating New Articles: If the article doesn't exist, it creates a new one with the initial like.

Response: Sends back the updated or newly created article with a 200 (OK) status.

Error Handling: In case of server errors, it returns a 500 (Internal Server Error).

This route is vital for enabling user interaction with articles, specifically for liking them, enhancing user engagement within Replyke.

// Route to like an article.
router.post("/replyke-articles/like", async (req: ExReq, res: ExRes) => {
  try {
    const { article_id, user_id } = req.body;

    // Validate the presence of article_id and user_id.
    if (!article_id || !user_id) {
      return res
        .status(400)
        .send("Missing article_id or user_id in request body");
    }

    // First we want to fetch the article
    const article: IArticle | null = await Article.findOne({
      article_id,
    }).lean();

    let newArticle;

    if (article) {
      // Prevent duplicate likes.
      if (article.likes.includes(user_id)) {
        return res.status(409).send("User already liked article");
      }

      // Update the article with the new like.
      newArticle = await Article.findOneAndUpdate(
        { article_id },
        {
          $inc: { likes_count: 1 },
          $push: { likes: user_id },
        },
        { new: true }
      );
    } else {
      // Create a new article if it doesn't exist.
      newArticle = new Article({
        article_id,
        likes: [user_id],
        likes_count: 1,
        comments_count: 0,
        replies_count: 0,
      });
      await newArticle.save();
    }

    // Return the updated or newly created article.
    return res.status(200).send(newArticle);
  } catch (err: any) {
    // Handle server errors.
    return res.status(500).send({ error: "Server error" });
  }
});
Enter fullscreen mode Exit fullscreen mode

Route for Unliking an Article

Endpoint: /replyke-articles/unlike
Method: POST

This route facilitates the removal of a user's like from an article. It requires article_id and user_id in the request body.

Input Validation: Checks for the presence of both article_id and user_id. If either is missing, it returns a 400 (Bad Request) status.

Article Verification:

  • Attempts to find the article and verifies if the user has previously liked it.

  • If the article doesn't exist or the user hasn't liked it, it sends a 409 (Conflict) status, indicating an inability to process the unlike request.

Unlike Logic: Updates the article by decrementing the likes_count and removing the user_id from the likes array.

Response: Sends back the updated article with a 200 (OK) status.

Error Handling: In case of server errors, it responds with a 500 (Internal Server Error).

This route ensures users can retract their likes, an important aspect of interactive user engagement in the Replyke system.

// Route to unlike an article.
router.post("/replyke-articles/unlike", async (req: ExReq, res: ExRes) => {
  try {
    const { article_id, user_id } = req.body;

    // Validate the presence of article_id and user_id.
    if (!article_id || !user_id) {
      return res
        .status(400)
        .send("Missing article_id or user_id in request body");
    }

    // Fetch the article to check if the user has already liked it.
    const article: IArticle | null = await Article.findOne({
      article_id,
    }).lean();

    // If the article does not exist or the user hasn't liked it.
    if (!article || !article.likes.includes(user_id)) {
      return res
        .status(409)
        .send("Can't unlike, as user didn't like article or article not found");
    }

    // Update the article, reducing the like count and removing the user's ID from likes.
    const updatedArticle = await Article.findOneAndUpdate(
      { article_id },
      {
        $inc: { likes_count: -1 },
        $pull: { likes: user_id },
      },
      { new: true }
    );

    // Return the updated article.
    return res.status(200).send(updatedArticle);
  } catch (err: any) {
    // Handle server errors.
    return res.status(500).send({ error: "Server error" });
  }
});
Enter fullscreen mode Exit fullscreen mode

Route for Retrieving Comments with Pagination and Sorting

Endpoint: /replyke-comments
Method: GET

This route fetches comments for an article with options for pagination and sorting, enhancing user experience by managing the display of comments efficiently.

Query Parameters: Receives article_id, sort_by (e.g., 'popular', 'newest', 'oldest'), parent (for thread-level comments), page, and limit as query parameters.

Parameter Validation:

  • Ensures that limit and page are valid numbers.

  • Checks that page is a whole number and greater than 0.

Sorting Logic: Determines the sorting order (sort) based on sort_by. For instance, sorting by 'popular' orders comments by likes_count and created_at.

Pagination Mechanics:

  • Calculates the skipCount to determine how many comments to skip, based on the current page and limit.

  • Uses Mongoose's .limit() and .skip() to paginate the results.

Fetching Comments: Retrieves comments from the database using the Comment.find() method with the provided filters and sorting.

Response: Returns the fetched comments with a 200 (OK) status.

Error Handling: In case of server errors, responds with a 500 (Internal Server Error).

This route is crucial for efficiently displaying comments, especially in scenarios where there are a large number of comments, ensuring a smooth user experience.

// Route to retrieve comments with pagination and sorting options.
router.get("/replyke-comments", async (req: ExReq, res: ExRes) => {
  try {
    const { article_id, sort_by, parent, page = 1, limit = 5 } = req.query;

    // Convert 'limit' and 'page' to numbers and validate them.
    const limitAsNumber = Number(limit);
    if (isNaN(limitAsNumber)) {
      throw new Error("Invalid request: limit must be a number");
    }

    const pageAsNumber = Number(page);
    if (isNaN(pageAsNumber)) {
      throw new Error("Invalid request: page must be a number");
    }

    // Ensure 'page' is a whole number and greater than 0.
    if (pageAsNumber < 1 || pageAsNumber % 1 !== 0) {
      throw new Error(
        "Invalid request: 'page' must be a whole number greater than 0"
      );
    }

    // Define the sort filter based on 'sort_by' query parameter.
    let sort: { [key: string]: SortOrder } = {};

    if (sort_by === "popular") {
      sort = { likes_count: -1, created_at: -1 };
    }
    if (sort_by === "newest") {
      sort = { created_at: 1 };
    }
    if (sort_by === "oldest") {
      sort = { created_at: -1 };
    }

    // Calculate the number of results to skip for pagination.
    const skipCount = (pageAsNumber - 1) * limitAsNumber;

    // Fetch comments based on filters, pagination, and sorting.
    const comments = await Comment.find({
      article_id,
      parent,
    })
      .limit(limitAsNumber)
      .skip(skipCount)
      .sort(sort);

    // Respond with the fetched comments.
    return res.status(200).send(comments);
  } catch (err: any) {
    // Handle any server errors.
    return res.status(500).send({ error: "Server error" });
  }
});
Enter fullscreen mode Exit fullscreen mode

Route for Posting a New Comment or Reply

Endpoint: /replyke-comments
Method: POST

This route enables users to post a new comment or reply to an article, a key feature for user interaction within the Replyke system.

Required Fields: The request body must contain article_id, comment_body, and author. The route validates these fields and returns a 400 (Bad Request) status if any are missing.

Creating the Comment:

  • A new Comment instance is created with details like article_id, body, parent (for replies), and author.

  • The comment is then saved to the database.

Updating Parent Comment: If the comment is a reply (parent is provided), the parent comment's replies_count is incremented.

Updating Article's Counts:

  • The route checks for the existence of the related article.

  • Updates the article's comments_count or replies_count depending on whether the comment is a direct comment or a reply.

  • If the article doesn't exist, it creates a new article record.

Response: The newly created comment or reply is sent back with a 200 (OK) status.

Error Handling: Handles server errors by returning a 500 (Internal Server Error).

This route is crucial for facilitating the core functionality of comment and reply posting, enhancing user engagement and interaction on the platform.

// Route for posting a new comment or reply to an article.
router.post("/replyke-comments", async (req: ExReq, res: ExRes) => {
  try {
    const { article_id, comment_body, parent, author } = req.body;

    // Validate the presence of required fields.
    if (!article_id || !comment_body || !author) {
      return res.status(400).send("Missing required comment details");
    }

    const comment = new Comment({
      article_id,
      body: comment_body,
      parent,
      likes: [],
      likes_count: 0,
      replies_count: 0,
      created_at: new Date(),
      author,
    });
    await comment.save();

    // Update the parent comment's reply count if this is a reply.
    if (parent) {
      await Comment.findByIdAndUpdate(parent, {
        $inc: { replies_count: 1 },
      });
    }

    // Fetch the article to update its comment or reply count.
    const article: IArticle | null = await Article.findOne({
      article_id,
    }).lean();

    if (article) {
      // Increment either the direct comments count or the replies count in the article.
      const updateField = parent
        ? { $inc: { replies_count: 1 } }
        : { $inc: { comments_count: 1 } };
      await Article.findOneAndUpdate({ article_id }, updateField);
    } else {
      // Create a new article record if it doesn't exist.
      const newArticle = new Article({
        article_id,
        likes: [],
        likes_count: 0,
        comments_count: 1,
        replies_count: 0,
      });
      await newArticle.save();
    }

    // Return the newly created comment or reply.
    return res.status(200).send(comment);
  } catch (err: any) {
    // Handle any server errors.
    return res.status(500).send({ error: "Server error" });
  }
});
Enter fullscreen mode Exit fullscreen mode

Route for Deleting a Comment and Its Replies

Endpoint: /replyke-comments
Method: DELETE

This route provides the functionality to delete a comment and all its associated replies, an important aspect for content moderation and user management within Replyke.

Input Validation: Requires comment_id in the request body. If it's not provided, the route returns a 400 (Bad Request) status.

Finding the Target Comment:

  • Attempts to retrieve the comment to be deleted using its ID.

  • If the comment is not found, it returns a 404 (Not Found) status.

Deletion Process:

  • Implements a recursive function to delete the comment and its child comments (replies).

  • Increments counters for deleted top-level comments and replies to keep track.

Updating Parent Comment: If the deleted comment was a reply, it decreases the replies_count of the parent comment.

Updating Article's Counts:

  • Adjusts the comments_count and replies_count of the corresponding article.

  • Throws an error if the related article is not found during the update.

Response: Returns the updated article data with a 200 (OK) status.

Error Handling: Captures and responds to server errors with a 500 (Internal Server Error).

This route is essential for maintaining the integrity and relevance of the comment section, allowing for efficient removal of comments and their replies.

// Route for deleting a comment and its replies.
router.delete("/replyke-comments", async (req: ExReq, res: ExRes) => {
  try {
    const { comment_id } = req.body;

    // Ensure comment_id is provided.
    if (!comment_id) {
      return res.status(400).send("Comment ID is required");
    }

    // Retrieve the target comment to initiate the delete operation.
    const targetComment: IComment | null = await Comment.findById(
      comment_id
    ).lean();
    if (!targetComment) {
      return res.status(404).send("Comment not found");
    }

    // Initialize counters for the number of deleted comments and replies.
    let firstLevelCommentsDeletedCount = 0;
    let repliesDeletedCount = 0;

    // Recursive function to delete a comment and its child comments (replies).
    async function deleteCommentAndChildren(commentId: string) {
      // We first delete the comment. If all good, we should get the comment document back.
      const comment: IComment | null = await Comment.findByIdAndDelete(
        commentId
      ).lean();

      if (!comment) {
        throw new Error("Comment deletion failed");
      }

      // Increment appropriate counters based on whether the comment is a reply.
      comment.parent ? repliesDeletedCount++ : firstLevelCommentsDeletedCount++;

      // Delete all child comments (replies) of the current comment.
      const childComments: IComment[] = await Comment.find({
        parent: comment._id,
      }).lean();
      for (const childComment of childComments) {
        await deleteCommentAndChildren(childComment._id);
      }
    }

    // Start the deletion process for the target comment and its replies.
    await deleteCommentAndChildren(comment_id);

    // Update the parent comment's reply count if the target comment was a reply.
    if (targetComment.parent) {
      await Comment.findByIdAndUpdate(targetComment.parent, {
        $inc: { replies_count: -1 },
      });
    }

    // Update the corresponding article's comment and reply counts.
    const article = await Article.findOneAndUpdate(
      { article_id: targetComment.article_id },
      {
        $inc: {
          comments_count: -firstLevelCommentsDeletedCount,
          replies_count: -repliesDeletedCount,
        },
      },
      { new: true }
    );

    if (!article) {
      throw new Error("Article not found for updating comment counts");
    }

    // Return the updated article data.
    return res.status(200).send(article);
  } catch (err: any) {
    // Handle any server errors.
    return res.status(500).send({ error: "Server error" });
  }
});
Enter fullscreen mode Exit fullscreen mode

Route for Updating a Comment's Content

Endpoint: /replyke-comments
Method: PATCH

This route allows users to update the text of an existing comment, providing flexibility in comment management.

Required Data: Checks for the presence of update (new comment text) and comment_id in the request body. If either is missing, it returns a 400 (Bad Request) status.

Updating the Comment:

  • The route attempts to update the specified comment with the new content using Comment.findOneAndUpdate.

  • It searches for the comment by its _id and updates the body field.

Response Handling:

  • If the comment is successfully updated, it returns the modified comment with a 200 (OK) status.

  • If no comment is found or the update fails, it sends a 404 (Not Found) status.

Error Management: In case of server errors, the route responds with a 500 (Internal Server Error).

This route is crucial for allowing users to edit their comments, an essential feature for dynamic and user-friendly comment sections.

// Route for updating the content of a comment.
router.patch("/replyke-comments", async (req: ExReq, res: ExRes) => {
  try {
    const { update, comment_id } = req.body;

    // Validate the presence of update content and comment_id.
    if (!update || !comment_id) {
      return res.status(400).send("Update content and comment ID are required");
    }

    // Update the comment with the provided content.
    const updatedComment = await Comment.findOneAndUpdate(
      { _id: comment_id },
      { body: update },
      { new: true }
    );

    // If no comment was found or updated, return a 404 (Not Found) status.
    if (!updatedComment) {
      return res.status(404).send("Comment not found or update failed");
    }

    // Return the updated comment.
    return res.status(200).send(updatedComment);
  } catch (err: any) {
    // Handle any server errors.
    return res.status(500).send({ error: "Server error" });
  }
});
Enter fullscreen mode Exit fullscreen mode

Route for Liking a Comment

Endpoint: /replyke-comments/like
Method: POST

This route enables users to like a specific comment, enhancing user engagement and interaction within the Replyke system.

Input Validation: Requires comment_id (the ID of the comment to like) and user_id (the ID of the user liking the comment) in the request body. It returns a 400 (Bad Request) status if either is missing.

Finding the Comment: Attempts to retrieve the specified comment using its ID. If not found, it responds with a 404 (Not Found) status.

Like Verification: Checks if the user has already liked the comment. If so, it sends a 409 (Conflict) status to prevent duplicate likes.

Updating the Like Count:

  • Increments the likes_count and adds the user_id to the likes array of the comment.

  • Uses Comment.findByIdAndUpdate to update the comment.

Response: Returns the updated comment with a 200 (OK) status.

Error Handling: Handles server errors by responding with a 500 (Internal Server Error).

This route adds a vital feature for user interactions, allowing them to express approval or support for comments.

// Route for liking a comment.
router.post("/replyke-comments/like", async (req: ExReq, res: ExRes) => {
  try {
    const { comment_id, user_id } = req.body;

    // Validate the presence of comment_id and user_id.
    if (!comment_id || !user_id) {
      return res.status(400).send("Comment ID and user ID are required");
    }

    // Retrieve the comment to be liked.
    const comment: IComment | null = await Comment.findById(comment_id).lean();
    if (!comment) {
      return res.status(404).send("Comment not found");
    }

    // Check if the user has already liked the comment.
    if (comment.likes.includes(user_id)) {
      return res.status(409).send("User has already liked this comment");
    }

    // Update the comment with the new like.
    const updatedComment = await Comment.findByIdAndUpdate(
      comment_id,
      { $inc: { likes_count: 1 }, $push: { likes: user_id } },
      { new: true }
    );

    // Return the updated comment.
    return res.status(200).send(updatedComment);
  } catch (err: any) {
    // Handle any server errors.
    return res.status(500).send({ error: "Server error" });
  }
});
Enter fullscreen mode Exit fullscreen mode

Route for Unliking a Comment

Endpoint: /replyke-comments/unlike
Method: POST

This route provides the functionality for users to retract their likes from a specific comment, an important feature for user engagement in the Replyke system.

Required Data: Checks for comment_id (the ID of the comment to unlike) and user_id (the ID of the user unliking the comment) in the request body. It returns a 400 (Bad Request) status if either is missing.

Locating the Comment: Attempts to find the specified comment by its ID. If the comment is not found, it responds with a 404 (Not Found) status.

Unlike Verification: Verifies if the user has previously liked the comment. If the user hasn't liked it, it sends a 409 (Conflict) status.

Updating the Like Count:

  • Decreases the likes_count and removes the user_id from the likes array of the comment.

  • Utilizes Comment.findByIdAndUpdate to apply the update.

Response: Sends back the updated comment with a 200 (OK) status.

Error Management: In case of server errors, the route responds with a 500 (Internal Server Error).

This route is essential for maintaining a dynamic and interactive comment section, allowing users to change their reactions as desired.

// Route for unliking a comment.
router.post("/replyke-comments/unlike", async (req: ExReq, res: ExRes) => {
  try {
    const { comment_id, user_id } = req.body;

    // Validate the presence of comment_id and user_id.
    if (!comment_id || !user_id) {
      return res.status(400).send("Comment ID and user ID are required");
    }

    // Retrieve the comment to be unliked.
    const comment: IComment | null = await Comment.findById(comment_id).lean();
    if (!comment) {
      return res.status(404).send("Comment not found");
    }

    // Check if the user has liked the comment.
    if (!comment.likes.includes(user_id)) {
      return res.status(409).send("User hasn't liked this comment");
    }

    // Update the comment to remove the like.
    const updatedComment = await Comment.findByIdAndUpdate(
      comment_id,
      { $inc: { likes_count: -1 }, $pull: { likes: user_id } },
      { new: true }
    );

    // Return the updated comment.
    return res.status(200).send(updatedComment);
  } catch (err: any) {
    // Handle any server errors.
    return res.status(500).send({ error: "Server error" });
  }
});
Enter fullscreen mode Exit fullscreen mode

Customizing your Comment Section

With the knowledge acquired from Part 1 on integrating the Replyke component into your React application, and this article's detailed guide on constructing the necessary API routes, you are now fully equipped to unlock the complete potential of Replyke. You have the tools and understanding to create a robust, interactive, and seamlessly integrated comment section that enhances user engagement on your website.

But there's more to explore! To elevate your comment section further, I invite you to dive into Part 3 of our series, which is available now. In this next installment, you'll discover how to skillfully customize and style your comment section. This guide will provide you with both creative and practical strategies to ensure your comment section is not only functional but also visually compelling, aligning perfectly with your website's design aesthetics. Embark on the journey to aesthetically transform your comment section with Part 3, where style meets functionality!

Top comments (0)