Hello guys, today I will write an article about how to create an app totaljs with mongoose. What is mongoose? Mongoose is an Object Data Modeling (ODM) for MongoDB database.

Why I create this tutorial? Actualy TotalJS has created an example to use TotalJS with Mongoose at here. But as you can see, it mongoose example has not been updated, it was too old as the last commit was 5 years ago. So in this article, I will using Mongoose 5, of course I have been research and make a test with latest MongoDB database version 4.2.0.

Why using Mongoose?

There are several reasons why use Mongoose instead MongoDB driver directly.

  • Schemas

Actualy TotalJS has schema, Mongoose has too, but not for MongoDB. As you are know that MongoDB is schema-less, so sometimes for big project, we still need a schema so your code will be more readable and maintainable. TotalJS schema is can not to be use as data model for MongoDB, so with using Mongoose you are saving many hours to not create schema by yourself as mongoose has this schema feature.

  • Validation

Actualy TotalJS schema has data model validation. but TotalJS schema validation is can not to be use for relationship data model and to validate some spesific data types in MongoDB. Mongoose has this feature, means you have a second layer validation in your TotalJS schema.

  • Instance Methods

You can create a custom methods data hook and pre or post functionality on successful read / write operations in particular document. MongoDB driver has this too but mongoose is more easier.

  • Auto Connection handler

With using mongoose, you can easily create connection to MongoDB without have to open() or close() it manually. Mongoose has been handled this automatically as default.

Actualy there is still many reasons but I can’t describe all because we are not talk about this in detail. Maybe in another article I will write pros and cons of using Mongoose (I’m not promise this. lol).

Reason to not use Mongoose

I can’t not answer this, because the reason to not use Mongoose is relative, everyone has their different answer also it is depend on your situations and conditions. But as for me, the reason to not use mongoose is:

  • If you need the fastest perfomance.
  • You are working alone (not in a team).
  • Your project is small.
  • Your database is strict to use denormalize model.
  • etc

If your condition is like above reasons, so you are better to use MongoDB instead mongoose library.

TotalJS with Mongoose

So let’s start to create a TotalJS project with Mongoose 5.

The goal of this project is:

  • Simple CRUD with latest Mongoose 5
  • Using TotalJS Schema and Mongoose Schema
  • Using MongoDB latest version 4.2.0
  • Json response is standardize between TotalJS and Mongoose response
  • Code must be clean, readable and maintainable

Directory Structure

Before we are going to create the project, this is the directory structure plan for the project.

  • controllers/
    • default.js
    • user.js
  • definitions/
    • mongoose_conn.js
    • mongooose_schema.js
  • models/
    • user.js
  • .gitignore
  • config
  • debug.js
  • package.json
  • postman.json
  • readme.md
  • release.js
  • test.js

package.json

For the first time, we must generate a package.json with using this command

1
$ npm init

Then I will install for total.js and mongoose library from NPM

1
$ npm install total.js mongoose

After install successfully, so here is the example of generated package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"name": "totaljs-mongoose-rest-example",
"version": "1.0.0",
"description": "Example TotalJS with Mongoose 5 for create a simple CRUD Rest API",
"main": "debug.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/aalfiann/totaljs-mongoose-rest-example.git"
},
"keywords": [
"totaljs",
"mongoose",
"rest-api",
"example"
],
"author": "M ABD AZIZ ALFIAN",
"license": "MIT",
"bugs": {
"url": "https://github.com/aalfiann/totaljs-mongoose-rest-example/issues"
},
"homepage": "https://github.com/aalfiann/totaljs-mongoose-rest-example#readme",
"dependencies": {
"mongoose": "^5.7.1",
"total.js": "^3.2.4"
}
}

.gitignore

.gitignore file is required to prevent some files or directory to be uploaded into github repository.

1
2
3
4
node_modules
package-lock.json
debug.pid
tmp

config

config file is for default variables as configuration for application.

Here is the default config file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
name                  : Total.js with Mongoose 5
author : M ABD AZIZ ALFIAN
version : 1.0.0

// Mail settings
mail_smtp : smtp.yourwebsite.com
mail_smtp_options : {"secure":false,"port":25,"user":"","password":"","timeout":10000}
mail_address_from : [email protected]
mail_address_reply : [email protected]
mail_address_copy :

// MongoDB settings
mongodb_url : mongodb://127.0.0.1:27017/app
mongodb_debug_event : true

debug.js

This debug.js file is the default from TotalJS documentation.

You can just copy and paste this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ===================================================
// FOR DEVELOPMENT
// Total.js - framework for Node.js platform
// https://www.totaljs.com
// ===================================================

const options = {};

// options.ip = '127.0.0.1';
// options.port = parseInt(process.argv[2]);
// options.config = { name: 'Total.js' };
// options.sleep = 3000;
// options.inspector = 9229;
// options.watch = ['private'];

require('total.js/debug')(options);

release.js

This release.js file is the default from TotalJS documentation.

You can just copy and paste this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ===================================================
// FOR PRODUCTION
// Total.js - framework for Node.js platform
// https://www.totaljs.com
// ===================================================

const options = {};

// options.ip = '127.0.0.1';
// options.port = parseInt(process.argv[2]);
// options.config = { name: 'Total.js' };
// options.sleep = 3000;

require('total.js').http('release', options);
// require('total.js').cluster.http(5, 'release', options);

test.js

This test.js file is the default from TotalJS documentation.

You can just copy and paste this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ===================================================
// FOR UNIT-TESTING
// Total.js - framework for Node.js platform
// https://www.totaljs.com
// ===================================================

const options = {};

// options.ip = '127.0.0.1';
// options.port = parseInt(process.argv[2]);
// options.config = { name: 'Total.js' };
// options.sleep = 3000;

require('total.js').http('test', options);

readme.md

This readme.md file is just to show detail information about this project.

Here is the readme.md file

1
2
3
4
5
6
7
8
9
# totaljs-mongoose-rest-example
Example TotalJS with Mongoose 5 for create a simple CRUD Rest API

- Download this example
- run `$ npm install`
- edit `config` file and change for `mongodb_url` value with yours.
- run `$ node debug.js`
- open browser `http://127.0.0.1:8000`
- Import `postman.json` into your postman for test

postman.json

This postman.json file is to make a test for all the request API.

You can just copy paste this postman.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
{
"info": {
"_postman_id": "920f56c3-99ab-47f5-b204-330dcac4b15e",
"name": "Example Totaljs with Mongoose 5",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Add User",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"name\":\"john\",\n\t\"address\":\"yogyakarta\"\n}"
},
"url": {
"raw": "http://127.0.0.1:8000/api/user/add",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8000",
"path": [
"api",
"user",
"add"
]
}
},
"response": []
},
{
"name": "List User",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://127.0.0.1:8000/api/user/list",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8000",
"path": [
"api",
"user",
"list"
]
}
},
"response": []
},
{
"name": "Search User",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://127.0.0.1:8000/api/user?search=",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8000",
"path": [
"api",
"user"
],
"query": [
{
"key": "search",
"value": ""
}
]
}
},
"response": []
},
{
"name": "Update User",
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"name": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"body": {
"mode": "raw",
"raw": "{\n\t\"id\":\"5d88ad7da18ff47df554eb77\",\n\t\"name\":\"john\",\n\t\"address\":\"indonesia\"\n}"
},
"url": {
"raw": "http://127.0.0.1:8000/api/user/update",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8000",
"path": [
"api",
"user",
"update"
]
}
},
"response": []
},
{
"name": "Delete User",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "http://127.0.0.1:8000/api/user/delete/{id}",
"protocol": "http",
"host": [
"127",
"0",
"0",
"1"
],
"port": "8000",
"path": [
"api",
"user",
"delete",
"{id}"
]
}
},
"response": []
}
]
}

definitions/mongoose_conn.js

Now we will create a connection from Mongoose to MongoDB database.

You can just copy and paste this mongoose_conn.js file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* You don't have to require this into your other file .js anymore
* because mongoose connection will open once then
* reuse the connection automatically
*/
const mongoose = require( 'mongoose' );

/**
* Mongoose starting connection
*/
mongoose.connect(F.config.mongodb_url, {
autoReconnect: true,
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
useFindAndModify: false,
reconnectTries: Number.MAX_SAFE_INTEGER
});

// CONNECTION EVENTS
if(F.config.mongodb_debug_event) {

// Mongoose start making connection to MongoDB
mongoose.connection.on('connected', function () {
console.log(new Date().format('yyyy-MM-dd HH:mm:ss')+' : '+'Mongoose start making connection to ' + F.config.mongodb_url);
});

// When successfully connected
mongoose.connection.on('connected', function () {
console.log(new Date().format('yyyy-MM-dd HH:mm:ss')+' : '+'Mongoose successfully connected with MongoDB');
});

// When connection lost mongoose will try to reconnect and the reconnect connection is successful connected
mongoose.connection.on('reconnected', function () {
console.log(new Date().format('yyyy-MM-dd HH:mm:ss')+' : '+'Mongoose successfully reconnected with MongoDB');
});

// If the connection throws an error
mongoose.connection.on('error',function (err) {
console.log(new Date().format('yyyy-MM-dd HH:mm:ss')+' : '+'Mongoose connection error: ' + err);
});

// Mongoose start disconnecting from MongoDB
mongoose.connection.on('disconnecting', function () {
console.log(new Date().format('yyyy-MM-dd HH:mm:ss')+' : '+'Disconnecting Mongoose from MongoDB');
});

// When the connection is already disconnected
mongoose.connection.on('disconnected', function () {
console.log(new Date().format('yyyy-MM-dd HH:mm:ss')+' : '+'Mongoose is already disconnected from MongoDB');
});

// If the Node process ends, close the Mongoose connection
process.on('SIGINT', function() {
mongoose.connection.close();
});

}

Note:

  • You don’t have to require this mongoose_conn.js, because this library will be called inside mongoose_schema.js.

mongoose_schema.js

Now we will create a mongoose_schema.js file, this library will work as helper for creating a TotalJS schema.

You can just copy and paste this mongoose_schema.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
const mongoose = require('mongoose');
const mongooseSchema = mongoose.Schema;

/**
* Transform document object
* @param {*} doc
* @param {*} ret
* @return {object}
*/
function transform(doc, ret) {
delete ret._id;
ret.id = doc._id.toString();
return ret;
}

/**
* Create Mongoose schema
* @param {object} obj
* @return {schema}
*/
function createSchema(obj) {
const schema = new mongooseSchema(obj, {
toObject: { transform },
toJSON: { transform }
});

schema.set('toObject', {virtuals:true});
return schema;
}

/**
* Set Mongoose model
* @param {string} name
* @param {object} schema
* @return {model}
*/
function setModel(name,schema) {
return mongoose.model(name, createSchema(schema));
}

/**
* Modify error from totaljs schema
* @param {string} name this is the error key name
* @return {ErrorBuilder}
*/
function schemaErrorBuilder(name){
return ErrorBuilder.addTransform(name, function(isResponse) {
var builder = [];

for (var i = 0, length = this.items.length; i < length; i++) {
var err = this.items[i];
builder.push({name:err.name,error:err.error});
}

if (isResponse) {
if (builder.length > 0){
this.status = 400;
return JSON.stringify({
code:this.status,
status:'error',
message:'Invalid parameter',
error:builder
});
} else {
this.status = 500;
return JSON.stringify({
code:this.status,
status:'error',
message:'Something went wrong...'
});
}
}
return builder;
});
}

/**
* Response error in Mongoose
* @param {controller} $ this is the totaljs controller
* @param {object} err this is the error detail from Mongoose
* @return {callback}
*/
function errorHandler($, err){
$.controller.status = 400;
var error = {
code:err.code,
status:'error',
message:err.errmsg,
error:{
driver:err.driver,
name:err.name,
index:err.index,
keyPattern:err.keyPattern,
keyValue:err.keyValue
}
}
$.callback(JSON.parse(JSON.stringify(error)));
}

/**
* Response success
* @param {controller} $ this is the totaljs controller
* @param {string} message this is the message of response
* @param {*} response this is the response detail
* @return {callback}
*/
function successResponse ($,message, response=[]) {
$.controller.status = 200;
var success = {
'code':200,
'status':'success',
'message':message,
'response':response
}
$.callback(JSON.parse(JSON.stringify(success)));
}

/**
* Response fail
* @param {controller} $ this is the totaljs controller
* @param {string} message this is the message of response
* @param {*} response this is the response detail
* @return {callback}
*/
function failResponse ($, message, response=[]) {
$.controller.status = 200;
var fail = {
'code':200,
'status':'error',
'message':message,
'response':response
}
$.callback(JSON.parse(JSON.stringify(fail)));
}

/**
* Response custom
* @param {controller} $ this is the totaljs controller
* @param {int} code this is the http code you want to sent in response header
* @param {string} status this is the status you want to sent in response body
* @param {string} message this is the message of response
* @param {*} response this is the response detail
* @param {bool} isError this is the type of success or error response
* @return {callback}
*/
function customResponse ($, code, status, message, response=[], isError=false) {
$.controller.status = code;
var custom = {
'code':code,
'status':status,
'message':message
}
if(response !== undefined && response !== null) {
var name = undefined;
if(isError) {
name = 'error';
} else {
name = 'response';
}
custom[name] = response;
}
$.callback(JSON.parse(JSON.stringify(custom)));
}

module.exports = {
createSchema,
setModel,
schemaErrorBuilder,
errorHandler,
successResponse,
failResponse,
customResponse
}

models/user.js

Now we will create user.js file inside models directory.

You can just copy and paste this user.js file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
NEWSCHEMA('User').make(function(schema) {

// Better to use var instead const
var mongoose = require(F.path.definitions('mongoose_schema'));

// Schema for Mongoose
var userSchema = {
name: {
type: String,
required: [true, "A name is required"],
trim: true,
max: [40, 'The maximum name length is 40'],
unique: true
},
address: {
type: String
}
};

// Define User model
var User = mongoose.setModel('User',userSchema);

// Define interface schema
schema.define('id', 'string');
schema.define('name', 'string', true);
schema.define('address', 'string');

// Listen schema validation error from totaljs
mongoose.schemaErrorBuilder('custom');
schema.setError((error) => { error.setTransform('custom') });

// List User
schema.addWorkflow('list',function($) {
User.find().then((response) => {
mongoose.successResponse($,'Data found',response);
}, (err) => {
mongoose.errorBuilder($,err);
});
});

// Add User
schema.addWorkflow('add', function($) {
var body = $.model.$clean();
new User(body).save().then((response) => {
mongoose.successResponse($,'Add user successful',response);
}, (err) => {
mongoose.errorHandler($,err);
});
});

// Search User
schema.addWorkflow('search', function($) {
var query = $.query;
User.find({
$or:[
{name:{$regex: query.search}},
{address:{$regex: query.search}}
]
}).then((response) => {
mongoose.successResponse($,'Data found',response);
},(err) => {
mongoose.errorHandler($,err);
});
});

// Update User
schema.addWorkflow('update', function($) {
var body = $.model.$clean();
User.findOneAndUpdate({
_id:body.id
}, {
name:body.name,
address:body.address
}, {
new:true
}).then((response) => {
mongoose.successResponse($,'Update data is succesful',response);
},(err) => {
mongoose.errorHandler($,err);
});
});

// Delete User
schema.addWorkflow('delete', function($) {
User.deleteOne({
_id:$.id
}).then((response) => {
mongoose.successResponse($,'Delete data is succesful',response);
},(err) => {
mongoose.errorHandler($,err);
});
});

});

As you can see on above code, We are using two schemas, it is TotalJS schema and Mongoose schema. So we have two layer validation for the data model.

controllers/default.js

I will create default controller to show your visitor about your API website.

You can just copy and paste this default.js file

1
2
3
4
5
6
7
8
9
10
exports.install = function() {

ROUTE('/', view_plain);

};

function view_plain() {
var self = this;
self.plain(F.config.name+'\n'+'Version : '+F.config.version+'\n'+'Author : '+F.config.author);
}

controllers/user.js

Now we will create the controller and routes for api user.js.

You can just copy and paste this user.js file

1
2
3
4
5
6
7
8
9
10
11
12
13
exports.install = function() {

// Allow Cors
CORS('/api/*', ['get', 'post', 'put', 'delete'], true);

// Set Route
ROUTE('/api/user', ['get','post','*User --> @search']);
ROUTE('/api/user/list', ['get','post','*User --> @list']);
ROUTE('/api/user/add', ['post','*User --> @add']);
ROUTE('/api/user/update', ['put','*User --> @update']);
ROUTE('/api/user/delete/{id}', ['delete','*User --> @delete']);

};

How to use

Now we have already simple Rest API with Mongoose 5. You can read the readme.md file for using this project.

But if you are following the step by step of this article, to use this project is

  • run the server by this command

    1
    $ node debug.js
  • now open the browser http://127.0.0.1:8000

  • if successfully, now open your postman app

  • Import the postman.json into your postman app

  • Now you can make a request to test your API from postman app

Conclusion

MongoDB version 4.2.0 is the latest MongoDB database and Mongoose 5 is the latest mongoose version for today as I write this article. I have tested this project and everything is running well with no any problems.

This project is just an example to create a Simple CRUD API TotalJS with Mongoose 5. Actualy this project has been uploaded into my github repository, You can just download the whole source code at here.

I don’t make explanation in very detail because the code is very simple and I’m sure this is not too hard for newbie to learning this source code. But if there is something that you don’t understand, just feel free to leave comment at below.

Thank You for your time to reading this article.