Type something to search...
Setting up Node JS, Express, MongoDB, Prettier, ESLint and Husky Application with Babel and authentication as an example

Setting up Node JS, Express, MongoDB, Prettier, ESLint and Husky Application with Babel and authentication as an example

Introduction

All code from this tutorial as a complete package is available in this repository. If you find this tutorial helpful, please share it with your friends and colleagues, and make sure to star the repository.

Since the ECMAScript JavaScript Standard is revised annually, it is a good idea to update our code as well.

The most recent JavaScript standards are occasionally incompatible with the browser. Something like to babel, which is nothing more than a JavaScript transpiler, is what we need to fix this sort of issue.

So, in this little tutorial, I’ll explain how to set up babel for a basic NodeJS Express application so that we may utilize the most recent ES6 syntax in it.

What is Babel?

Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments. Here are the main things Babel can do for you:

  • Transform syntax
  • Polyfill features that are missing in your target environment (through a third-party polyfill such as core-js)
  • Source code transformations (codemods)

Project Setup

We’ll begin by creating a new directory called backend-template and then we’ll create a new package.json file. We’re going to be using yarn for this example, but you could just as easily use NPM if you choose, but yarn is a lot more convenient.

Create the project directory
mkdir backend-template
cd backend-template
yarn init -y

Engine Locking

The same Node engine and package management that we use should be available to all developers working on this project. We create two new files in order to achieve that:

  • .nvmrc: Will disclose to other project users the Node version that is being utilized.
  • .npmrc: reveals to other project users the package manager being used.
Create the .nvmrc files
touch .nvmrc
echo "lts/fermium" > .nvmrc
Create the .npmrc files
touch .npmrc
echo 'engine-strict=true\r\nsave-exact = true\r\ntag-version-prefix=""\r\nstrict-peer-dependencies = false\r\nauto-install-peers = true\r\nlockfile = true' > .npmrc

Notably, the usage of engine-strict said nothing about yarn in particular; we handle that in packages.json:

open packages.json add the engines:

packages.json
{
"name": "tutorial",
"version": "0.0.0",
"description": "",
"keywords": [],
"main": "index.js",
"license": "MIT",
"author": {
"name": "Mohammad Abu Mattar",
"email": "mohammad.abumattar@outlook.com",
"url": "https://mkabumattar.github.io/"
},
"homepage": "YOUR_GIT_REPO_URL#readme",
"repository": {
"type": "git",
"url": "git+YOUR_GIT_REPO_URL.git"
},
"bugs": {
"url": "YOUR_GIT_REPO_URL/issues"
},
"engines": {
"node": ">=14.0.0",
"yarn": ">=1.20.0",
"npm": "please-use-yarn"
}
}

Babel Setup

In order to set up babel in the project, we must first install three main packages.

  • babel-core: The primary package for running any babel setup or configuration is babel-core.
  • babel-node: Any version of ES may be converted to ordinary JavaScript using the babel-node library.
  • babel-preset-env: This package gives us access to forthcoming functionalities that node.js does not yet comprehend. New features are constantly being developed, thus it will probably take some time for NodeJS to incorporate them.

Babel is mostly used in the code base to take advantage of new JavaScript capabilities. Unless the code is pure JavaScript, we don’t know if the server’s NodeJS will comprehend the specific code or not.

Therefore, transiling the code before deployment is usually advised. Two different types of babel transpiling code exist.

Development Setup:

Install the babel packages
yarn global add nodemon
# for linux users
sudo yarn global add nodemon
Install the babel packages
yarn add express mongoose cors dotenv @babel/core @babel/node @babel/preset-env
## compression cookie-parser core-js crypto-js helmet jsonwebtoken lodash regenerator-runtime
Install the babel packages
yarn add -D @babel/cli babel-plugin-module-resolver

Here, we initialize the package.json and install the basic express server, mongoose, cors, dotenv, babel-core, babel-node, babel-preset-env, babel-plugin-module-resolver.

Babel Configration

After that, we need to create a file called .babelrc in the project’s root directory, and we paste the following block of code there.

Create the .babelrc file
touch .babelrc
.babelrc
{
"presets": ["@babel/preset-env"]
}

Code Formatting and Quality Tools

We will be using two tools in order to establish a standard that will be utilized by all project participants to maintain consistency in the coding style and the use of fundamental best practices:

  • Prettier: A tool that will help us to format our code consistently.
  • ESLint: A tool that will help us to enforce a consistent coding style.

Prettier

Prettier will handle the automated file formatting for us. Add it to the project right now.

It’s only needed during development, so I’ll add it as a devDependency with -D

Install Prettier
yarn add -D prettier

Additionally, I advise getting the Prettier VS Code extension so that you may avoid using the command line tool and have VS Code take care of the file formatting for you. It’s still required to include it here even when it’s installed and set up in your project since VSCode will utilize your project’s settings.

We’ll create two files in the root:

  • .prettierrc: This file will contain the configuration for prettier.
  • .prettierignore: This file will contain the list of files that should be ignored by prettier.
.prettierrc
{
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true
}
.prettierignore
node_modules
build

I’ve listed the folders in that file that I don’t want Prettier to waste any time working on. If you’d want to disregard specific file types in groups, you may also use patterns like *.html.

Now we add a new script to package.json so we can run Prettier:

package.json
6 collapsed lines
{
"name": "tutorial",
"version": "0.0.0",
"description": "",
"keywords": [],
"main": "index.js",
"scripts": {
6 collapsed lines
"start": "node build/bin/www.js",
"dev": "nodemon --exec babel-node src/bin/www.js",
"clean": "rm -rf build",
"build": "yarn clean && npx babel src -d build --minified --presets @babel/preset-env",
"lint": "eslint \"src/**/*.js\" --fix",
"lint:check": "eslint \"src/**/*.js\"",
"prettier": "prettier --write \"src/**/*.js\"",
"prettier:check": "prettier --check \"src/**/*.js\"",
"prepare": "husky install"
},
49 collapsed lines
"license": "MIT",
"author": {
"name": "YOUR_NAME",
"email": "YOUR_EMAIL",
"url": "YOUR_WEBSITE"
},
"homepage": "YOUR_GIT_REPO_URL#readme",
"repository": {
"type": "git",
"url": "git+YOUR_GIT_REPO_URL.git"
},
"bugs": {
"url": "YOUR_GIT_REPO_URL/issues"
},
"engines": {
"node": ">=14.0.0",
"yarn": ">=1.20.0",
"npm": "please-use-yarn"
},
"dependencies": {
"@babel/core": "7.18.5",
"@babel/node": "7.18.5",
"@babel/preset-env": "7.18.2",
"compression": "1.7.4",
"cookie-parser": "1.4.6",
"core-js": "3.23.2",
"cors": "2.8.5",
"crypto-js": "4.1.1",
"dotenv": "16.0.1",
"express": "4.18.1",
"helmet": "5.1.0",
"husky": "8.0.1",
"jsonwebtoken": "9.0.0",
"mongoose": "6.11.3",
"regenerator-runtime": "0.13.9",
"winston": "3.8.0"
},
"devDependencies": {
"@babel/cli": "7.17.10",
"@commitlint/cli": "17.0.3",
"@commitlint/config-conventional": "17.0.3",
"babel-plugin-module-resolver": "4.1.0",
"eslint": "8.18.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-prettier": "4.0.0",
"prettier": "2.7.1"
}
}

You can now run yarn prettier to format all files in the project, or yarn prettier:check to check if all files are formatted correctly.

Run Prettier
yarn prettier:check
yarn prettier

to automatically format, repair, and save all files in your project that you haven’t ignored. My formatter updated around 7 files by default. The source control tab on the left of VS Code has a list of altered files where you may find them.

ESLint

We’ll begin with ESLint, which is a tool that will help us to enforce a consistent coding style, at first need to install the dependencies.

Install ESLint
yarn add -D eslint eslint-config-airbnb-base eslint-config-prettier eslint-plugin-import eslint-plugin-prettier

We’ll create two files in the root:

  • .eslintrc: This file will contain the configuration for ESLint.
  • .eslintignore: This file will contain the list of files that should be ignored by ESLint.
.eslintrc
{
"extends": [
"airbnb-base",
"plugin:prettier/recommended",
"plugin:import/errors",
"plugin:import/warnings"
],
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error",
"import/no-named-as-default": "off",
"no-underscore-dangle": "off"
}
}
.eslintignore
node_modules
build

Now we add a new script to package.json so we can run ESLint:

package.json
6 collapsed lines
{
"name": "tutorial",
"version": "0.0.0",
"description": "",
"keywords": [],
"main": "index.js",
"scripts": {
4 collapsed lines
"start": "node build/bin/www.js",
"dev": "nodemon --exec babel-node src/bin/www.js",
"clean": "rm -rf build",
"build": "yarn clean && npx babel src -d build --minified --presets @babel/preset-env",
"lint": "eslint \"src/**/*.js\" --fix",
"lint:check": "eslint \"src/**/*.js\"",
3 collapsed lines
"prettier": "prettier --write \"src/**/*.js\"",
"prettier:check": "prettier --check \"src/**/*.js\"",
"prepare": "husky install"
},
49 collapsed lines
"license": "MIT",
"author": {
"name": "YOUR_NAME",
"email": "YOUR_EMAIL",
"url": "YOUR_WEBSITE"
},
"homepage": "YOUR_GIT_REPO_URL#readme",
"repository": {
"type": "git",
"url": "git+YOUR_GIT_REPO_URL.git"
},
"bugs": {
"url": "YOUR_GIT_REPO_URL/issues"
},
"engines": {
"node": ">=14.0.0",
"yarn": ">=1.20.0",
"npm": "please-use-yarn"
},
"dependencies": {
"@babel/core": "7.18.5",
"@babel/node": "7.18.5",
"@babel/preset-env": "7.18.2",
"compression": "1.7.4",
"cookie-parser": "1.4.6",
"core-js": "3.23.2",
"cors": "2.8.5",
"crypto-js": "4.1.1",
"dotenv": "16.0.1",
"express": "4.18.1",
"helmet": "5.1.0",
"husky": "8.0.1",
"jsonwebtoken": "9.0.0",
"mongoose": "6.11.3",
"regenerator-runtime": "0.13.9",
"winston": "3.8.0"
},
"devDependencies": {
"@babel/cli": "7.17.10",
"@commitlint/cli": "17.0.3",
"@commitlint/config-conventional": "17.0.3",
"babel-plugin-module-resolver": "4.1.0",
"eslint": "8.18.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-prettier": "4.0.0",
"prettier": "2.7.1"
}
}

You can test out your config by running:

You can now run yarn lint to format all files in the project, or yarn lint:check to check if all files are formatted correctly.

Run ESLint
yarn lint:check
yarn lint

Setup logger for development

I had a problem setting up the logger when constructing a server-side application based on Node and Express router. Conditions for the answer:

  • Logging application event
  • Ability to specify multiple logs level
  • Logging of HTTP requests
  • Ability to write logs into a different source (console and file)

I found two possible solutions:

  • Morgan: HTTP logging middleware for express. It provides the ability to log incoming requests by specifying the formatting of log instance based on different request related information.
  • Winston: Multiple types of transports are supported by a lightweight yet effective logging library. Because I want to simultaneously log events into a file and a terminal, this practical feature is crucial for me.

I’ll use Winston for the logging, first I’ll install the package:

Install Winston
yarn add winston

Then I’ll create a file called utils/logger.util.js in it:

Create the logger.util.js file
touch src/utils/logger.util.js
src/utils/logger.util.js
import winston from 'winston';
import {NODE_ENV} from '../env/variable.env';
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4,
};
const level = () => {
const env = NODE_ENV || 'development';
const isDevelopment = env === 'development';
return isDevelopment ? 'debug' : 'warn';
};
const colors = {
error: 'red',
warn: 'yellow',
info: 'green',
http: 'magenta',
debug: 'white',
};
winston.addColors(colors);
const format = winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss:ms',
}),
winston.format.colorize({all: true}),
winston.format.printf(
(info) => `${info.timestamp} ${info.level}: ${info.message}`,
),
);
const transports = [
new winston.transports.Console(),
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
}),
new winston.transports.File({filename: 'logs/all.log'}),
];
const logger = winston.createLogger({
level: level(),
levels,
format,
transports,
});
export default logger;

Build file structure and basic express application

at first we’ll create a directory called src, and we’ll build the file structure inside of it.

Create the src directory
mkdir src
cd src
mkdir bin config constants controllers env middlewares models repositories routers schemas security services validations

for robust and don’t repeat some text we’ll create constants files in the constants directory:

Create the constants directory
touch src/constants/api.constant.js src/constants/dateformat.constant.js src/constants/http.code.constant.js src/constants/http.reason.constant.js src/constants/message.constant.js src/constants/model.constant.js src/constants/number.constant.js src/constants/path.constant.js src/constants/regex.constant.js

there are some constants that are used in the application, but that are not related to the application itself. For example, the constants that are used in the API are in the api.constant.js file, and there is a file that provides freely for HTTP codes and replies, but we’ll make one from start.

src/constants/dateformat.constant.js
export const YYYY_MM_DD_HH_MM_SS_MS = 'YYYY-MM-DD HH:mm:ss:ms';
export default {
YYYY_MM_DD_HH_MM_SS_MS,
};
src/constants/http.code.constant.js
export const CONTINUE = 100;
export const SWITCHING_PROTOCOLS = 101;
export const PROCESSING = 102;
export const OK = 200;
export const CREATED = 201;
export const ACCEPTED = 202;
export const NON_AUTHORITATIVE_INFORMATION = 203;
export const NO_CONTENT = 204;
export const RESET_CONTENT = 205;
export const PARTIAL_CONTENT = 206;
export const MULTI_STATUS = 207;
export const ALREADY_REPORTED = 208;
export const IM_USED = 226;
export const MULTIPLE_CHOICES = 300;
export const MOVED_PERMANENTLY = 301;
export const MOVED_TEMPORARILY = 302;
export const SEE_OTHER = 303;
export const NOT_MODIFIED = 304;
export const USE_PROXY = 305;
export const SWITCH_PROXY = 306;
export const TEMPORARY_REDIRECT = 307;
export const BAD_REQUEST = 400;
export const UNAUTHORIZED = 401;
export const PAYMENT_REQUIRED = 402;
export const FORBIDDEN = 403;
export const NOT_FOUND = 404;
export const METHOD_NOT_ALLOWED = 405;
export const NOT_ACCEPTABLE = 406;
export const PROXY_AUTHENTICATION_REQUIRED = 407;
export const REQUEST_TIMEOUT = 408;
export const CONFLICT = 409;
export const GONE = 410;
export const LENGTH_REQUIRED = 411;
export const PRECONDITION_FAILED = 412;
export const PAYLOAD_TOO_LARGE = 413;
export const REQUEST_URI_TOO_LONG = 414;
export const UNSUPPORTED_MEDIA_TYPE = 415;
export const REQUESTED_RANGE_NOT_SATISFIABLE = 416;
export const EXPECTATION_FAILED = 417;
export const IM_A_TEAPOT = 418;
export const METHOD_FAILURE = 420;
export const MISDIRECTED_REQUEST = 421;
export const UNPROCESSABLE_ENTITY = 422;
export const LOCKED = 423;
export const FAILED_DEPENDENCY = 424;
export const UPGRADE_REQUIRED = 426;
export const PRECONDITION_REQUIRED = 428;
export const TOO_MANY_REQUESTS = 429;
export const REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
export const UNAVAILABLE_FOR_LEGAL_REASONS = 451;
export const INTERNAL_SERVER_ERROR = 500;
export const NOT_IMPLEMENTED = 501;
export const BAD_GATEWAY = 502;
export const SERVICE_UNAVAILABLE = 503;
export const GATEWAY_TIMEOUT = 504;
export const HTTP_VERSION_NOT_SUPPORTED = 505;
export const VARIANT_ALSO_NEGOTIATES = 506;
export const INSUFFICIENT_STORAGE = 507;
export const LOOP_DETECTED = 508;
export const NOT_EXTENDED = 510;
export const NETWORK_AUTHENTICATION_REQUIRED = 511;
export const NETWORK_CONNECT_TIMEOUT_ERROR = 599;
export default {
CONTINUE,
SWITCHING_PROTOCOLS,
PROCESSING,
OK,
CREATED,
ACCEPTED,
NON_AUTHORITATIVE_INFORMATION,
NO_CONTENT,
RESET_CONTENT,
PARTIAL_CONTENT,
MULTI_STATUS,
ALREADY_REPORTED,
IM_USED,
MULTIPLE_CHOICES,
MOVED_PERMANENTLY,
MOVED_TEMPORARILY,
SEE_OTHER,
NOT_MODIFIED,
USE_PROXY,
SWITCH_PROXY,
TEMPORARY_REDIRECT,
BAD_REQUEST,
UNAUTHORIZED,
PAYMENT_REQUIRED,
FORBIDDEN,
NOT_FOUND,
METHOD_NOT_ALLOWED,
NOT_ACCEPTABLE,
PROXY_AUTHENTICATION_REQUIRED,
REQUEST_TIMEOUT,
CONFLICT,
GONE,
LENGTH_REQUIRED,
PRECONDITION_FAILED,
PAYLOAD_TOO_LARGE,
REQUEST_URI_TOO_LONG,
UNSUPPORTED_MEDIA_TYPE,
REQUESTED_RANGE_NOT_SATISFIABLE,
EXPECTATION_FAILED,
IM_A_TEAPOT,
METHOD_FAILURE,
MISDIRECTED_REQUEST,
UNPROCESSABLE_ENTITY,
LOCKED,
FAILED_DEPENDENCY,
UPGRADE_REQUIRED,
PRECONDITION_REQUIRED,
TOO_MANY_REQUESTS,
REQUEST_HEADER_FIELDS_TOO_LARGE,
UNAVAILABLE_FOR_LEGAL_REASONS,
INTERNAL_SERVER_ERROR,
NOT_IMPLEMENTED,
BAD_GATEWAY,
SERVICE_UNAVAILABLE,
GATEWAY_TIMEOUT,
HTTP_VERSION_NOT_SUPPORTED,
VARIANT_ALSO_NEGOTIATES,
INSUFFICIENT_STORAGE,
LOOP_DETECTED,
NOT_EXTENDED,
NETWORK_AUTHENTICATION_REQUIRED,
NETWORK_CONNECT_TIMEOUT_ERROR,
};
src/constants/http.reason.constant.js
export const CONTINUE = 'Continue';
export const SWITCHING_PROTOCOLS = 'Switching Protocols';
export const PROCESSING = 'Processing';
export const OK = 'OK';
export const CREATED = 'Created';
export const ACCEPTED = 'Accepted';
export const NON_AUTHORITATIVE_INFORMATION = 'Non-Authoritative Information';
export const NO_CONTENT = 'No Content';
export const RESET_CONTENT = 'Reset Content';
export const PARTIAL_CONTENT = 'Partial Content';
export const MULTI_STATUS = 'Multi-Status';
export const ALREADY_REPORTED = 'Already Reported';
export const IM_USED = 'IM Used';
export const MULTIPLE_CHOICES = 'Multiple Choices';
export const MOVED_PERMANENTLY = 'Moved Permanently';
export const MOVED_TEMPORARILY = 'Moved Temporarily';
export const SEE_OTHER = 'See Other';
export const NOT_MODIFIED = 'Not Modified';
export const USE_PROXY = 'Use Proxy';
export const SWITCH_PROXY = 'Switch Proxy';
export const TEMPORARY_REDIRECT = 'Temporary Redirect';
export const BAD_REQUEST = 'Bad Request';
export const UNAUTHORIZED = 'Unauthorized';
export const PAYMENT_REQUIRED = 'Payment Required';
export const FORBIDDEN = 'Forbidden';
export const NOT_FOUND = 'Not Found';
export const METHOD_NOT_ALLOWED = 'Method Not Allowed';
export const NOT_ACCEPTABLE = 'Not Acceptable';
export const PROXY_AUTHENTICATION_REQUIRED = 'Proxy Authentication Required';
export const REQUEST_TIMEOUT = 'Request Timeout';
export const CONFLICT = 'Conflict';
export const GONE = 'Gone';
export const LENGTH_REQUIRED = 'Length Required';
export const PRECONDITION_FAILED = 'Precondition Failed';
export const PAYLOAD_TOO_LARGE = 'Payload Too Large';
export const REQUEST_URI_TOO_LONG = 'Request URI Too Long';
export const UNSUPPORTED_MEDIA_TYPE = 'Unsupported Media Type';
export const REQUESTED_RANGE_NOT_SATISFIABLE =
'Requested Range Not Satisfiable';
export const EXPECTATION_FAILED = 'Expectation Failed';
export const IM_A_TEAPOT = "I'm a teapot";
export const METHOD_FAILURE = 'Method Failure';
export const MISDIRECTED_REQUEST = 'Misdirected Request';
export const UNPROCESSABLE_ENTITY = 'Unprocessable Entity';
export const LOCKED = 'Locked';
export const FAILED_DEPENDENCY = 'Failed Dependency';
export const UPGRADE_REQUIRED = 'Upgrade Required';
export const PRECONDITION_REQUIRED = 'Precondition Required';
export const TOO_MANY_REQUESTS = 'Too Many Requests';
export const REQUEST_HEADER_FIELDS_TOO_LARGE =
'Request Header Fields Too Large';
export const UNAVAILABLE_FOR_LEGAL_REASONS = 'Unavailable For Legal Reasons';
export const INTERNAL_SERVER_ERROR = 'Internal Server Error';
export const NOT_IMPLEMENTED = 'Not Implemented';
export const BAD_GATEWAY = 'Bad Gateway';
export const SERVICE_UNAVAILABLE = 'Service Unavailable';
export const GATEWAY_TIMEOUT = 'Gateway Timeout';
export const HTTP_VERSION_NOT_SUPPORTED = 'HTTP Version Not Supported';
export const VARIANT_ALSO_NEGOTIATES = 'Variant Also Negotiates';
export const INSUFFICIENT_STORAGE = 'Insufficient Storage';
export const LOOP_DETECTED = 'Loop Detected';
export const NOT_EXTENDED = 'Not Extended';
export const NETWORK_AUTHENTICATION_REQUIRED =
'Network Authentication Required';
export const NETWORK_CONNECT_TIMEOUT_ERROR = 'Network Connect Timeout Error';
export default {
CONTINUE,
SWITCHING_PROTOCOLS,
PROCESSING,
OK,
CREATED,
ACCEPTED,
NON_AUTHORITATIVE_INFORMATION,
NO_CONTENT,
RESET_CONTENT,
PARTIAL_CONTENT,
MULTI_STATUS,
ALREADY_REPORTED,
IM_USED,
MULTIPLE_CHOICES,
MOVED_PERMANENTLY,
MOVED_TEMPORARILY,
SEE_OTHER,
NOT_MODIFIED,
USE_PROXY,
SWITCH_PROXY,
TEMPORARY_REDIRECT,
BAD_REQUEST,
UNAUTHORIZED,
PAYMENT_REQUIRED,
FORBIDDEN,
NOT_FOUND,
METHOD_NOT_ALLOWED,
NOT_ACCEPTABLE,
PROXY_AUTHENTICATION_REQUIRED,
REQUEST_TIMEOUT,
CONFLICT,
GONE,
LENGTH_REQUIRED,
PRECONDITION_FAILED,
PAYLOAD_TOO_LARGE,
REQUEST_URI_TOO_LONG,
UNSUPPORTED_MEDIA_TYPE,
REQUESTED_RANGE_NOT_SATISFIABLE,
EXPECTATION_FAILED,
IM_A_TEAPOT,
METHOD_FAILURE,
MISDIRECTED_REQUEST,
UNPROCESSABLE_ENTITY,
LOCKED,
FAILED_DEPENDENCY,
UPGRADE_REQUIRED,
PRECONDITION_REQUIRED,
TOO_MANY_REQUESTS,
REQUEST_HEADER_FIELDS_TOO_LARGE,
UNAVAILABLE_FOR_LEGAL_REASONS,
INTERNAL_SERVER_ERROR,
NOT_IMPLEMENTED,
BAD_GATEWAY,
SERVICE_UNAVAILABLE,
GATEWAY_TIMEOUT,
HTTP_VERSION_NOT_SUPPORTED,
VARIANT_ALSO_NEGOTIATES,
INSUFFICIENT_STORAGE,
LOOP_DETECTED,
NOT_EXTENDED,
NETWORK_AUTHENTICATION_REQUIRED,
NETWORK_CONNECT_TIMEOUT_ERROR,
};
src/constants/path.constant.js
export const LOGS_ALL = 'logs/all.log';
export const LOGS_ERROR = 'logs/error.log';
export default {
LOGS_ALL,
LOGS_ERROR,
};

and we’ll add to other constants in the future.

after creating the directory structure, we’ll create a file called .env and .env.example in the root directory:

  • .env: This file will contain the configuration for the application.
  • .env.example: is the file that contains all of the configurations for constants that .env has, but without values; only this one is versioned. env.example serves as a template for building a .env file that contains the information required to start the program.
Create the .env files
cd ..
touch .env .env.example

Now we add a new variable to .env:

.env
NODE_ENV=development
# NODE_ENV=production
PORT=3030
DATABASE_URL=mongodb://127.0.0.1:27017/example
.env.example
NODE_ENV=development
# NODE_ENV=production
PORT=3030
DATABASE_URL=mongodb://

after creating the directory structure and .env files, we’ll create a file called index.js, bin/www.js, config/db.config.js and env/variable.env.js in the src directory.

Create the index.js, bin/www.js, db.config.js and variable.env.js files
touch src/index.js src/bin/www.js src/config/db.config.js src/env/variable.env.js

new files will be created in the src directory, and we’ll add the following code to each of them:

src/config/db.config.js
import {connect} from 'mongoose';
import logger from '../utils/logger.util';
const connectDb = async (URL) => {
const connectionParams = {
useNewUrlParser: true,
useUnifiedTopology: true,
};
try {
const connection = await connect(URL, connectionParams);
logger.info(`Mongo DB is connected to: ${connection.connection.host}`);
} catch (err) {
logger.error(`An error ocurred\n\r\n\r${err}`);
}
};
export default connectDb;
src/env/variable.env.js
import dotenv from 'dotenv';
dotenv.config();
export const {NODE_ENV} = process.env;
export const {PORT} = process.env;
export const {DATABASE_URL} = process.env;
export default {
NODE_ENV,
PORT,
DATABASE_URL,
};
src/index.js
import express from 'express';
import cors from 'cors';
import {DATABASE_URL} from './env/variable.env';
import connectDb from './config/db.config';
//http constant
import ConstantHttpCode from './constants/http.code.constant';
import ConstantHttpReason from './constants/http.reason.constant';
connectDb(DATABASE_URL);
const app = express();
app.use(express.urlencoded({extended: true}));
app.use(express.json());
app.use(cors());
app.get('/', (req, res, next) => {
try {
res.status(ConstantHttpCode.OK).json({
status: {
code: ConstantHttpCode.OK,
msg: ConstantHttpReason.OK,
},
API: 'Work',
});
} catch (err) {
next(err);
}
});
export default app;
src/bin/www.js
#!/user/bin/env node
import http from 'http';
import app from '..';
import {PORT} from '../env/variable.env';
import logger from '../utils/logger.util';
/**
* Normalize a port into a number, string, or false.
*/
const normalizePort = (val) => {
const port = parseInt(val, 10);
if (Number.isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
};
const port = normalizePort(PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
const server = http.createServer(app);
/**
* Event listener for HTTP server "error" event.
*/
const onError = (error) => {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
logger.error(`${bind} requires elevated privileges`);
process.exit(1);
break;
case 'EADDRINUSE':
logger.error(`${bind} is already in use`);
process.exit(1);
break;
default:
throw error;
}
};
/**
* Event listener for HTTP server "listening" event.
*/
const onListening = () => {
const addr = server.address();
const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`;
logger.info(`Listening on ${bind}`);
};
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

Now we add a new script to package.json so we can run the application:

package.json
6 collapsed lines
{
"name": "tutorial",
"version": "0.0.0",
"description": "",
"keywords": [],
"main": "index.js",
"scripts": {
"start": "node build/bin/www.js",
"dev": "nodemon --exec babel-node src/bin/www.js",
"clean": "rm -rf build",
"build": "yarn clean && npx babel src -d build --minified --presets @babel/preset-env",
"lint": "eslint \"src/**/*.js\" --fix",
"lint:check": "eslint \"src/**/*.js\"",
"prettier": "prettier --write \"src/**/*.js\"",
"prettier:check": "prettier --check \"src/**/*.js\"",
"prepare": "husky install"
},
49 collapsed lines
"license": "MIT",
"author": {
"name": "YOUR_NAME",
"email": "YOUR_EMAIL",
"url": "YOUR_WEBSITE"
},
"homepage": "YOUR_GIT_REPO_URL#readme",
"repository": {
"type": "git",
"url": "git+YOUR_GIT_REPO_URL.git"
},
"bugs": {
"url": "YOUR_GIT_REPO_URL/issues"
},
"engines": {
"node": ">=14.0.0",
"yarn": ">=1.20.0",
"npm": "please-use-yarn"
},
"dependencies": {
"@babel/core": "7.18.5",
"@babel/node": "7.18.5",
"@babel/preset-env": "7.18.2",
"compression": "1.7.4",
"cookie-parser": "1.4.6",
"core-js": "3.23.2",
"cors": "2.8.5",
"crypto-js": "4.1.1",
"dotenv": "16.0.1",
"express": "4.18.1",
"helmet": "5.1.0",
"husky": "8.0.1",
"jsonwebtoken": "9.0.0",
"mongoose": "6.11.3",
"regenerator-runtime": "0.13.9",
"winston": "3.8.0"
},
"devDependencies": {
"@babel/cli": "7.17.10",
"@commitlint/cli": "17.0.3",
"@commitlint/config-conventional": "17.0.3",
"babel-plugin-module-resolver": "4.1.0",
"eslint": "8.18.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-prettier": "4.0.0",
"prettier": "2.7.1"
}
}

New you can run the application with yarn start or yarn dev, and you can also run the application with yarn build to create a production version.

Run the application
yarn dev
yarn start
yarn build

New we’ll add a new package:

compression: Your Node.js app’s main file contains middleware for compression. GZIP, which supports a variety of compression techniques, will then be enabled. Your JSON response and any static file replies will be smaller as a result.

Install compression
yarn add compression

cookie-parser: Your Node.js app’s main file contains middleware for cookie-parser. This middleware will parse the cookies in the request and set them as properties of the request object.

Install cookie-parser
yarn add cookie-parser

core-js: Your Node.js app’s main file contains middleware for core-js. This middleware will add the necessary polyfills to your application.

Install core-js
yarn add core-js

helmet: Your Node.js app’s main file contains middleware for helmet. This middleware will add security headers to your application.

Install helmet
yarn add helmet

regenerator-runtime: Your Node.js app’s main file contains middleware for regenerator-runtime. This middleware will add the necessary polyfills to your application.

Install regenerator-runtime
yarn add regenerator-runtime

we’ll change the index.js file:

src/index.js
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';
import cookieParser from 'cookie-parser';
import {DATABASE_URL} from './env/variable.env';
import connectDb from './config/db.config';
//http constant
import ConstantHttpCode from './constants/http.code.constant';
import ConstantHttpReason from './constants/http.reason.constant';
connectDb(DATABASE_URL);
const app = express();
//helmet
app.use(helmet());
app.use(express.urlencoded({extended: true}));
app.use(express.json());
app.use(compression());
app.use(cors());
app.use(cookieParser());
app.get('/', (req, res, next) => {
try {
res.status(ConstantHttpCode.OK).json({
status: {
code: ConstantHttpCode.OK,
msg: ConstantHttpReason.OK,
},
API: 'Work',
});
} catch (err) {
next(err);
}
});
export default app;

and we’ll change the bin/www.js file:

src/bin/www.js
#!/user/bin/env node
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import http from 'http';
import app from '..';
import {PORT} from '../env/variable.env';
import logger from '../utils/logger.util';
/**
* Normalize a port into a number, string, or false.
*/
const normalizePort = (val) => {
const port = parseInt(val, 10);
if (Number.isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
};
const port = normalizePort(PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
const server = http.createServer(app);
/**
* Event listener for HTTP server "error" event.
*/
const onError = (error) => {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
logger.error(`${bind} requires elevated privileges`);
process.exit(1);
break;
case 'EADDRINUSE':
logger.error(`${bind} is already in use`);
process.exit(1);
break;
default:
throw error;
}
};
/**
* Event listener for HTTP server "listening" event.
*/
const onListening = () => {
const addr = server.address();
const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`;
logger.info(`Listening on ${bind}`);
};
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

new we’ll run the application with yarn dev:

Run the application
╭─mkabumattar@mkabumattar in ~/work/tutorial is v0.0.0 via v18.3.0 took 243ms
╰─λ yarn dev
yarn run v1.22.18
$ nodemon --exec babel-node src/bin/www.js
[nodemon] 2.0.16
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `babel-node src/bin/www.js`
2022-06-25 11:57:38:5738 info: Listening on port 3030
2022-06-25 11:57:38:5738 info: Mongo DB is connected to: 127.0.0.1

Git Hooks

Before moving on to component development, there is one more section on configuration. If you want to expand on this project in the future, especially with a team of other developers, keep in mind that you’ll want it to be as stable as possible. To get it right from the beginning is time well spent.

We’re going to use a program called Husky.

Husky

Husky is a tool for executing scripts at various git stages, such as add, commit, push, etc. We would like to be able to specify requirements and, provided our project is of acceptable quality, only enable actions like commit and push to proceed if our code satisfies those requirements.

To install Husky run

Install Husky
yarn add husky
git init
npx husky install

This will create a .husky directory in your project. Your hooks will be located here. As it is meant for other developers as well as yourself, make sure this directory is included in your code repository.

Create the .gitignore file
touch .gitignore
.gitignore
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

A .husky directory will be created in your project by the second command. Your hooks will be located here. As it is meant for other developers as well as yourself, make sure this directory is included in your code repository.

Add the following script to your package.json file:

package.json
6 collapsed lines
{
"name": "tutorial",
"version": "0.0.0",
"description": "",
"keywords": [],
"main": "index.js",
"scripts": {
8 collapsed lines
"start": "node build/bin/www.js",
"dev": "nodemon --exec babel-node src/bin/www.js",
"clean": "rm -rf build",
"build": "yarn clean && npx babel src -d build --minified --presets @babel/preset-env",
"lint": "eslint \"src/**/*.js\" --fix",
"lint:check": "eslint \"src/**/*.js\"",
"prettier": "prettier --write \"src/**/*.js\"",
"prettier:check": "prettier --check \"src/**/*.js\"",
"prepare": "husky install"
},
49 collapsed lines
"license": "MIT",
"author": {
"name": "YOUR_NAME",
"email": "YOUR_EMAIL",
"url": "YOUR_WEBSITE"
},
"homepage": "YOUR_GIT_REPO_URL#readme",
"repository": {
"type": "git",
"url": "git+YOUR_GIT_REPO_URL.git"
},
"bugs": {
"url": "YOUR_GIT_REPO_URL/issues"
},
"engines": {
"node": ">=14.0.0",
"yarn": ">=1.20.0",
"npm": "please-use-yarn"
},
"dependencies": {
"@babel/core": "7.18.5",
"@babel/node": "7.18.5",
"@babel/preset-env": "7.18.2",
"compression": "1.7.4",
"cookie-parser": "1.4.6",
"core-js": "3.23.2",
"cors": "2.8.5",
"crypto-js": "4.1.1",
"dotenv": "16.0.1",
"express": "4.18.1",
"helmet": "5.1.0",
"husky": "8.0.1",
"jsonwebtoken": "9.0.0",
"mongoose": "6.11.3",
"regenerator-runtime": "0.13.9",
"winston": "3.8.0"
},
"devDependencies": {
"@babel/cli": "7.17.10",
"@commitlint/cli": "17.0.3",
"@commitlint/config-conventional": "17.0.3",
"babel-plugin-module-resolver": "4.1.0",
"eslint": "8.18.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-prettier": "4.0.0",
"prettier": "2.7.1"
}
}

This will ensure Husky gets installed automatically when other developers run the project.

To create a hook run:

Create a hook
npx husky add .husky/pre-commit "yarn lint"

The aforementioned states that the yarn lint script must run and be successful before our commit may be successful. Success here refers to the absence of mistakes. You will be able to get warnings (remember in the ESLint config a setting of 1 is a warning and 2 is an error in case you want to adjust settings).

We’re going to add another one:

Create a hook
npx husky add .husky/pre-push "yarn build"

This makes sure that we can’t push to the remote repository until our code has built correctly. That sounds like a very acceptable requirement, don’t you think? By making this adjustment and attempting to push, feel free to test it.

Commitlint

Finally, we’ll add one more tool. Let’s make sure that everyone on the team is adhering to them as well (including ourselves! ), since we have been using a uniform format for all of our commit messages so far. For our commit messages, we may add a linter.

Install Commitlint
yarn add -D @commitlint/config-conventional @commitlint/cli

We will configure it using a set of common defaults, but since I occasionally forget what prefixes are available, I like to explicitly provide that list in a commitlint.config.js file:

Create a commitlint.config.js file
touch commitlint.config.js
commitlint.config.js
// build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
// ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
// docs: Documentation only changes
// feat: A new feature
// fix: A bug fix
// perf: A code change that improves performance
// refactor: A code change that neither fixes a bug nor adds a feature
// style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
// test: Adding missing tests or correcting existing tests
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'body-leading-blank': [1, 'always'],
'body-max-line-length': [2, 'always', 100],
'footer-leading-blank': [1, 'always'],
'footer-max-line-length': [2, 'always', 100],
'header-max-length': [2, 'always', 100],
'scope-case': [2, 'always', 'lower-case'],
'subject-case': [
2,
'never',
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'build',
'chore',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test',
'translation',
'security',
'changeset',
],
],
},
};

Afterward, use Husky to enable commitlint by using:

Create a hook
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'

Know connected this repository with GitHub, you can now push your commits to GitHub.

Push to GitHub
echo "# Setting up Node JS, Express, MongoDB, Prettier, ESLint and Husky Application with Babel and authentication as an example" >> README.md
git init
git add README.md
git commit -m "ci: Initial commit"
git branch -M main
git remote add origin git@github.com:<your-github-username>/<your-github-repository-name>.git
git push -u origin main

VS Code

Configuration

We can now take advantage of some useful VS Code functionality to have ESLint and Prettier run automatically since we have implemented them.

Make a settings.json file and a directory called .vscode at the top of your project. This will be a list of values that overrides the VS Code installation’s default settings.

Because we may set up particular parameters that only apply to this project and share them with the rest of our team by adding them to the code repository, we want to put them in a folder for the project.

Within settings.json we will add the following values:

Create the settings.json file
mkdir .vscode
touch .vscode/settings.json

settings.json

"./
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.organizeImports": true
}
}

Debugging

In case we encounter any problems while developing our program, let’s set up a handy environment for debugging.

Inside of your .vscode directory create a launch.json file:

Terminal window
touch .vscode/launch.json
.vscode/launch.json
{
"version": "0.1.0",
"configurations": [
{
"name": "debug server",
"type": "node-terminal",
"request": "launch",
"command": "yarn dev"
}
]
}

Authentication

Authentication Setup

We’ll add a new packages to our project:

  • crypto-js: A JavaScript library for encryption and decryption.
  • jsonwebtoken: A JavaScript library for creating and verifying JSON Web Tokens.
Install crypto-js and jsonwebtoken
yarn add crypto-js jsonwebtoken

add secret key to .env file:

.env
...
JWT_SECRET=secret
PASS_SECRET=secret

add JWT_SECRET and PASS_SECRET to variable.env.js file:

src/env/variable.env.js
...
export const { JWT_SECRET } = process.env
export const { PASS_SECRET } = process.env
export default {
...,
JWT_SECRET,
PASS_SECRET,
}

know we’ll add the constants to:

src/constants/api.constant.js
// api
export const API_AUTH = '/api/auth';
export const API_USERS = '/api/users';
// auth
export const AUTH_REGISTER = '/register';
export const AUTH_LOGIN = '/login';
// users
export const USER_UPDATE_USERNAME = '/update-username/:id';
export const USER_UPDATE_NAME = '/update-name/:id';
export const USER_UPDATE_EMAIL = '/update-email/:id';
export const USER_UPDATE_PASSWORD = '/update-password/:id';
export const USER_UPDATE_PHONE = '/update-phone/:id';
export const USER_UPDATE_ADDRESS = '/update-address/:id';
export const USER_DELETE = '/delete/:id';
export const USER_GET = '/find/:id';
export const USER_GET_ALL = '/';
export const USER_GET_ALL_STATS = '/stats';
export default {
// api
API_AUTH,
API_USERS,
// auth
AUTH_REGISTER,
AUTH_LOGIN,
// users
USER_UPDATE_USERNAME,
USER_UPDATE_NAME,
USER_UPDATE_EMAIL,
USER_UPDATE_PASSWORD,
USER_UPDATE_PHONE,
USER_UPDATE_ADDRESS,
USER_DELETE,
USER_GET,
USER_GET_ALL,
USER_GET_ALL_STATS,
};
src/constants/message.constant.js
// token
export const TOKEN_NOT_VALID = 'Token not valid';
export const NOT_AUTHENTICATED = 'Not authenticated';
export const NOT_ALLOWED = 'Not allowed';
// auth
export const USERNAME_NOT_VALID = 'username is not valid';
export const NAME_NOT_VALID = 'name is not valid';
export const EMAIL_NOT_VALID = 'email is not valid';
export const PASSWORD_NOT_VALID = 'password is not valid';
export const PHONE_NOT_VALID = 'phone is not valid';
export const ADDRESS_NOT_VALID = 'address is not valid';
export const USERNAME_EXIST = 'username is exist';
export const EMAIL_EXIST = 'email is exist';
export const PHONE_EXIST = 'phone is exist';
export const USER_NOT_CREATE = 'user is not create, please try again';
export const USER_CREATE_SUCCESS = 'user is create success, please login';
export const USER_NOT_FOUND = 'user is not found';
export const PASSWORD_NOT_MATCH = 'password is not match';
export const USER_LOGIN_SUCCESS = 'user is login success';
// user
export const USERNAME_NOT_CHANGE = 'username is not change';
export const USERNAME_CHANGE_SUCCESS = 'username is change success';
export const NAME_NOT_CHANGE = 'name is not change';
export const NAME_CHANGE_SUCCESS = 'name is change success';
export const EMAIL_NOT_CHANGE = 'email is not change';
export const EMAIL_CHANGE_SUCCESS = 'email is change success';
export const PASSWORD_NOT_CHANGE = 'password is not change';
export const PASSWORD_CHANGE_SUCCESS = 'password is change success';
export const PHONE_NOT_CHANGE = 'phone is not change';
export const PHONE_CHANGE_SUCCESS = 'phone is change success';
export const ADDRESS_NOT_CHANGE = 'address is not change';
export const ADDRESS_CHANGE_SUCCESS = 'address is change success';
export const USER_NOT_DELETE = 'user is not delete, please try again';
export const USER_DELETE_SUCCESS = 'user is delete success';
export const USER_FOUND = 'user is found';
export default {
// token
TOKEN_NOT_VALID,
NOT_AUTHENTICATED,
NOT_ALLOWED,
// auth
USERNAME_NOT_VALID,
NAME_NOT_VALID,
EMAIL_NOT_VALID,
PASSWORD_NOT_VALID,
PHONE_NOT_VALID,
ADDRESS_NOT_VALID,
USERNAME_EXIST,
EMAIL_EXIST,
PHONE_EXIST,
USER_NOT_CREATE,
USER_CREATE_SUCCESS,
USER_NOT_FOUND,
PASSWORD_NOT_MATCH,
USER_LOGIN_SUCCESS,
// user
USERNAME_NOT_CHANGE,
USERNAME_CHANGE_SUCCESS,
NAME_NOT_CHANGE,
NAME_CHANGE_SUCCESS,
EMAIL_NOT_CHANGE,
EMAIL_CHANGE_SUCCESS,
PASSWORD_NOT_CHANGE,
PASSWORD_CHANGE_SUCCESS,
PHONE_NOT_CHANGE,
PHONE_CHANGE_SUCCESS,
ADDRESS_NOT_CHANGE,
ADDRESS_CHANGE_SUCCESS,
USER_NOT_DELETE,
USER_DELETE_SUCCESS,
USER_FOUND,
};
src/constants/model.constant.js
export const USER_MODEL = 'UserModel';
export default {
USER_MODEL,
};
src/constants/number.constant.js
// user
export const USERNAME_MIN_LENGTH = 3;
export const USERNAME_MAX_LENGTH = 20;
export const NAME_MIN_LENGTH = 3;
export const NAME_MAX_LENGTH = 80;
export const EMAIL_MAX_LENGTH = 50;
export const PASSWORD_MIN_LENGTH = 8;
export const PHONE_MIN_LENGTH = 10;
export const PHONE_MAX_LENGTH = 20;
export const ADDRESS_MIN_LENGTH = 10;
export const ADDRESS_MAX_LENGTH = 200;
export default {
USERNAME_MIN_LENGTH,
USERNAME_MAX_LENGTH,
NAME_MIN_LENGTH,
NAME_MAX_LENGTH,
EMAIL_MAX_LENGTH,
PASSWORD_MIN_LENGTH,
PHONE_MIN_LENGTH,
PHONE_MAX_LENGTH,
ADDRESS_MIN_LENGTH,
ADDRESS_MAX_LENGTH,
};
src/constants/regex.constant.js
export const USERNAME = /^(?!.*\.\.)(?!.*\.$)[^\W][\w.]{3,32}$/;
export const EMAIL =
/^(([^<>()\\[\]\\.,;:\s@"]+(\.[^<>()\\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export const PASSWORD =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
export const NAME = /^[a-zA-Z ]{2,35}$/;
export const PHONE =
/^\s*(?:\+?(\d{1,3}))?([-. (]*(\d{3})[-. )]*)?((\d{3})[-. ]*(\d{2,4})(?:[-.x ]*(\d+))?)\s*$/gm;
export const ADDRESS = /^[a-zA-Z0-9\s,'-]{10,200}$/;
export default {
USERNAME,
EMAIL,
PASSWORD,
NAME,
PHONE,
ADDRESS,
};

Authentication Middleware

src/middlewares/token.middleware.js
import jwt from 'jsonwebtoken';
import ConstantMessage from '../constants/message.constant';
import {JWT_SECRET} from '../env/variable.env';
// http constant
import ConstantHttpCode from '../constants/http.code.constant';
import ConstantHttpReason from '../constants/http.reason.constant';
// logger
import logger from '../utils/logger.util';
export const verifyToken = (req, res, next) => {
const authHeader = req.headers.token;
logger.info(`authHeader: ${authHeader}`);
if (authHeader) {
const token = authHeader.split(' ')[1];
return jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
res.status(ConstantHttpCode.FORBIDDEN).json({
status: {
code: ConstantHttpCode.FORBIDDEN,
msg: ConstantHttpReason.FORBIDDEN,
},
msg: ConstantMessage.TOKEN_NOT_VALID,
});
}
req.user = user;
return next();
});
}
return res.status(ConstantHttpCode.UNAUTHORIZED).json({
status: {
code: ConstantHttpCode.UNAUTHORIZED,
msg: ConstantHttpReason.UNAUTHORIZED,
},
msg: ConstantMessage.NOT_AUTHENTICATED,
});
};
export const verifyTokenAndAuthorization = (req, res, next) => {
verifyToken(req, res, () => {
if (req.user.id === req.params.id || req.user.isAdmin) {
return next();
}
return res.status(ConstantHttpCode.FORBIDDEN).json({
status: {
code: ConstantHttpCode.FORBIDDEN,
msg: ConstantHttpReason.FORBIDDEN,
},
msg: ConstantMessage.NOT_ALLOWED,
});
});
};
export const verifyTokenAndAdmin = (req, res, next) => {
verifyToken(req, res, () => {
if (req.user.isAdmin) {
return next();
}
return res.status(ConstantHttpCode.FORBIDDEN).json({
status: {
code: ConstantHttpCode.FORBIDDEN,
msg: ConstantHttpReason.FORBIDDEN,
},
msg: ConstantMessage.NOT_ALLOWED,
});
});
};
export default {
verifyToken,
verifyTokenAndAuthorization,
verifyTokenAndAdmin,
};

Authentication Security

src/security/user.security.js
import CryptoJS from 'crypto-js';
import jwt from 'jsonwebtoken';
import {JWT_SECRET, PASS_SECRET} from '../env/variable.env';
export const encryptedPassword = (password) => {
return CryptoJS.AES.encrypt(password, PASS_SECRET).toString();
};
export const decryptedPassword = (password) => {
return CryptoJS.AES.decrypt(password, PASS_SECRET).toString(
CryptoJS.enc.Utf8,
);
};
export const comparePassword = (password, dPassword) => {
const compare = decryptedPassword(dPassword) === password;
return compare;
};
export const generateAccessToken = (id, isAdmin) => {
return jwt.sign({id, isAdmin}, JWT_SECRET, {expiresIn: '3d'});
};
export default {
encryptedPassword,
decryptedPassword,
comparePassword,
generateAccessToken,
};

Authentication validations

src/validations/user.validation.js
import ConstantRegex from '../constants/regex.constant';
export const validateUsername = async (username) => {
return ConstantRegex.USERNAME.test(username);
};
export const validateName = async (name) => {
return ConstantRegex.NAME.test(name);
};
export const validateEmail = async (email) => {
return ConstantRegex.EMAIL.test(email);
};
export const validatePassword = async (password) => {
return ConstantRegex.PASSWORD.test(password);
};
export const validatePhone = async (phone) => {
return ConstantRegex.PHONE.test(phone);
};
export const validateAddress = async (address) => {
return ConstantRegex.ADDRESS.test(address);
};
export default {
validateUsername,
validateName,
validateEmail,
validatePassword,
validatePhone,
validateAddress,
};

Authentication schemas

src/schemas/user.schema.js
import mongoose from 'mongoose';
import ConstantNumber from '../constants/number.constant';
const UserSchema = new mongoose.Schema(
{
username: {
type: String,
required: true,
unique: true,
min: ConstantNumber.USERNAME_MIN_LENGTH,
max: ConstantNumber.USERNAME_MAX_LENGTH,
},
name: {
type: String,
required: true,
min: ConstantNumber.NAME_MIN_LENGTH,
max: ConstantNumber.NAME_MAX_LENGTH,
},
email: {
type: String,
required: true,
unique: true,
max: ConstantNumber.EMAIL_MAX_LENGTH,
},
password: {
type: String,
required: true,
min: ConstantNumber.PASSWORD_MIN_LENGTH,
},
phone: {
type: String,
required: true,
unique: true,
min: ConstantNumber.PHONE_MIN_LENGTH,
max: ConstantNumber.PHONE_MAX_LENGTH,
},
address: {
type: String,
required: true,
min: ConstantNumber.ADDRESS_MIN_LENGTH,
max: ConstantNumber.ADDRESS_MAX_LENGTH,
},
isAdmin: {
type: Boolean,
default: true,
},
},
{
versionKey: false,
timestamps: true,
},
);
export default UserSchema;

Authentication Models

src/models/user.model.js
import mongoose from 'mongoose';
import UserSchema from '../schemas/user.schema';
import ConstantModel from '../constants/model.constant';
const UserModel = mongoose.model(ConstantModel.USER_MODEL, UserSchema);
export default UserModel;

Authentication Repositories

src/repositories/user.repository.js
import User from '../models/user.model';
export const findAll = async () => {
const users = await User.find({}).select('-password');
return users;
};
export const findById = async (id) => {
const user = await User.findById(id).select('-password');
return user;
};
export const findByIdWithPassword = async (id) => {
const user = await User.findById(id);
return user;
};
export const findByUser = async (username) => {
const user = await User.findOne({username}).select('-password');
return user;
};
export const findByEmail = async (email) => {
const user = await User.findOne({email}).select('-password');
return user;
};
export const findByEmailWithPassword = async (email) => {
const user = await User.findOne({email});
return user;
};
export const findByPhone = async (phone) => {
const user = await User.findOne({phone}).select('-password');
return user;
};
export const createUser = async (user) => {
const newUser = new User({
username: user.username,
name: user.name,
email: user.email,
password: user.password,
phone: user.phone,
address: user.address,
isAdmin: user.isAdmin,
});
const savedUser = await newUser.save();
return savedUser;
};
export const updateUsername = async (id, username) => {
const user = await User.findByIdAndUpdate(id, {username}, {new: true}).select(
'-password',
);
return user;
};
export const updateName = async (id, name) => {
const user = await User.findByIdAndUpdate(id, {name}, {new: true}).select(
'-password',
);
return user;
};
export const updateEmail = async (id, email) => {
const user = await User.findByIdAndUpdate(id, {email}, {new: true}).select(
'-password',
);
return user;
};
export const updatePassword = async (id, password) => {
const user = await User.findByIdAndUpdate(id, {password}, {new: true}).select(
'-password',
);
return user;
};
export const updatePhone = async (id, phone) => {
const user = await User.findByIdAndUpdate(id, {phone}, {new: true}).select(
'-password',
);
return user;
};
export const updateAddress = async (id, address) => {
const user = await User.findByIdAndUpdate(id, {address}, {new: true}).select(
'-password',
);
return user;
};
export const deleteUser = async (id) => {
const user = await User.findByIdAndDelete(id);
return user;
};
export const getUsersStats = async (lastYear) => {
const users = await User.aggregate([
{$match: {createdAt: {$gte: lastYear}}},
{
$project: {
month: {$month: '$createdAt'},
},
},
{
$group: {
_id: '$month',
total: {$sum: 1},
},
},
]);
return users;
};
export default {
findAll,
findById,
findByIdWithPassword,
findByUser,
findByEmail,
findByEmailWithPassword,
findByPhone,
createUser,
updateUsername,
updateName,
updateEmail,
updatePassword,
updatePhone,
updateAddress,
deleteUser,
getUsersStats,
};

Authentication Services

src/services/auth.service.js
import UserRepository from '../repositories/user.repository';
import UserSecurity from '../security/user.security';
import UserValidation from '../validations/user.validation';
export const validateUsername = (username) => {
return UserValidation.validateUsername(username);
};
export const validateName = (name) => {
return UserValidation.validateName(name);
};
export const validateEmail = (email) => {
return UserValidation.validateEmail(email);
};
export const validatePassword = (password) => {
return UserValidation.validatePassword(password);
};
export const comparePassword = (password, encryptedPassword) => {
return UserSecurity.comparePassword(password, encryptedPassword);
};
export const validatePhone = (phone) => {
return UserValidation.validatePhone(phone);
};
export const validateAddress = (address) => {
return UserValidation.validateAddress(address);
};
export const findByUser = async (username) => {
const user = await UserRepository.findByUser(username);
return user;
};
export const findByEmail = async (email) => {
const user = await UserRepository.findByEmailWithPassword(email);
return user;
};
export const findByPhone = async (phone) => {
const user = await UserRepository.findByPhone(phone);
return user;
};
export const createUser = async (user) => {
const encryptedPassword = UserSecurity.encryptedPassword(user.password);
const newUser = {
username: user.username,
name: user.name,
email: user.email,
password: encryptedPassword,
phone: user.phone,
address: user.address,
isAdmin: user.isAdmin,
};
const savedUser = await UserRepository.createUser(newUser);
return savedUser;
};
export const generateAccessToken = async (user) => {
return `Bearer ${UserSecurity.generateAccessToken(user.id, user.isAdmin)}`;
};
export default {
validateUsername,
validateName,
validateEmail,
validatePassword,
comparePassword,
validatePhone,
validateAddress,
findByUser,
findByEmail,
findByPhone,
createUser,
generateAccessToken,
};
src/services/user.service.js
import UserRepository from '../repositories/user.repository';
import UserSecurity from '../security/user.security';
import UserValidation from '../validations/user.validation';
export const validateUsername = (username) => {
return UserValidation.validateUsername(username);
};
export const validateName = (name) => {
return UserValidation.validateName(name);
};
export const validateEmail = (email) => {
return UserValidation.validateEmail(email);
};
export const validatePassword = (name) => {
return UserValidation.validatePassword(name);
};
export const comparePassword = (password, encryptedPassword) => {
return UserSecurity.comparePassword(password, encryptedPassword);
};
export const validatePhone = (phone) => {
return UserValidation.validatePhone(phone);
};
export const validateAddress = (address) => {
return UserValidation.validateAddress(address);
};
export const findAll = async () => {
const users = await UserRepository.findAll();
return users;
};
export const findById = async (id) => {
const user = await UserRepository.findByIdWithPassword(id);
return user;
};
export const findByIdWithOutPassword = async (id) => {
const user = await UserRepository.findById(id);
return user;
};
export const findByEmail = async (email) => {
const user = await UserRepository.findByEmail(email);
return user;
};
export const findByPhone = async (phone) => {
const user = await UserRepository.findByPhone(phone);
return user;
};
export const findByUser = async (username) => {
const user = await UserRepository.findByUser(username);
return user;
};
export const updateUsername = async (id, username) => {
const user = await UserRepository.updateUsername(id, username);
return user;
};
export const updateName = async (id, name) => {
const user = await UserRepository.updateName(id, name);
return user;
};
export const updateEmail = async (id, email) => {
const user = await UserRepository.updateEmail(id, email);
return user;
};
export const updatePassword = async (id, password) => {
const encryptedPassword = UserSecurity.encryptedPassword(password);
const user = await UserRepository.updatePassword(id, encryptedPassword);
return user;
};
export const updatePhone = async (id, phone) => {
const user = await UserRepository.updatePhone(id, phone);
return user;
};
export const updateAddress = async (id, address) => {
const user = await UserRepository.updateAddress(id, address);
return user;
};
export const deleteUser = async (id) => {
const user = await UserRepository.deleteUser(id);
return user;
};
export const getUsersStats = async () => {
const users = await UserRepository.getUsersStats();
return users;
};
export default {
validateUsername,
validateName,
validateEmail,
validatePassword,
comparePassword,
validatePhone,
validateAddress,
findAll,
findById,
findByIdWithOutPassword,
findByEmail,
findByPhone,
findByUser,
updateUsername,
updateName,
updateEmail,
updatePassword,
updatePhone,
updateAddress,
deleteUser,
getUsersStats,
};

Authentication Controllers

auth.controller.js

import ConstantMessage from '../constants/message.constant';
import AuthServices from '../services/auth.service';
// http constant
import ConstantHttpCode from '../constants/http.code.constant';
import ConstantHttpReason from '../constants/http.reason.constant';
// logger
import logger from '../utils/logger.util';
export const register = async (req, res, next) => {
try {
const {username, name, email, password, phone, address} = req.body;
const usernameValidated = AuthServices.validateUsername(username);
if (!usernameValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.USERNAME_NOT_VALID,
});
}
logger.info(`username ${username} is valid`);
const nameValidated = AuthServices.validateName(name);
if (!nameValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.NAME_NOT_VALID,
});
}
logger.info(`name ${name} is valid`);
const emailValidated = AuthServices.validateEmail(email);
if (!emailValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.EMAIL_NOT_VALID,
});
}
logger.info(`email ${email} is valid`);
const passwordValidated = AuthServices.validatePassword(password);
if (!passwordValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_VALID,
});
}
const phoneValidated = AuthServices.validatePhone(phone);
if (!phoneValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PHONE_NOT_VALID,
});
}
const addressValidated = AuthServices.validateAddress(address);
if (!addressValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.ADDRESS_NOT_VALID,
});
}
const usernameCheck = await AuthServices.findByUser(username);
if (usernameCheck) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.USERNAME_EXIST,
});
}
const emailCheck = await AuthServices.findByEmail(email);
if (emailCheck) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.EMAIL_EXIST,
});
}
const phoneCheck = await AuthServices.findByPhone(phone);
if (phoneCheck) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PHONE_EXIST,
});
}
const newUserData = {
username,
name,
email,
password,
phone,
address,
};
const user = await AuthServices.createUser(newUserData);
if (!user) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.USER_NOT_CREATE,
});
}
const newUser = {...user}._doc;
logger.info({newUserpassword: newUser.password});
delete newUser.password;
logger.info({newUserpassword: newUser.password});
return res.status(ConstantHttpCode.CREATED).json({
status: {
code: ConstantHttpCode.CREATED,
msg: ConstantHttpReason.CREATED,
},
msg: ConstantMessage.USER_CREATE_SUCCESS,
data: user,
});
} catch (err) {
return next(err);
}
};
export const login = async (req, res, next) => {
try {
const {email, password} = req.body;
const emailValidated = AuthServices.validateEmail(email);
if (!emailValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.EMAIL_NOT_VALID,
});
}
const passwordValidated = AuthServices.validatePassword(password);
if (!passwordValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_VALID,
});
}
const user = await AuthServices.findByEmail(email);
if (!user) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.USER_NOT_FOUND,
});
}
const isMatch = AuthServices.comparePassword(password, user.password);
if (!isMatch) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_MATCH,
});
}
const accessToken = await AuthServices.generateAccessToken(user);
logger.info(`accessToken: ${accessToken}`);
const newUser = {...user}._doc;
logger.info({newUserpassword: newUser.password});
delete newUser.password;
logger.info({newUserpassword: newUser.password});
return res.status(ConstantHttpCode.OK).json({
status: {
code: ConstantHttpCode.OK,
msg: ConstantHttpReason.OK,
},
msg: ConstantMessage.LOGIN_SUCCESS,
data: {
user,
accessToken,
},
});
} catch (err) {
return next(err);
}
};
export default {
register,
login,
};

user.controller.js

import ConstantMessage from '../constants/message.constant';
import UserService from '../services/user.service';
// http constant
import ConstantHttpCode from '../constants/http.code.constant';
import ConstantHttpReason from '../constants/http.reason.constant';
// logger
import logger from '../utils/logger.util';
export const updateUsername = async (req, res, next) => {
try {
const {username, password} = req.body;
const {id} = req.params;
const user = await UserService.findById(id);
if (!user) {
return res.status(ConstantHttpCode.NOT_FOUND).json({
status: {
code: ConstantHttpCode.NOT_FOUND,
msg: ConstantHttpReason.NOT_FOUND,
},
msg: ConstantMessage.USER_NOT_FOUND,
});
}
logger.info(`user ${user.username} found`);
const usernameValidated = UserService.validateUsername(username);
if (!usernameValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.USERNAME_NOT_VALID,
});
}
logger.info(`username ${username} is valid`);
const passwordValidated = UserService.validatePassword(password);
if (!passwordValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_VALID,
});
}
logger.info(`password ${password} is valid`);
const isMatch = UserService.comparePassword(password, user.password);
if (!isMatch) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_MATCH,
});
}
const usernameCheck = await UserService.findByUser(username);
if (usernameCheck) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.USERNAME_EXIST,
});
}
if (user.username === username) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.USERNAME_NOT_CHANGE,
});
}
const updatedUser = await UserService.updateUsername(id, username);
if (!updatedUser) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.USERNAME_NOT_CHANGE,
});
}
logger.info(`user ${user.username} updated`);
return res.status(ConstantHttpCode.OK).json({
status: {
code: ConstantHttpCode.OK,
msg: ConstantHttpReason.OK,
},
msg: ConstantMessage.USERNAME_CHANGE_SUCCESS,
data: {
user: updatedUser,
},
});
} catch (err) {
return next(err);
}
};
export const updateName = async (req, res, next) => {
try {
const {name, password} = req.body;
const {id} = req.params;
const user = await UserService.findById(id);
if (!user) {
return res.status(ConstantHttpCode.NOT_FOUND).json({
status: {
code: ConstantHttpCode.NOT_FOUND,
msg: ConstantHttpReason.NOT_FOUND,
},
msg: ConstantMessage.USER_NOT_FOUND,
});
}
logger.info(`user ${user.username} found`);
const nameValidated = UserService.validateName(name);
if (!nameValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.NAME_NOT_VALID,
});
}
const passwordValidated = UserService.validatePassword(password);
if (!passwordValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_VALID,
});
}
const isMatch = UserService.comparePassword(password, user.password);
if (!isMatch) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_MATCH,
});
}
logger.info(`password ${password} is valid`);
if (user.name === name) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.NAME_NOT_CHANGE,
});
}
logger.info(`name ${name} is valid`);
const updatedUser = await UserService.updateName(id, name);
if (!updatedUser) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.NAME_NOT_CHANGE,
});
}
logger.info(`user ${user.username} updated`);
return res.status(ConstantHttpCode.OK).json({
status: {
code: ConstantHttpCode.OK,
msg: ConstantHttpReason.OK,
},
msg: ConstantMessage.NAME_CHANGE_SUCCESS,
data: {
user: updatedUser,
},
});
} catch (err) {
return next(err);
}
};
export const updateEmail = async (req, res, next) => {
try {
const {email, password} = req.body;
const {id} = req.params;
const user = await UserService.findById(id);
if (!user) {
return res.status(ConstantHttpCode.NOT_FOUND).json({
status: {
code: ConstantHttpCode.NOT_FOUND,
msg: ConstantHttpReason.NOT_FOUND,
},
msg: ConstantMessage.USER_NOT_FOUND,
});
}
logger.info(`user ${user.username} found`);
const emailValidated = UserService.validateEmail(email);
if (!emailValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.EMAIL_NOT_VALID,
});
}
logger.info(`email ${email} is valid`);
const passwordValidated = UserService.validatePassword(password);
if (!passwordValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_VALID,
});
}
logger.info(`password ${password} is valid`);
const isMatch = UserService.comparePassword(password, user.password);
if (!isMatch) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_MATCH,
});
}
logger.info(`password ${password} is valid`);
if (user.email === email) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.EMAIL_NOT_CHANGE,
});
}
logger.info(`email ${email} is valid`);
const emailCheck = await UserService.findByEmail(email);
if (emailCheck) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.EMAIL_EXIST,
});
}
const updatedUser = await UserService.updateEmail(id, email);
if (!updatedUser) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.EMAIL_NOT_CHANGE,
});
}
logger.info(`user ${user.username} updated`);
return res.status(ConstantHttpCode.OK).json({
status: {
code: ConstantHttpCode.OK,
msg: ConstantHttpReason.OK,
},
msg: ConstantMessage.EMAIL_CHANGE_SUCCESS,
data: {
user: updatedUser,
},
});
} catch (err) {
return next(err);
}
};
export const updatePassword = async (req, res, next) => {
try {
const {oldPassword, newPassword, confirmPassword} = req.body;
const {id} = req.params;
if (newPassword !== confirmPassword) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_MATCH,
});
}
const user = await UserService.findById(id);
if (!user) {
return res.status(ConstantHttpCode.NOT_FOUND).json({
status: {
code: ConstantHttpCode.NOT_FOUND,
msg: ConstantHttpReason.NOT_FOUND,
},
msg: ConstantMessage.USER_NOT_FOUND,
});
}
logger.info(`user ${user.username} found`);
const oldPasswordValidated = UserService.validatePassword(oldPassword);
if (!oldPasswordValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_VALID,
});
}
logger.info(`password ${oldPassword} is valid`);
const newPasswordValidated = UserService.validatePassword(newPassword);
if (!newPasswordValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_VALID,
});
}
logger.info(`password ${newPassword} is valid`);
const confirmPasswordValidated =
UserService.validatePassword(confirmPassword);
if (!confirmPasswordValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_VALID,
});
}
logger.info(`password ${confirmPassword} is valid`);
if (oldPassword === newPassword) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_CHANGE,
});
}
const isMatch = UserService.comparePassword(oldPassword, user.password);
if (!isMatch) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_MATCH,
});
}
const updatedUser = await UserService.updatePassword(id, newPassword);
if (!updatedUser) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_CHANGE,
});
}
logger.info(`user ${user.username} updated`);
return res.status(ConstantHttpCode.OK).json({
status: {
code: ConstantHttpCode.OK,
msg: ConstantHttpReason.OK,
},
msg: ConstantMessage.PASSWORD_CHANGE_SUCCESS,
data: {
user: updatedUser,
},
});
} catch (err) {
return next(err);
}
};
export const updatePhone = async (req, res, next) => {
try {
const {phone, password} = req.body;
const {id} = req.params;
const user = await UserService.findById(id);
if (!user) {
return res.status(ConstantHttpCode.NOT_FOUND).json({
status: {
code: ConstantHttpCode.NOT_FOUND,
msg: ConstantHttpReason.NOT_FOUND,
},
msg: ConstantMessage.USER_NOT_FOUND,
});
}
logger.info(`user ${user.username} found`);
const phoneValidated = UserService.validatePhone(phone);
if (!phoneValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PHONE_NOT_VALID,
});
}
const passwordValidated = UserService.validatePassword(password);
if (!passwordValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_VALID,
});
}
logger.info(`password ${password} is valid`);
const isMatch = UserService.comparePassword(password, user.password);
if (!isMatch) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_MATCH,
});
}
logger.info(`password ${password} is valid`);
if (user.phone === phone) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PHONE_NOT_CHANGE,
});
}
const phoneCheck = await UserService.findByPhone(phone);
if (phoneCheck) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PHONE_EXIST,
});
}
const updatedUser = await UserService.updatePhone(id, phone);
if (!updatedUser) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PHONE_NOT_CHANGE,
});
}
logger.info(`user ${user.username} updated`);
return res.status(ConstantHttpCode.OK).json({
status: {
code: ConstantHttpCode.OK,
msg: ConstantHttpReason.OK,
},
msg: ConstantMessage.PHONE_CHANGE_SUCCESS,
data: {
user: updatedUser,
},
});
} catch (err) {
return next(err);
}
};
export const updateAddress = async (req, res, next) => {
try {
const {address, password} = req.body;
const {id} = req.params;
const user = await UserService.findById(id);
if (!user) {
return res.status(ConstantHttpCode.NOT_FOUND).json({
status: {
code: ConstantHttpCode.NOT_FOUND,
msg: ConstantHttpReason.NOT_FOUND,
},
msg: ConstantMessage.USER_NOT_FOUND,
});
}
logger.info(`user ${user.username} found`);
const addressValidated = UserService.validateAddress(address);
if (!addressValidated) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.ADDRESS_NOT_VALID,
});
}
const isMatch = UserService.comparePassword(password, user.password);
if (!isMatch) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.PASSWORD_NOT_MATCH,
});
}
logger.info(`password ${password} is valid`);
if (user.address === address) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.ADDRESS_NOT_CHANGE,
});
}
const updatedUser = await UserService.updateAddress(id, address);
if (!updatedUser) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.ADDRESS_NOT_CHANGE,
});
}
logger.info(`user ${user.username} updated`);
return res.status(ConstantHttpCode.OK).json({
status: {
code: ConstantHttpCode.OK,
msg: ConstantHttpReason.OK,
},
msg: ConstantMessage.ADDRESS_CHANGE_SUCCESS,
data: {
user: updatedUser,
},
});
} catch (err) {
return next(err);
}
};
export const deleteUser = async (req, res, next) => {
try {
const {id} = req.params;
const user = await UserService.findById(id);
if (!user) {
return res.status(ConstantHttpCode.NOT_FOUND).json({
status: {
code: ConstantHttpCode.NOT_FOUND,
msg: ConstantHttpReason.NOT_FOUND,
},
msg: ConstantMessage.USER_NOT_FOUND,
});
}
logger.info(`user ${user.username} found`);
const deletedUser = await UserService.deleteUser(id);
if (!deletedUser) {
return res.status(ConstantHttpCode.BAD_REQUEST).json({
status: {
code: ConstantHttpCode.BAD_REQUEST,
msg: ConstantHttpReason.BAD_REQUEST,
},
msg: ConstantMessage.USER_NOT_DELETE,
});
}
logger.info(`user ${user.username} deleted`);
return res.status(ConstantHttpCode.OK).json({
status: {
code: ConstantHttpCode.OK,
msg: ConstantHttpReason.OK,
},
msg: ConstantMessage.USER_DELETE_SUCCESS,
});
} catch (err) {
return next(err);
}
};
export const getUser = async (req, res, next) => {
try {
const {id} = req.params;
logger.info(`user ${id} found`);
const user = await UserService.findByIdWithOutPassword(id);
logger.info(`user ${user} found`);
if (!user) {
return res.status(ConstantHttpCode.NOT_FOUND).json({
status: {
code: ConstantHttpCode.NOT_FOUND,
msg: ConstantHttpReason.NOT_FOUND,
},
msg: ConstantMessage.USER_NOT_FOUND,
});
}
logger.info(`user ${user.username} found`);
return res.status(ConstantHttpCode.OK).json({
status: {
code: ConstantHttpCode.OK,
msg: ConstantHttpReason.OK,
},
msg: ConstantMessage.USER_FOUND,
data: {
user,
},
});
} catch (err) {
return next(err);
}
};
export const getUsers = async (req, res, next) => {
try {
const users = await UserService.findAll();
if (!users) {
return res.status(ConstantHttpCode.NOT_FOUND).json({
status: {
code: ConstantHttpCode.NOT_FOUND,
msg: ConstantHttpReason.NOT_FOUND,
},
msg: ConstantMessage.USER_NOT_FOUND,
});
}
logger.info(`users found`);
return res.status(ConstantHttpCode.OK).json({
status: {
code: ConstantHttpCode.OK,
msg: ConstantHttpReason.OK,
},
msg: ConstantMessage.USER_FOUND,
data: {
users,
},
});
} catch (err) {
return next(err);
}
};
export const getUsersStats = async (req, res, next) => {
try {
const usersStats = await UserService.getUsersStats();
if (!usersStats) {
return res.status(ConstantHttpCode.NOT_FOUND).json({
status: {
code: ConstantHttpCode.NOT_FOUND,
msg: ConstantHttpReason.NOT_FOUND,
},
msg: ConstantMessage.USER_NOT_FOUND,
});
}
logger.info(`users stats found`);
return res.status(ConstantHttpCode.OK).json({
status: {
code: ConstantHttpCode.OK,
msg: ConstantHttpReason.OK,
},
msg: ConstantMessage.USER_FOUND,
data: {
users: usersStats,
},
});
} catch (err) {
return next(err);
}
};
export default {
updateUsername,
updateName,
updateEmail,
updatePassword,
updatePhone,
updateAddress,
deleteUser,
getUser,
getUsers,
getUsersStats,
};

Authentication Routes

auth.router.js

import express from 'express';
import ConstantAPI from '../constants/api.constant';
import AuthController from '../controllers/auth.controller';
const router = express.Router();
router.post(ConstantAPI.AUTH_REGISTER, AuthController.register);
router.post(ConstantAPI.AUTH_LOGIN, AuthController.login);
export default router;

user.router.js

import express from 'express';
import ConstantAPI from '../constants/api.constant';
import UserController from '../controllers/user.controller';
import TokenMiddleware from '../middlewares/token.middleware';
const router = express.Router();
router.post(
ConstantAPI.USER_UPDATE_USERNAME,
TokenMiddleware.verifyTokenAndAuthorization,
UserController.updateUsername,
);
router.post(
ConstantAPI.USER_UPDATE_NAME,
TokenMiddleware.verifyTokenAndAuthorization,
UserController.updateName,
);
router.post(
ConstantAPI.USER_UPDATE_EMAIL,
TokenMiddleware.verifyTokenAndAuthorization,
UserController.updateEmail,
);
router.post(
ConstantAPI.USER_UPDATE_PASSWORD,
TokenMiddleware.verifyTokenAndAuthorization,
UserController.updatePassword,
);
router.post(
ConstantAPI.USER_UPDATE_PHONE,
TokenMiddleware.verifyTokenAndAuthorization,
UserController.updatePhone,
);
router.post(
ConstantAPI.USER_UPDATE_ADDRESS,
TokenMiddleware.verifyTokenAndAuthorization,
UserController.updateAddress,
);
router.post(
ConstantAPI.USER_DELETE,
TokenMiddleware.verifyTokenAndAuthorization,
UserController.deleteUser,
);
router.get(
ConstantAPI.USER_GET,
TokenMiddleware.verifyTokenAndAuthorization,
UserController.getUser,
);
router.get(
ConstantAPI.USER_GET_ALL,
TokenMiddleware.verifyTokenAndAdmin,
UserController.getUsers,
);
router.get(
ConstantAPI.USER_GET_ALL_STATS,
TokenMiddleware.verifyTokenAndAdmin,
UserController.getUsersStats,
);
export default router;

edit index.js

import compression from 'compression';
import cookieParser from 'cookie-parser';
import cors from 'cors';
import express from 'express';
import helmet from 'helmet';
import connectDb from './config/db.config';
import {DATABASE_URL} from './env/variable.env';
// http constant
import ConstantHttpCode from './constants/http.code.constant';
import ConstantHttpReason from './constants/http.reason.constant';
// api constant
import ConstantAPI from './constants/api.constant';
// routers
import AuthRouter from './routers/auth.router';
import UserRouter from './routers/user.router';
connectDb(DATABASE_URL);
const app = express();
// helmet
app.use(helmet());
app.use(express.urlencoded({extended: true}));
app.use(express.json());
app.use(compression());
app.use(cors());
app.use(cookieParser());
app.get('/', (req, res, next) => {
try {
return res.status(ConstantHttpCode.OK).json({
status: {
code: ConstantHttpCode.OK,
msg: ConstantHttpReason.OK,
},
API: 'Work',
});
} catch (err) {
return next(err);
}
});
app.use(ConstantAPI.API_AUTH, AuthRouter);
app.use(ConstantAPI.API_USERS, UserRouter);
export default app;

Summary

Finally, after compilation, we can now need to deploy the compiled version in the NodeJS production server.

All code from this tutorial as a complete package is available in this repository.

Related Posts

Check out some of our other posts

Setting up JWT Authentication in Typescript with Express, MongoDB, Babel, Prettier, ESLint, and Husky: Part 2

Setting up JWT Authentication in Typescript with Express, MongoDB, Babel, Prettier, ESLint, and Husky: Part 2

Introduction Why do we even need an authentication mechanism in an application? in my opinion, it doesn't need to be explained. The phrases authentication and authorization have likely crossed yo

read more
Introduction to Spring Boot Framework

Introduction to Spring Boot Framework

Introduction For creating web apps and microservices, many developers utilize the Spring Boot framework. The fact that it is built on top of the Spring Framework and offers a number of advantages

read more
RESTful API vs. GraphQL: Which API is the Right Choice for Your Project?

RESTful API vs. GraphQL: Which API is the Right Choice for Your Project?

TL;DR When deciding between RESTful and GraphQL APIs for a data analysis and display application, it is important to consider the advantages and disadvantages of each. RESTful APIs have been arou

read more
Decoding REST API Architecture: A Comprehensive Guide for Developers

Decoding REST API Architecture: A Comprehensive Guide for Developers

Introduction Hey there, fellow developers! Buckle up because we're about to dive into the crazy world of REST API architecture. Prepare to decode the mysterious differences between REST API and R

read more
Mastering Caching Strategies with Redis Cache: Boosting Performance in Node.js and TypeScript

Mastering Caching Strategies with Redis Cache: Boosting Performance in Node.js and TypeScript

Introduction In the ever-evolving realm of software development, the pursuit of optimizing application performance is a perpetual endeavor. Among the arsenal of strategies to attain this goal, th

read more