How to log a Node.js API in an Express.js app with Mongoose plugins

This tutorial will require prior knowledge of mongoose ORM

Introduction to Logging

As your application grows, logging becomes a crucial part to keep track of everything and most importantly for debugging purpose.

Nowadays there are already logging modules available at npm which you can store in a file in different formats, levels etc. but we are going to discuss the API logging in your node.js express app using popular orm mongoose.

So how you would create mongoose plugin that will do logging for you in a cleaner way and make API logging easy?

What is a plugin in mongoose js?

In mongoose schemas are pluggable, a plugin is like a function that you can plug in your schema and reuse it again and again over schema instance

Mongoose also provides global plugins which you can use for all schemas. For example, we are going to write a plugin that will create a diff of two jsons and write to mongodb .

Step — 1 Creating Basic Log Schema Model

Let’s create a basic log schema with the following six properties

Action: As per its name, this will have a course of action of API whether it is create update delete or something else.

As per its name, this will have a course of action of API whether it is or something else. Category: API category for example Doctors, patients etc. more like a class

API category for example Doctors, patients etc. more like a class CreatedBy: User who is using the api or invoked it.

User who is using the api or invoked it. Message: Here you can include any kind of message you want to show that will make sense or help during debugging

Here you can include any kind of message you want to show that will make sense or help during debugging Diff: This is main property which will have diff of two JSON

You can add more fields if you want that would make sense for your own application. As schema can be changed and upgraded according to requirements

Here is our model: models/log.js

const mongoose = require('mongoose') const Schema = mongoose.Schema const { ObjectId } = Schema const LogSchema = new Schema({ action: { type: String, required: true }, category: { type: String, required: true }, createdBy: { type: ObjectId, ref: 'Account', required: true }, message: { type: String, required: true }, diff: { type: Schema.Types.Mixed }, },{ timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' }, }) LogSchema.index({ action: 1, category: 1 }) module.exports = mongoose.model('Log', LogSchema)

Step — 2 Write a function to get the difference between 2 JSONs

So the next step is you need a reusable function that will create a diff of two jsons on the fly

Lets call it diff.js

const _ = require('lodash') exports.getDiff = (curr, prev) => { function changes(object, base) { return _.transform(object, (result, value, key) => { if (!_.isEqual(value, base[key])) result[key] = (_.isObject(value) && _.isObject(base[key])) ? changes(value, base[key]) : value }) } return changes(curr, prev) }

I have used lodash popular library for the same.

Let’s break down above function and see what’s goin on:

_.transform: Its an alternative to _.reduce or similar to .reduce for arrays.

Basically, it will iterate over your object keys and values and provide an accumulator which is a first argument here result as you can see in callback which is mutable.

Its an alternative to or similar to for arrays. Basically, it will iterate over your object and and provide an which is a first argument here as you can see in callback which is mutable. _.isEqual: Performs a deep comparison between two values to determine if they are equivalent.

isEqual: This method supports comparing arrays, array buffers, booleans, date objects, error objects, maps, numbers, Object objects, regexes, sets, strings, symbols, and typed arrays. Object objects are compared by their own, not inherited, enumerable properties. Functions and DOM nodes are compared by strict equality, i.e. === .

Here we are iterating over each object property and value and comparing it with our old/prev object.

if the value of current object is not equal to a value of same property in the previous object: base[key] and if that value is an object itself we call the function changes recursively until it get a value which will be finally stored in result as result[key] = value

Step — 3 Create a plugin to use diff and save it to database

Now we need to keep the track of previous document in database and create a diff before saving to mongodb

const _ = require('lodash') const LogSchema = require('../models/log') const { getDiff } = require('../utils/diff') const plugin = function (schema) { schema.post('init', doc => { doc._original = doc.toObject({transform: false}) }) schema.pre('save', function (next) { if (this.isNew) { next() }else { this._diff = getDiff(this, this._original) next() } }) schema.methods.log = function (data) { data.diff = { before: this._original, after: this._diff, } return LogSchema.create(data) } } module.exports = plugin

In mongoose, there are different hooks available, for now, we need to use init and save methods available on the schema.

this.isNew() : If you are creating the new document then just return next() middleware.

In schema.post('init') toObject() :

doc._original = doc.toObject({transform: false})

Mongoose Model s inherit from Document s, which have a toObject() method so it will convert a document into an Object() and transform:false is for not allowing to transform the return object.

Step — 4 Usage: How to use in express.js API

In your main server.js or app.js :

Initialise global plugin so that it will be available for all schemas, you can also use for particular schema by initializing in schema model:



const mongoose = require('mongoose') mongoose.plugin(require('./app/utils/diff-plugin'))

Here is basic example of user update api:



const User = require('../models/user') exports.updateUser = (req, res, next) => { return User.findById(req.params.id) .then(user => { if (!user) throw new Error('Target user does not exist. Failed to update.') const { name } = req.body if (name) user.name = name return user.save() }) .then(result => { res.json(result) return result }) .catch(next) .then(user => { if (user && typeof user.log === 'function') { const data = { action: 'update-user', category: 'users', createdBy: req.user.id, message: 'Updated user name', } return user.log(data) } }).catch(err => { console.log('Caught error while logging: ', err) }) }

Conclusion

In this tutorial, you learned how to create a mongoose plugin and use it log the changes in your api. You can do a lot more with plugins to build a robust node application.

Here are resources to learn more about mongoose and plugins usage:

80/20 Guide to mongoose plugins:



thecodebarbarian.com





thecodebarbarian.com Mongoose Plugins

I hope you find this tutorial useful, feel free to reach out if you have any questions or comment below 🙂