author

Kien Duong

April 29, 2022

NodeJS Boilerplate (Express, Typescript, TypeORM, MySQL)

In this blog, we’re going to create a nodejs boilerplate project that connects to MySQL database via TypeORM library. Here is the reference project structure:

nodejs boilerplate 1

We will go through each directory and see the intended use.

  • configs: setup the configuration for the project (express, logger,…).
  • constants: define the constants that have used in the project.
  • controllers: handle logic for all controller layers.
  • entities: define the types & the relation of the tables.
  • enums: define the enums that have used in the project.
  • errors: build logic to custom handling errors.
  • interfaces: define the interfaces that have used in the project.
  • middlewares: create the middlewares for pre-processing the requests.
  • migrations: define the migrations to process the tables structure & seed data.
  • mocks: for testing.
  • routes: define all endpoints.
  • seeds: create sample data when running the migrations.
  • services: handle logic for actions to the database via TypeORM library.
  • utilities: build utility functions.
  • validations: define the schemas that have been used to validate the requests.

nodejs boilerplate 2

    // tsconfig.json
{
  "compilerOptions": {
    "module": "commonjs",
    "esModuleInterop": true,
    "target": "es6",
    "noImplicitAny": true,
    "moduleResolution": "node",
    "sourceMap": false,
    "outDir": "dist",
    "baseUrl": ".",
    "rootDir": "src",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "paths": {
      "*": [
        "node_modules/*",
        "src/interfaces/*"
      ]
    },
    "lib": [
      "es2015", "es5", "es6", "dom"
    ]
  },
  "include": [
    "src/**/*"
  ]
}
  
    // ormconfig.js
require('dotenv/config');

module.exports = {
  type: 'mysql',
  host: process.env.DB_HOST || '127.0.0.1',
  username: process.env.DB_USER || 'root',
  password: process.env.DB_PASSWORD || '',
  database: process.env.DB_NAME || 'nodejs-sample',
  port: process.env.DB_PORT || 3306,
  charset: 'utf8',
  driver: 'mysql',
  synchronize: false,
  entities: process.env.NODE_ENV !== 'production' ? ['**/**.entity.ts'] : ['dist/**/*.entity.js'],
  logging: process.env.NODE_ENV !== 'production' ? 'all' : 'error',
  migrations: process.env.NODE_ENV !== 'production' ? ['src/migrations/*.ts'] : ['dist/migrations/*.js'],
  cli: {
    migrationsDir: 'src/migrations'
  },
  connectTimeout: 30000,
  acquireTimeout: 30000
};

  
    // .env
NODE_ENV=development
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=12345678
DB_NAME=nodejs-sample
PORT=5555
TOKEN_SECRET_KEY=test
  
    // nodemon.json
{
  "watch": ["src"],
  "ext": "ts",
  "ignore": ["src/**/*.spec.ts"],
  "exec": "ts-node ./src/index.ts"
}

  

In the package.json file, we will define some commands to run & build the project.

    // package.json

...
"scripts": {
    "test": "jest --collectCoverage",
    "prebuild": "rimraf dist/",
    "build": "tsc",
    "start": "yarn serve",
    "serve": "node dist/index.js",
    "dev": "nodemon",
    "watch-ts": "tsc -w",
    "migration:generate": "typeorm migration:create -n",
    "migration:run": "ts-node ./node_modules/typeorm/cli.js migration:run",
    "migration:revert": "ts-node ./node_modules/typeorm/cli.js migration:revert"
  },
...
  
    // index.ts
require('dotenv').config();

import 'reflect-metadata';
import { createConnection } from 'typeorm';
import logger from './configs/logger.config';
import app from './configs/express.config';

const PORT = process.env.PORT || 5000;

const connect = async () => {
  try {
    const connection = await createConnection(); // Connect to the DB that is setup in the ormconfig.js
    await connection.runMigrations(); // Run all migrations
    logger.info('Connect to database successfully');
    app.listen(PORT, () => {
      logger.info(`Server running at ${PORT}`);
    });
  } catch (e) {
    logger.info(`The connection to database was failed with error: ${e}`);
  }
}

connect();

  

1. Migrations

In the ormconfig.js file, we define two properties: migrations & cli. It means that typeorm will handle all ts files inside the src/migrations folder.

In the package.json file, we define 3 commands:

  • migration:generate to create a migration file.
  • migration:run to run all migration files.
  • migration:revert to revert to the previous migration.

Run two commands: yarn migration:generate CreateUserTable & yarn migration:generate SeedUserTable to create user table & sample data.

    // 1651270421471-CreateUserTable.ts
import { MigrationInterface, QueryRunner, Table } from 'typeorm';

export class CreateUserTable1651270421471 implements MigrationInterface {

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.createTable(
      new Table({
        name: 'user',
        columns: [
          {
            name: 'id',
            type: 'int',
            isPrimary: true,
            isGenerated: true,
            generationStrategy: 'increment',
          },
          {
            name: 'createdAt',
            type: 'datetime',
            default: 'CURRENT_TIMESTAMP',
          },
          {
            name: 'updatedAt',
            type: 'datetime',
            default: 'CURRENT_TIMESTAMP',
          },
          {
            name: 'email',
            type: 'varchar',
            length: '100',
            isNullable: false,
          },
          {
            name: 'password',
            type: 'varchar',
            length: '100',
            isNullable: false,
          },
          {
            name: 'firstName',
            type: 'varchar',
            length: '255',
            isNullable: false,
          },
          {
            name: 'lastName',
            type: 'varchar',
            length: '255',
            isNullable: false,
          },
          {
            name: 'isDeleted',
            type: 'tinyint',
            default: '0',
          },
        ],
        uniques: [
          {
            columnNames: ['email'],
          },
        ],
      }),
    );
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.dropTable('user');
  }

}

  
    // 1651270465906-SeedUserTable.ts
import { MigrationInterface, QueryRunner, getRepository } from 'typeorm';
import { User } from '../entities/user/user.entity';
import { userSeed } from '../seeds/user.seed';

export class SeedUserTable1651270465906 implements MigrationInterface {

  public async up(queryRunner: QueryRunner): Promise<void> {
    await getRepository(User).save(userSeed);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
  }

}

  

2. Entities

Inside entities folder, we create two files:

  • base.entity.ts is extended by other entities.
  • user.entity.ts an entity for user table.

nodejs boilerplate 3

    // base.entity.ts
import { CreateDateColumn, UpdateDateColumn } from 'typeorm';

export class BaseEntity {
  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

  
    // user.entity.ts
import { Column, Entity, PrimaryGeneratedColumn, Unique, OneToMany } from 'typeorm';

// Entities
import { BaseEntity } from '../base/base.entity';

@Entity('user', { orderBy: { id: 'DESC' } })
export class User extends BaseEntity {

  @PrimaryGeneratedColumn({ type: 'int' })
  id: number;

  @Column({ length: 100, nullable: false })
  @Unique(['email'])
  email: string;

  @Column({ length: 100, nullable: false, select: false })
  password: string;

  @Column({ length: 255, nullable: false })
  firstName: string;

  @Column({ length: 255, nullable: false })
  lastName: string;

  @Column({ default: false })
  isDeleted: boolean;

  toJSON() {
    delete this.isDeleted;
    return this;
  }

}

  

3. Services

We create user.service.ts to handle CRUD logic for users.

nodejs boilerplate 4

    // user.service.ts
import { getRepository } from 'typeorm';

// Entities
import { User } from '../../entities/user/user.entity';

// Utilities
import Encryption from '../../utilities/encryption.utility';
import ApiUtility from '../../utilities/api.utility';
import DateTimeUtility from '../../utilities/date-time.utility';

// Interfaces
import {
  ICreateUser,
  ILoginUser,
  IUpdateUser,
  IUserQueryParams,
} from '../../interfaces/user.interface';
import { IDeleteById, IDetailById } from '../../interfaces/common.interface';

// Errors
import { StringError } from '../../errors/string.error';

const where = { isDeleted: false };

const create = async (params: ICreateUser) => {
  const item = new User();
  item.email = params.email;
  item.password = await Encryption.generateHash(params.password, 10);
  item.firstName = params.firstName;
  item.lastName = params.lastName;
  const userData = await getRepository(User).save(item);
  return ApiUtility.sanitizeUser(userData);
};

const login = async (params: ILoginUser) => {
  const user = await getRepository(User)
    .createQueryBuilder('user')
    .where('user.email = :email', { email: params.email })
    .select([
      'user.createdAt',
      'user.updatedAt',
      'user.id',
      'user.email',
      'user.password',
      'user.firstName',
      'user.lastName',
      'user.isDeleted',
    ])
    .getOne();

  if (!user) {
    throw new StringError('Your email has not been registered');
  }

  if (await Encryption.verifyHash(params.password, user.password)) {
    return ApiUtility.sanitizeUser(user);
  }

  throw new StringError('Your password is not correct');
};

const getById = async (params: IDetailById) => {
  try {
    const data = await getRepository(User).findOne({ id: params.id });
    return ApiUtility.sanitizeUser(data);
  } catch (e) {
    return null;
  }
};

const detail = async (params: IDetailById) => {
  const query = {
    where: { ...where, id: params.id },
  }

  const user = await getRepository(User).findOne(query);
  if (!user) {
    throw new StringError('User is not existed');
  }

  return ApiUtility.sanitizeUser(user);
}

const update = async (params: IUpdateUser) => {
  const query = { ...where, id: params.id };

  const user = await getRepository(User).findOne(query);
  if (!user) {
    throw new StringError('User is not existed');
  }

  return await getRepository(User).update(query, {
    firstName: params.firstName,
    lastName: params.lastName,
    updatedAt: DateTimeUtility.getCurrentTimeStamp(),
  });
}

const list = async (params: IUserQueryParams) => {
  let userRepo = getRepository(User).createQueryBuilder('user');
  userRepo = userRepo.where('user.isDeleted = :isDeleted', { isDeleted: false });

  if (params.keyword) {
    userRepo = userRepo.andWhere(
      '(LOWER(user.firstName) LIKE LOWER(:keyword) OR LOWER(user.lastName) LIKE LOWER(:keyword))',
      { keyword: `%${params.keyword}%` },
    );
  }

  // Pagination
  const paginationRepo = userRepo;
  const total = await paginationRepo.getMany();
  const pagRes = ApiUtility.getPagination(total.length, params.limit, params.page);

  userRepo = userRepo.limit(params.limit).offset(ApiUtility.getOffset(params.limit, params.page));
  const users = await userRepo.getMany();

  const response = [];
  if (users && users.length) {
    for (const item of users) {
      response.push(ApiUtility.sanitizeUser(item));
    }
  }
  return { response, pagination: pagRes.pagination };
};

const remove = async (params: IDeleteById) => {
  const query = { ...where, id: params.id };

  const user = await getRepository(User).findOne(query);
  if (!user) {
    throw new StringError('User is not existed');
  }

  return await getRepository(User).update(query, {
    isDeleted: true,
    updatedAt: DateTimeUtility.getCurrentTimeStamp(),
  });
}

export default {
  create,
  login,
  getById,
  detail,
  update,
  list,
  remove,
}

  

4. Controllers

We create user.controller.ts to handle CRUD logic for users.

nodejs boilerplate 5

    // user.controller.ts
import httpStatusCodes from 'http-status-codes';

// Interfaces
import IController from '../../interfaces/IController';
import {
  ICreateUser,
  ILoginUser,
  IUpdateUser,
  IUserQueryParams,
} from '../../interfaces/user.interface';
import { IDeleteById, IDetailById } from '../../interfaces/common.interface';

// Errors
import { StringError } from '../../errors/string.error';

// Services
import userService from '../../services/user/user.service';

// Utilities
import ApiResponse from '../../utilities/api-response.utility';
import Encryption from '../../utilities/encryption.utility';
import ApiUtility from '../../utilities/api.utility';

// Constants
import constants from '../../constants';

const create: IController = async (req, res) => {
  try {
    const params: ICreateUser = {
      email: req.body.email,
      password: req.body.password,
      firstName: req.body.firstName,
      lastName: req.body.lastName,
    }
    const user = await userService.create(params);
    return ApiResponse.result(res, user, httpStatusCodes.CREATED);
  } catch (e) {
    if (e.code === constants.ERROR_CODE.DUPLICATED) {
      return ApiResponse.error(res, httpStatusCodes.CONFLICT, 'Email already exists.');
    }
    return ApiResponse.error(res, httpStatusCodes.BAD_REQUEST);
  }
};

const login: IController = async (req, res) => {
  try {
    const params: ILoginUser = {
      email: req.body.email,
      password: req.body.password,
    }
    const user = await userService.login(params);
    const cookie = await generateUserCookie(user.id);
    return ApiResponse.result(res, user, httpStatusCodes.OK, cookie);
  } catch (e) {
    if (e instanceof StringError) {
      return ApiResponse.error(res, httpStatusCodes.BAD_REQUEST, e.message);
    }
    return ApiResponse.error(res, httpStatusCodes.BAD_REQUEST, 'Something went wrong');
  }
};

const me: IController = async (req, res) => {
  const cookie = await generateUserCookie(req.user.id);
  return ApiResponse.result(res, req.user, httpStatusCodes.OK, cookie);
};

const detail: IController = async (req, res) => {
  try {
    const params: IDetailById = {
      id: parseInt(req.params.id, 10),
    }
    const data = await userService.detail(params);
    return ApiResponse.result(res, data, httpStatusCodes.OK);
  } catch (e) {
    ApiResponse.exception(res, e);
  }
};

const update: IController = async (req, res) => {
  try {
    const params: IUpdateUser = {
      firstName: req.body.firstName,
      lastName: req.body.lastName,
      id: parseInt(req.params.id, 10),
    }
    await userService.update(params);
    return ApiResponse.result(res, params, httpStatusCodes.OK);
  } catch (e) {
    ApiResponse.exception(res, e);
  }
};

const updateMe: IController = async (req, res) => {
  try {
    const params: IUpdateUser = {
      firstName: req.body.firstName,
      lastName: req.body.lastName,
      id: req.user.id,
    }
    await userService.update(params);
    return ApiResponse.result(res, params, httpStatusCodes.OK);
  } catch (e) {
    ApiResponse.exception(res, e);
  }
};

const list: IController = async (req, res) => {
  try {
    const limit = ApiUtility.getQueryParam(req, 'limit');
    const page = ApiUtility.getQueryParam(req, 'page');
    const keyword = ApiUtility.getQueryParam(req, 'keyword');
    const params: IUserQueryParams = { limit, page, keyword };
    const data = await userService.list(params);
    return ApiResponse.result(res, data.response, httpStatusCodes.OK, null, data.pagination);
  } catch (e) {
    ApiResponse.exception(res, e);
  }
};

const remove: IController = async (req, res) => {
  try {
    const params: IDeleteById = {
      id: parseInt(req.params.id, 10),
    }
    await userService.remove(params);
    return ApiResponse.result(res, params, httpStatusCodes.OK);
  } catch (e) {
    ApiResponse.exception(res, e);
  }
};

const generateUserCookie = async (userId: number) => {
  return {
    key: constants.COOKIE.COOKIE_USER,
    value: await Encryption.generateCookie(constants.COOKIE.KEY_USER_ID, userId.toString()),
  };
};

export default {
  create,
  login,
  me,
  detail,
  update,
  updateMe,
  list,
  remove,
};

  

5. Routes

We define the endpoints for authentication, user

nodejs boilerplate 6

    // index.route.ts
import * as express from 'express';

import defaultRouter from './default/default.route';
import authRouter from './auth/auth.route';
import meRouter from './me/me.route';
import userRouter from './user/user.route';

const router = express.Router();

router.use('/', defaultRouter);
router.use('/auth', authRouter);
router.use('/me', meRouter);
router.use('/user', userRouter);

export default router;

  
    // auth.route.ts
import express from 'express';
const schemaValidator = require('express-joi-validator');

// Controller
import userController from '../../controllers/user/user.controller';

// Schema
import userSchema from '../../validations/schemas/user.schema';

const router = express.Router();

router.post(
  '/register',
  schemaValidator(userSchema.register),
  userController.create,
);

router.post(
  '/login',
  schemaValidator(userSchema.login),
  userController.login,
);

export default router;

  
    // me.route.ts
import express from 'express';
const schemaValidator = require('express-joi-validator');

// Controller
import userController from '../../controllers/user/user.controller';

// Schema
import userSchema from '../../validations/schemas/user.schema';

const router = express.Router();

router.get(
  '/',
  userController.me,
);

router.put(
  '/',
  schemaValidator(userSchema.updateMe),
  userController.updateMe,
);

export default router;

  
    // user.route.ts
import express from 'express';
const schemaValidator = require('express-joi-validator');

// Controller
import userController from '../../controllers/user/user.controller';

// Schema
import userSchema from '../../validations/schemas/user.schema';

// Middleware
import { isAdmin } from '../../middlewares/permission-handler.middleware';

const router = express.Router();

router.get(
  '/',
  userController.list,
);

router.delete(
  '/:id',
  isAdmin(),
  userController.remove,
);

export default router;

  

6. Middlewares

Inside middlewares folder, we create 4 middlewares to handle the requests & the responses.

nodejs boilerplate 7

  • api-error-handler.middleware.ts handle all error responses.
  • authenticate.middleware.ts verify cookies to make sure that user is logged.
  • joi-error-handler.middleware.ts handle all error validations that use joi library.
  • permission-handler.middleware.ts to check the permission of the current user.

    // api-error-handler.middleware.ts
import HttpStatus from 'http-status-codes';
import express from 'express';

export interface IError {
  status?: number;
  code?: number;
  message?: string;
}

export const notFoundErrorHandler = (
  req: express.Request,
  res: express.Response,
  next: express.NextFunction,
) => {
  res.status(HttpStatus.NOT_FOUND).json({
    success: false,
    error: {
      code: HttpStatus.NOT_FOUND,
      message: HttpStatus.getStatusText(HttpStatus.NOT_FOUND),
    },
  });
}

export const errorHandler = (
  err: IError,
  req: express.Request,
  res: express.Response,
  next: express.NextFunction,
) => {
  res.status(err.status || HttpStatus.INTERNAL_SERVER_ERROR).json({
    success: false,
    error: {
      code: err.code || HttpStatus.INTERNAL_SERVER_ERROR,
      message: err.message || HttpStatus.getStatusText(HttpStatus.INTERNAL_SERVER_ERROR),
    },
  });
}

  
    // authenticate.middleware.ts
import express from 'express';
import httpStatusCodes from 'http-status-codes';

// Services
import userService from '../services/user/user.service';

// Interfaces
import IRequest from '../interfaces/IRequest';

// Utilities
import ApiResponse from '../utilities/api-response.utility';
import Encryption from '../utilities/encryption.utility';
import ApiUtility from '../utilities/api.utility';

// Constants
import constants from '../constants';

export default async (
  req: IRequest,
  res: express.Response,
  next: express.NextFunction,
) => {
  if (constants.APPLICATION.authorizationIgnorePath.indexOf(`${req.originalUrl}`) === -1) {
    const authorizationHeader = ApiUtility.getCookieFromRequest(req, constants.COOKIE.COOKIE_USER);

    if (authorizationHeader) {
      const decoded = await Encryption.verifyCookie(authorizationHeader);

      if (decoded) {
        const user = await userService.getById({ id: decoded.data[constants.COOKIE.KEY_USER_ID] });

        if (user) {
          // @ts-ignore
          req.user = user;
        } else {
          return ApiResponse.error(res, httpStatusCodes.UNAUTHORIZED);
        }
      } else {
        return ApiResponse.error(res, httpStatusCodes.UNAUTHORIZED);
      }
    } else {
      return ApiResponse.error(res, httpStatusCodes.FORBIDDEN);
    }
  }

  next();
};

  
    // joi-error-handler.middleware.ts
import express from 'express';
import * as HttpStatus from 'http-status-codes';
import { IError } from './api-error-handler.middleware';

interface IJoiErrorDetail {
  message?: string;
  path?: string;
}

interface IJoiError extends IError {
  isJoi?: boolean;
  // tslint:disable-next-line: prefer-array-literal
  details?: Array<IJoiErrorDetail>;
}

export default (
  err: IJoiError,
  req: express.Request,
  res: express.Response,
  next: express.NextFunction,
) => {
  if (err.isJoi) {
    const error = {
      code: HttpStatus.BAD_REQUEST,
      message: HttpStatus.getStatusText(HttpStatus.BAD_REQUEST),
      details:
        err.details &&
        err.details.map(err => ({
          message: err.message,
          param: err.path,
        })),
    };
    return res.status(HttpStatus.BAD_REQUEST).json(error);
  }

  return next(err);
};

  
    // permission-handler.middleware.ts
import express from 'express';
import httpStatusCodes from 'http-status-codes';

// Interfaces
import IRequest from '../interfaces/IRequest';

// Utilities
import ApiResponse from '../utilities/api-response.utility';

export const isAdmin = () => {
  return async (req: IRequest, res: express.Response, next: express.NextFunction) => {
    if (req.user.email !== 'admin@gmail.com') {
      return ApiResponse.error(res, httpStatusCodes.UNAUTHORIZED);
    }
    next();
  };
};

  

Recent Blogs