In this post I will show how I implemented the commenting feature for my blog using a microservice api.
I previously described how I wrote the UI for my commenting feature using Svelte.
This article will focus on the backend of my commenting feature.
Microservices as a pattern has become a fairly popular choice when architecting application apis. The basic idea is to split your code into smaller units, or services. The term “micro” comes from giving each service/api a very target area of responsibility.
My blog was originally implemented as a single Node/Express application. However, I figured it would make sense to split out the comment api as a separate node application.
Not only does this reduce complexity in the original application. It also helps share some of the user load between two applications.
My blog is deployed to a low-end tier in Azure, which means I am subject to resource quotas at the application level. Shifting some of the load to another application will make my blog more scalable.
When the main application serves a blog post, comments are loaded asynchronously from the new comment api. My comment api supports CORS, so the comment requests are made directly from the browser.
Implementation
The first part is the server itself. Essentially the "server" boils down to configuring an api with CORS enabled endpoints. Most of the logic is delegated to CommentService.
server.js
let express = require('express')
let app = express();
let mongoose = require('mongoose');
mongoose.connect('[connection string]');
let CommentService = require('./comment-service');
let commentService = new CommentService();
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
app.get('/comment/:articleId', (req, res) => {
commentService.getComments(req.params.articleId)
.then((comments) => {
res.json(comments);
})
.catch((error) => {
res.json(error);
});
});
app.listen(process.env.PORT);
Next up is CommentService.
CommentService is where I load comments from MongoDB by articleId, and build a recursive tree of comment threads.
The comments are stored as individual documents in a single MongoDB comment collection. CommentService has logic to parse the flat list of documents into a tree of connected comments.
CommentService is also responsible for maintaining a cache of already loaded comments.
One of the great things about working in Node is that you can use es6 syntax without worrying about transpilation. Node will execute things like promises and classes directly.
comment-service.js
let Comment = require('./comment');
let LoggerService = require('./logger-service');
let loggerService = new LoggerService();
let commentModel = require('./comment-model');
class CommentService {
constructor() {
this.commentCache = {};
}
getComments(articleId){
if(this.commentCache[articleId]) {
return Promise.resolve(this.commentCache[articleId]);
}
return new Promise((resolve, reject) => {
commentModel.find({approved: true, articleId: articleId}).sort({created: 'asc'}).exec((err, comments) => {
if(err) {
loggerService.logError(err);
reject('There was an error fetching comments');
return;
}
if(comments.length === 0) {
resolve([]);
return;
}
this.commentCache[articleId] = this.buildCommentBranchRecursive(null, comments);
resolve(this.commentCache[articleId]);
});
});
}
buildCommentBranchRecursive(parentId, allComments) {
let siblings = allComments.filter(c => c.parentId === parentId);
let comments = [];
siblings.forEach((node) => {
var comment = new Comment(node.author, node.text, node._id.toString(), parentId, node.created);
comment.children = this.buildCommentBranchRecursive(comment.id, allComments);
comments.push(comment);
});
return comments;
}
}
module.exports = CommentService;
I am using Mongoose to talk to MongoDB. Here is my Mongoose schema:
comment-model.js
let mongoose = require('mongoose');
mongoose.Promise = global.Promise;
var commentSchema = new mongoose.Schema(
{
author: {type: String},
text: {type: String},
approved: {type: Boolean, default: false},
articleId: {type: String},
parentId: {type: String},
created: {type: Date, default: Date.now }
},
{ versionKey: false }
);
module.exports = mongoose.model('Comments', commentSchema);
The last piece is a Comment class used to represent comments in the UI. The comment api returns a tree of objects of type Comment.
comment.js
class Comment {
constructor (author,text,id,parentId,created) {
this.id = id;
this.author = author;
this.text = text;
this.parentId = parentId;
this.created = created;
this.children = [];
}
}
module.exports = Comment;