Hello guys, this time I will write an article about Best Practice to Create Rest API with TotalJS Framework.
We already know that there is an example emptyproject-restservice from official TotalJS documentaion. But it example only give us the simple way to create a rest api with TotalJS. Simple way doesn’t same meaning with best practice way. So how the best practice way to create rest api with TotalJS framework?

What is the goal?

Before we start to create a basic rest api with TotalJS framework. We will create an empty skeleton for rest api, which is this skeleton will be benefit for us to be use in every new project. I also will upload this project on my github, so you can just download this skeleton again and again in the future.

So here is the goals of this skeleton :

  • An empty rest api project
  • No default session (you are free to use what session method for your project)
  • Use standard uuid and bcrypt for better security
  • Use middleware for custom header x-token
  • Use models as schemas
  • Json response is standardized
  • Code must be clean and readable
  • Best practice way

Make a skeleton

Now for the first time, I will create new project name totaljs-rest-skeleton.

  • Create the package.json
    1
    $ npm init

After generate the package.json with NPM init, so here is my 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
{
"name": "totaljs-rest-skeleton",
"version": "1.0.0",
"description": "TotalJS Rest Skeleton with best practice way",
"main": "debug.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/aalfiann/totaljs-rest-skeleton.git"
},
"keywords": [
"totaljs",
"totaljs-rest",
"totaljs-rest-api",
"totaljs-rest-skeleton"
],
"author": "M ABD AZIZ ALFIAN",
"license": "MIT",
"bugs": {
"url": "https://github.com/aalfiann/totaljs-rest-skeleton/issues"
},
"homepage": "https://github.com/aalfiann/totaljs-rest-skeleton#readme"
}
  • Install total.js, uuid and bcryptjs from NPM
    1
    $ npm install total.js uuid bcryptjs

After install required library successfully, if you take a look in your package.json file, now it’s added new line.

1
2
3
4
5
"dependencies": {
"bcryptjs": "^2.4.3",
"total.js": "^3.3.2",
"uuid": "^3.3.3"
}

Now let’s we continue to the next step.

Directory Structure

The project is still empty, so now we must create the directory structure plan first.

  • controllers/
    • api.js
    • default.js
  • definitions/
    • app_helper.js
    • middleware.js
  • models/
    • account.js
  • .gitignore
  • config
  • debug.js
  • LICENSE
  • package.json
  • postman.json
  • readme.md
  • release.js
  • test.js

The above directory structure is we will create a rest api skeleton with MVC architecture. This design is follow the documentation from TotalJS.

.gitignore

.gitignore is required to prevent the files which is doesn’t need to upload into github repository. In this skeleton, I just prevent the node_modules and package-lock.json to be uploaded into github repository.

So the .gitignore file is must be like this:

1
2
3
4
node_modules
package-lock.json
databases
tmp

config

config file is to set the default settings for application. In this skeleton, I just set for default variables.

So the config file is like this:

1
2
3
4
name                            : Total.js Rest Skeleton
author : M ABD AZIZ ALFIAN (https://github.com/aalfiann)
version : 1.0.0
api_xtoken : 12345678

Note:

  • Config key must be no any space and must be string.
  • Config value could be Object, json, number, boolean, date, array, env or config linking.

Example config:

1
2
3
4
5
6
7
8
9
10
custom_config_object_raw      (Object)    : { name: 'Total.js', date: new Date() }
custom_config_object_json (JSON) : { "name": "Total.js" }
custom_config_number (number) : 320.34
custom_config_boolean (boolean) : true
custom_config_date (date) : 2016-07-26
custom_config_array (array) : [1, 2, 3, 4]
binds_environment_value (env) : APP_NAME

// +v2.9.0 supports config linking
binds_config_value (config) : name

For more detail about config, please see at here

debug.js

debug.js file is to run TotalJS framework as debug mode.

Create debug.js file is very simple as like 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);

To run this debug.js in TotalJS

1
node debug.js

Now your application is running with debug mode.

What is debug.js used for? When you are still in development progress, if there is an update or refactor in your code, application will update it automatically without have to restart the current service.

release.js

release.js file is to run TotalJS framework as release mode.

Create release.js file is very simple as like 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);

To run this release.js in TotalJS

1
node release.js

Now your application is running with release mode.

What is release.js used for? When you are have complete the development progress, you can just run the stable version of your code. If there is an update or refactor in your code, application will not update it automatically and you also have to restart the current service.

test.js

test.js file is to run TotalJS framework as unit test mode.

Create test.js file is very simple as like 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);

To run this test.js in TotalJS

1
node test.js

Now your application is running as unit test mode.

What is test.js used for? TotalJS framework has it own unit test feature, so test mode will only execute the unit test and get the result for you in seconds.

LICENSE

license type file will using MIT, so that everyone will more freedom to use this rest api skeleton in their projects.

Example MIT License:

1
2
3
4
5
6
7
Copyright 2019 M ABD AZIZ ALFIAN

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

readme.md

Readme.md file is must be written about your detail project information.

For default readme.md file is like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# totaljs-rest-skeleton
TotalJS Rest Skeleton with best practice way

## How to use
- Download and extract this project
- open terminal / command-line
- go to extracted directory
- install the library from NPM `$ npm install`
- run `$ node debug.js`
- open browser `http://127.0.0.1:8000`

## How to test the Rest API
- make sure you are able to open browser `http://127.0.0.1:8000`
- open postman app
- import collection `postman.json`
- done, now you are able to make test the rest api

definitions/app_helper.js

Now I will create an app_helper.js, this is used for modify the default response become standardize json response.

So you can just copy paste the class below here :

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
'use strict';

const bcrypt = require('bcryptjs');

module.exports = {
schemaErrorBuilder,
builderErrorResponse,
successResponse,
failResponse,
customResponse,
cryptPassword,
comparePassword
}

/**
* Modify error from schema
* @param {string} name this is the error key name
*/
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) {
this.status = 400;
if (builder.length > 0){
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;
});
}

/**
* Database builder error response
* @param {controller} $ this is the totaljs controller
* @return {callback}
*/
function builderErrorResponse($,err){
$.controller.status = 409;
$.callback(JSON.parse(JSON.stringify({
code:409,
status:'error',
message:'Something went wrong...',
error:err
})));
}

/**
* 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)));
}

/**
* Encrypt password
* @param {string} password this is the user password
* @param {*} callback
* @return {callback}
*/
function cryptPassword(password, callback) {
bcrypt.genSalt(10, function(err, salt) {
if (err)
return callback(err);

bcrypt.hash(password, salt, function(err, hash) {
return callback(err, hash);
});
});
};

/**
* Compare password
* @param {string} plainPass this is the user password
* @param {string} hashword this is the hashed user password
* @param {*} callback
* @return {callback}
*/
function comparePassword(plainPass, hashword, callback) {
bcrypt.compare(plainPass, hashword, function(err, isPasswordMatch) {
return err == null ?
callback(null, isPasswordMatch) :
callback(err);
});
};

definitions/middleware.js

Now I will create the default middleware for authenticate the API.

So you can just create the file middleware.js then copy paste this code below:

1
2
3
4
5
6
7
8
9
10
// Authenticate api
MIDDLEWARE('auth_api',function($) {
$.controller.req.headers['x_token'] = ($.controller.req.headers['x_token'] == undefined)?'':$.controller.req.headers['x_token'];
if($.controller.req.headers['x_token'] == F.config.api_xtoken) {
$.next();
} else {
$.controller.status = 401;
$.controller.json({code:401,status:'error',message:'You\'re not authorized to use this API!'});
}
});

models/account.js

Here is the models for account.

  • create account.js then just copy paste this code below:
    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
    const helper = require(F.path.definitions('app_helper'));
    const uuidv4 = require('uuid/v4');

    NEWSCHEMA('Account').make(function(schema) {
    schema.define('username', 'string');
    schema.define('password', 'string');
    schema.define('email', 'string');

    schema.required('username,password', function(model, op) {
    switch(true) {
    case (op.login == 1):
    return true;
    case (op.register == 1):
    return true;
    default:
    return false;
    }
    });

    schema.required('email', function(model, op) {
    return op.register;
    });

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

    schema.addWorkflow('register', function($) {
    var data = $.model.$clean();
    var username = data.username.toString().toLowerCase();

    var nosql = NOSQL('user_data');

    nosql.find().make(function(builder) {
    builder.where('username', username);
    builder.callback(function(err, response, count) {
    if(err) helper.builderErrorResponse($,err);
    if (count) {
    helper.failResponse($,'Sorry, Username is already exists!')
    } else {
    helper.cryptPassword(data.password,function(err,hash) {
    if(err) helper.customResponse($,409,'error','Failed to encrypt password!',err,true);

    var inputdata = {
    id:uuidv4(),
    username:username,
    hash:hash,
    email:data.email,
    date_created:Date.now()
    };

    nosql.insert(inputdata).callback(function(err) {
    if(err) {
    helper.builderErrorResponse($,err);
    } else {
    helper.successResponse($,'Register is successfully!');
    }
    });
    });
    }
    });
    });
    });

    schema.addWorkflow('login', function($) {
    var data = $.model.$clean();
    var nosql = NOSQL('user_data');
    nosql.find().make(function(builder) {
    builder.where('username', data.username.toString().toLowerCase());
    builder.callback(function(err,response,count) {
    if(err) helper.builderErrorResponse($,err);
    if(count) {
    var user = response[0];
    helper.comparePassword(data.password,user.hash, function(err,isPasswordMatch){
    if(err) helper.customResponse($,409,'error','Failed to compare password!',err,true);
    if(isPasswordMatch) {
    helper.successResponse($,'Login successful');
    } else {
    helper.failResponse($,'Wrong username or password!');
    }
    });
    } else {
    helper.failResponse($,'Wrong username or password!');
    }
    });
    });
    });

    });

I just create example models only for an account to register and login. You have to create how to logout by yourself, because in this skeleton, there is no session.

controllers/api.js

Now I will create the controller for route to the account.js models.

  • create api.js then just copy and paste this code below:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    exports.install = function() {

    // Sets cors for the entire API
    CORS();

    // Account
    ROUTE('/api/account/register', ['post','*Account --> @register','#auth_api']);
    ROUTE('/api/account/login', ['post','*Account --> @login','#auth_api']);

    }

That is very clean right. There is no need extra line code for logic. It was already handled by Schemas in models/ directory.

controller/default.js

default.js in controller is just create plain view to tell your visitor about your api website.

1
2
3
4
5
exports.install = function() {

ROUTE('/', function(){this.plain('REST Service {0}\nVersion: {1}'.format(CONF.name, CONF.version))});

};

postman.json

postman.json is the file for using with postman app to test the request api. You have to import this file into your postman app. Then you are able to make a test your rest api.

  • You 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
    {
    "info": {
    "_postman_id": "f0efd831-06c7-4d07-ad6e-aaad9d0b682a",
    "name": "totaljs-rest-skeleton",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
    },
    "item": [
    {
    "name": "Register",
    "request": {
    "method": "POST",
    "header": [
    {
    "key": "Content-Type",
    "name": "Content-Type",
    "value": "application/json",
    "type": "text"
    },
    {
    "key": "x_token",
    "value": "12345678",
    "type": "text"
    }
    ],
    "body": {
    "mode": "raw",
    "raw": "{\n\t\"username\":\"admin\",\n\t\"password\":\"12345678\",\n\t\"email\":\"[email protected]\"\n}"
    },
    "url": {
    "raw": "http://127.0.0.1:8000/api/account/register",
    "protocol": "http",
    "host": [
    "127",
    "0",
    "0",
    "1"
    ],
    "port": "8000",
    "path": [
    "api",
    "account",
    "register"
    ]
    }
    },
    "response": []
    },
    {
    "name": "http://127.0.0.1:8000/api/account/login",
    "request": {
    "method": "POST",
    "header": [
    {
    "key": "Content-Type",
    "name": "Content-Type",
    "value": "application/json",
    "type": "text"
    },
    {
    "key": "x_token",
    "value": "12345678",
    "type": "text"
    }
    ],
    "body": {
    "mode": "raw",
    "raw": "{\n\t\"username\":\"admin\",\n\t\"password\":\"12345678\"\n}"
    },
    "url": {
    "raw": "http://127.0.0.1:8000/api/account/login",
    "protocol": "http",
    "host": [
    "127",
    "0",
    "0",
    "1"
    ],
    "port": "8000",
    "path": [
    "api",
    "account",
    "login"
    ]
    }
    },
    "response": []
    }
    ]
    }

How to use

Please see the readme.md file, there is step by step how to use or test this rest api skeleton.

Conclusion

TotalJS has many example, but all of the examples is the simple way. Simple way is doesn’t same meaning as best practice. So how do I know about the best practice way? Just read correctly all the documentation from TotalJS and make research by yourself.

This rest api skeleton have uploaded into my github repository, You can just download this skeleton for your project at here.

If you have a question about this skeleton, feel free to ask me by leaving a comment below, or just create an issue in my github repository.

Thank You for your time to reading my article.