NodeJS service structure in Typescript with TypeORM

rishabh gandhi
11 min readApr 26, 2022

Hello Dev, this blog is written in the context of creating a Node.JS service code structure using Express.JS in Typescript with TypeORM

Let us start by creating a service directory called “node-typeorm-service”

  • Initialize it by running “npm init” in the command prompt. Few options will be prompted, answer accordingly and complete the process. You can refer to the below screenshot.
  • Add the below content to your package.json file,

{
“name”: “typeorm-service”,
“version”: “1.0.0”,
“description”: “this service is connected to database and perform different operations on it”,
“main”: “index.js”,
“scripts”: {
“dev”: “ts-node-dev — respawn — transpile-only ./src/server/index.ts”,
“prod”: “node ./src/server/index.js”,
“check”: “gts check”,
“clean”: “gts clean”,
“compile”: “tsc -p .”,
“fix”: “gts fix”,
“prepare”: “npm run compile”,
“pretest”: “npm run compile”,
“posttest”: “npm run check”
},
“author”: “”,
“license”: “ISC”,
“dependencies”: {
“compression”: “¹.7.4”,
“cors”: “².8.5”,
“date-fns”: “².28.0”,
“express”: “⁴.17.1”,
“express-http-context”: “¹.2.3”,
“helmet”: “³.20.1”,
“http-status-codes”: “¹.3.2”,
“jsonwebtoken”: “⁸.5.1”,
“makeerror”: “¹.0.11”,
“moment”: “².29.3”,
“pg”: “⁸.3.3”,
“reflect-metadata”: “⁰.1.13”,
“spdy”: “⁴.0.2”,
“supports-color”: “6.1.0”,
“swagger-jsdoc”: “³.4.0”,
“swagger-ui-express”: “⁴.1.1”,
“typeorm”: “⁰.2.22”,
“winston”: “³.2.1”,
“winston-daily-rotate-file”: “⁴.2.0”,
“winston-splunk-httplogger”: “².3.1”
},
“devDependencies”: {
@types/compression”: “¹.0.1”,
@types/express”: “⁴.17.0”,
@types/helmet”: “⁰.0.44”,
@types/jest”: “²⁴.0.17”,
@types/node”: “¹².7.2”,
@types/spdy”: “³.4.4”,
@types/swagger-jsdoc”: “³.0.2”,
@types/swagger-ui-express”: “³.0.1”,
@types/validator”: “¹⁰.11.3”,
“dotenv”: “⁸.1.0”,
“gts”: “³.1.0”,
“husky”: “³.1.0”,
“lint-staged”: “¹⁰.0.0–1”,
“prettier”: “1.18.2”,
“ts-node-dev”: “¹.0.0-pre.41”,
“typescript”: “³.5.3”
},
“lint-staged”: {
“src/**/*.ts”: [
“npm run fix”,
“npm run check”,
“git add”
]
}
}

  • Create a tslint.json file and add the below content,

{
“extends”: “gts/tslint.json”,
“linterOptions”: {
“exclude”: [
“**/*.json”
]
}
}

  • Create a tsconfig.json file and add the below content,

{
“extends”: “./node_modules/gts/tsconfig-google.json”,
“compilerOptions”: {
“lib”: [
“es5”,
“es6”
],
“target”: “es5”,
“module”: “commonjs”,
“moduleResolution”: “node”,
“rootDir”: “.”,
“outDir”: “build”,
“esModuleInterop”: true,
“emitDecoratorMetadata”: true,
“experimentalDecorators”: true,
“sourceMap”: true
},
“roots”: [
“/src”
],
“include”: [
“src/**/*.ts”
],
“exclude”: [
“**/__mocks__/*”
]
}

  • Create a prettier.config.js file and add the below content,

module.exports = {
singleQuote: true,
trailingComma: ‘es5’,
};

  • Create a .env file and the below content,

# Application Port
PORT = 3001
APP_VERSION = v1

# Application Certificate
CERT_KEY =
CERT_FILE =

# Postges DB configurations
DB_HOST = localhost
DB_USERNAME = postgres
DB_PASSWORD = postgres
DB_PORT = 5432
DB_DATABASE = postgres
DB_SCHEMA = public
DB_SYNCHRONIZE = true
DB_LOGGING = false
DB_ENTITIES = src/server/dao/entity/**/*.ts
DB_MIGRATIONS = src/migration/entity/**/*.ts
DB_MIGRATION_RUN = false

# Application Variables
LOG_LEVEL = debug

  • Create an ormconfig.js file and add the below content,

module.exports = {
type: ‘postgres’,
host: process.env.DB_HOST,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
schema: process.env.DB_SCHEMA,
port: Number(process.env.DB_PORT),
// comment below line for local development
// ssl: {
// rejectUnauthorized: false,
// },
synchronize: process.env.DB_SYNCHRONIZE === ‘true’,
logging: process.env.DB_LOGGING === ‘true’,
entities: [process.env.DB_ENTITIES || ‘’],
}

  • After completing the above steps, the folder should have similar files as shown below,
  • Now, let's start creating folders as shown below,
  • First, we’ll add code to make the server up and running.
  • Create app.ts file under the “server/config” folder and paste the below-provided content

import express = require(‘express’);
import helmet from ‘helmet’; // Security
import compression from ‘compression’; // compresses requests
import { vars } from ‘./vars’;

const logger = require(‘../logger’);
const cors = require(‘cors’);

// Create a new express application instance
const app: express.Application = express();
const className = ‘[App]’;
app.use(helmet());
app.use(compression());

app.use(express.json());
app.use(express.urlencoded());

// Enable Cross Origin Resource Sharing
app.use(
cors({
origin: ‘*’,
})
);

export const startServer = (cb: Function, port?: number) => {
let server = null;
const methodName = ‘[startServer]’;
logger.info(className + methodName + ‘start’);

// start express server
port = port ? port : Number(vars.port);
server = (server || app).listen(port, () => {
logger.info(‘Service: Listening on port ‘ + port + ‘!’);
});
cb(server);
};

  • Then, add the below code to the index.ts file in the “server” folder

// import .env variables
process.env[‘NODE_TLS_REJECT_UNAUTHORIZED’] = ‘0’;
require(‘dotenv’).config();

import ‘reflect-metadata’;
import { Server } from ‘https’;
import { startServer } from ‘./config/app’;

startServer(async (server: Server) => {
process.on(‘SIGINT’, () => {
server.close(() => {
logger.info(‘Service: Finished all requests’);
});
});
});

  • Let’s now create a database table and then we’ll add database connection code with the service
  • Under dao/entity folder, create the entity file “user.details.entity.ts” as and add the content shown below,

import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from ‘typeorm’;

@Entity({ name: ‘user_details’ })
export class UserDetails {
@PrimaryGeneratedColumn()
id!: number;

@Column({ name: ‘name’, length: 100 })
name!: string;

@Column({ name: ‘email_id’, length: 500, unique: true })
emailId!: string;

@Column({ name: ‘role’, length: 500, default: ‘user’ })
role!: string;

@Column({ name: ‘created_by’, default: 1 })
createdBy!: number;

@Column({ name: ‘updated_by’, default: 1 })
updatedBy!: number;

@CreateDateColumn({
name: ‘created_at’,
type: ‘timestamp’,
default: () => ‘NOW()’,
})
createdAt!: Date;

@UpdateDateColumn({
name: ‘updated_at’,
type: ‘timestamp’,
default: () => ‘NOW()’,
})
updatedAt!: Date;
}

NOTE: For TypeORM related queries, you can refer here

  • Again open the index.ts file and replace the code with the below one,

// import .env variables
process.env[‘NODE_TLS_REJECT_UNAUTHORIZED’] = ‘0’;
require(‘dotenv’).config();

import ‘reflect-metadata’;
import { Server } from ‘https’;
import { createConnection } from ‘typeorm’;
import { startServer } from ‘./config/app’;
import { vars } from ‘./config/vars’;

// Create database connection
createConnection()
.then(async connection => {
// Start Express server
startServer(async (server: Server) => {
process.on(‘SIGINT’, () => {
server.close(() => {
logger.info(‘Service: Finished all requests’);
});
});
// this will sync the database again with updated entity models.
// data will be lost
// For production application, avoid this and use migration scripts
if (vars.dbSync) {
await connection.synchronize(true);
}
});
})
.catch(error => {
logger.error(‘Error: [Typeorm-createConnection] ‘ + error);
});

  • You can see that, we’ve added a database connection method at server start so we can use that instance. The connection method will take connection details from the ormconfig.js file that we’ve created at the root level.
  • Now we’ll add logging in to our service. So for that, create index.ts file under the server/logger folder and paste the below code,

‘use strict’;

// Require modules
const uuidv1 = require(‘uuid/v1’);
const path = require(‘path’);
const winston = require(‘winston’);
const { format } = require(‘winston’);
const { combine, timestamp, printf, prettyPrint } = format;
require(‘winston-daily-rotate-file’);
import { vars } from ‘../config/vars’;
const logDirectory = path.join(__dirname, ‘/logs/winston’);

const transports = [];

const consoleLog = new winston.transports.Console({
name: ‘debug-console’,
level: vars.logLevel,
handleExceptions: false,
json: false,
colorize: true,
});
transports.push(consoleLog);

const fileLog = new winston.transports.DailyRotateFile({
level: vars.logLevel,
filename: `${logDirectory}/%DATE%.log`,
datePattern: ‘YYYY-MM-DD’,
handleExceptions: true,
zippedArchive: true,
colorized: true,
});

transports.push(fileLog);

// eslint-disable-next-line
const formatMessage = printf(
(info: { timestamp: string; level: string; message: string }) => {
const reqId = uuidv1();
return JSON.stringify({
reqId,
timestamp: info.timestamp,
level: info.level,
message: info.message,
});
}
);

const logger = new winston.createLogger({
format: combine(timestamp(), prettyPrint(), formatMessage),
transports,
exitOnError: false,
});

module.exports = logger;

  • Here I’ve tried to use Winston logging, once service will start, you’ll be able to see logs under the logs folder of logger
  • Create vars.ts file under config folder and paste the below code,

export const vars = {
port: process.env.PORT || 3001,
appVersion: process.env.APP_VERSION || ‘v1’,
dbUserName: process.env.DB_USERNAME,
certKey: process.env.CERT_KEY || ‘’,
certFile: process.env.CERT_FILE || ‘’,
baseUrl: process.env.BASE_URL || ‘’,
logLevel: process.env.LOG_LEVEL || ‘info’,
stage: process.env.STAGE || ‘dev’,
requestIdHeader: ‘x-request-id’,
queryRunnerType: process.env.QUERY_RUNNER_TYPE || ‘postgres’,
dbSync: process.env.DB_SYNCHRONIZE === ‘true’
};

  • Once, all the steps are done, we can run our service without any error
  • Let’s now create a few APIs to create/update/fetch a user/userlist. For that, we’ll need to create a route, controller, service, and dao files.

Let's get started…

  • Create a user.details.router.ts file under server/route/v1 folder and paste the below content,

import express from ‘express’;
import { UserDetailsController } from ‘../../controller/user.details.controller’;
export const userRoutes: express.Router = express.Router();

const userController = UserDetailsController.createInstance();

/**
* @swagger
* components:
* schemas:
* UserModel:
* properties:
* id:
* type: number
* name:
* type: string
* email:
* type: string
* role:
* type: string
* createdAt:
* type: timestamp
* updatedAt:
* type: timestamp
* createdBy:
* type: number
* updatedBy:
* type: number
* CreateUserRequest:
* properties:
* name:
* type: string
* email:
* type: string
* role:
* type: string
* UpdateUserRequest:
* properties:
* name:
* type: string
* email:
* type: string
* role:
* type: string
* UserCreationResponse:
* properties:
* status:
* type: boolean
* message:
* type: string
* result:
* $ref: “#/components/schemas/UserModel”
* UserListResponse:
* properties:
* status:
* type: boolean
* message:
* type: string
* result:
* type: object
* properties:
* users:
* type: array
* items:
* $ref: ‘#/components/schemas/UserModel’
* total:
* type: number
* hasNext:
* type: boolean
* hasPrev:
* type: boolean
* TokenExpiredResponse:
* properties:
* status:
* type: boolean
* message:
* type: string
* example: ‘Token is expired. Please login again. / Token is not valid. Unauthorized Access.’
*/

/**
* Create User
*/

/**
* @swagger
*
* /api/user:
* post:
* tags:
* — User
* description: Create new user entry
* produces:
* — application/json
* requestBody:
* description: “JSON Payload”
* required: true
* content:
* application/json:
* schema:
* $ref: “#/components/schemas/CreateUserRequest”
* responses:
* 200:
* description: Result with object of user details
* content:
* application/json:
* schema:
* $ref: ‘#/components/schemas/UserCreationResponse’
* 401:
* description: Token Expired / Unauthorized Access
* content:
* application/json:
* schema:
* $ref: ‘#/components/schemas/TokenExpiredResponse’
* 500:
* description: Internal Server Error
*/
userRoutes
.route(‘/user’)
.post(userController.create);

/**
* Update User
*/

/**
* @swagger
*
* /api/user/{userId}:
* put:
* tags:
* — User
* description: Update user details
* produces:
* — application/json
* parameters:
* — in: “path”
* name: “userId”
* description: “user details unique id”
* required: true
* requestBody:
* description: “JSON Payload”
* required: true
* content:
* application/json:
* schema:
* $ref: “#/components/schemas/UpdateUserRequest”
* responses:
* 200:
* description: Result with object of updated user details
* content:
* application/json:
* schema:
* $ref: ‘#/components/schemas/UserCreationResponse’
* 401:
* description: Token Expired / Unauthorized Access
* content:
* application/json:
* schema:
* $ref: ‘#/components/schemas/TokenExpiredResponse’
* 500:
* description: Internal Server Error
*/
userRoutes
.route(‘/user/:userId’)
.put(userController.update);

/**
* Fetch User List
*/

/**
* @swagger
*
* /api/user:
* get:
* tags:
* — User
* description: Fetch user list with pagination
* produces:
* — application/json
* parameters:
* — name: “pageSize”
* in: “query”
* description: “number of records to be fetched per request”
* default: 20
* — name: “pageNo”
* in: “query”
* description: “records to be fetched from any page skipping records”
* default: 0
* — name: “sortOrder”
* in: “query”
* description: “records to sort in ASC or DESC order”
* default: ASC
* responses:
* 200:
* description: Result with User List and pagination details
* content:
* application/json:
* schema:
* $ref: ‘#/components/schemas/UserListResponse’
* 401:
* description: Token Expired / Unauthorized Access
* content:
* application/json:
* schema:
* $ref: ‘#/components/schemas/TokenExpiredResponse’
* 500:
* description: Internal Server Error
*/
userRoutes
.route(‘/user’)
.get(userController.fetch);

  • Create index.ts file under server/route folder and paste the below content,

import express from ‘express’;

import { vars } from ‘../config/vars’;

export const router = express.Router();

import(‘./’ + vars.appVersion + ‘/user.details.router’).then(route =>
router.use(route.userRoutes)
);

  • Here, we’ve configured route to use userRoutes. Now, we need to pass the route object to the app instance
  • For that, go to the app.ts file in server/config folder and paste the below line of codes before the startServer method,

// Mount API routes
app.use(‘/api’, router);

  • Now, create user.details.controller.ts file in controller folder and paste the below code,

import { Request, Response } from ‘express’;
import { UserDetailsService } from ‘../service/user.details.service’;
import { HttpResponse } from ‘../utility/http.response’;
import httpStatusCodes from ‘http-status-codes’;
import { SearchQuery } from ‘../type/requests/search.query.type’;
import { SortOrder } from ‘../enum/sort.order.enum’;
import { UserDetails } from ‘../dao/entity/user.details.entity’;

const user = new UserDetails()
user.id = 1

const logger = require(‘../logger’);
const className = ‘[UserDetailsController]’;
export class UserDetailsController {
static createInstance() {
return new UserDetailsController();
}

async create(request: Request, response: Response) {
const input = request.body;
const methodName = ‘[create]’;
logger.info(className + methodName + ‘start’);
logger.debug(
className + methodName + ‘ Request body to create user ‘ + JSON.stringify(input)
);
const userDetailsService = await UserDetailsService.createInstance();
const result = await userDetailsService.save(input, user);
HttpResponse.setResponse(
response,
true,
httpStatusCodes.OK,
‘’,
‘’,
result
);
}

async update(request: Request, response: Response) {
const methodName = ‘[update]’;
logger.info(className + methodName + ‘start’);
const input = request.body;
const userId: number = Number(request.params.userId);
logger.debug(
className +
methodName +
‘ Request body to update user :: ‘ +
JSON.stringify(input) +
‘ for id :: ‘ +
userId
);
const userDetailsService = await UserDetailsService.createInstance();
const result = await userDetailsService.update(input, userId, user);
HttpResponse.setResponse(
response,
true,
httpStatusCodes.OK,
‘’,
‘’,
result
);
}

async fetch(request: Request, response: Response) {
const methodName = ‘[fetch]’;
const queryRequest = request.query;
logger.info(className + methodName + ‘start’);
logger.debug(
className +
methodName +
‘ Task to be fetched for search query :: ‘ +
JSON.stringify(queryRequest)
);
const userDetailsService = await UserDetailsService.createInstance();
const searchQuery = {} as SearchQuery;
searchQuery.pageNo = queryRequest.pageNo
? Number(queryRequest.pageNo)
: undefined;
searchQuery.pageSize = queryRequest.pageSize
? Number(queryRequest.pageSize)
: undefined;
searchQuery.sortBy = queryRequest.sortBy
? (queryRequest.sortBy as string)
: undefined;
searchQuery.sortOrder = queryRequest.sortOrder
? (queryRequest.sortOrder as SortOrder)
: undefined;
const result = await userDetailsService.fetch(searchQuery);
HttpResponse.setResponse(
response,
true,
httpStatusCodes.OK,
‘’,
‘’,
result
);
}
}

  • Now, create user.details.service.ts file in service folder and paste the below code,

import { Request } from ‘express’;
import { UserDetailsDAO } from ‘../dao/user.details.dao’;
import { UserDetails } from ‘../dao/entity/user.details.entity’;
import { SearchQuery } from ‘../type/requests/search.query.type’;
import { SortOrder } from ‘../enum/sort.order.enum’;
const logger = require(‘../logger’);
export class UserDetailsService {
static async createInstance() {
return new UserDetailsService();
}

static CLASS_NAME = ‘[UserDetailsService]’;
private UserDetailsDAO = UserDetailsDAO.createInstance();

async save(userDetails: UserDetails, user: UserDetails) {
const methodName = ‘[save]’;
logger.debug(
UserDetailsService.CLASS_NAME +
methodName +
‘ start: Creating user :: ‘ +
JSON.stringify(userDetails)
);
userDetails.createdBy = user.id;
userDetails.updatedBy = user.id;
return this.UserDetailsDAO.save(userDetails);
}

async update(userDetails: UserDetails, userId: number, user: UserDetails) {
const methodName = ‘[update]’;
logger.debug(
UserDetailsService.CLASS_NAME +
methodName +
‘ start: Updating user for userId :: ‘ +
userId
);
userDetails.id = userId;
userDetails.updatedAt = new Date();
userDetails.updatedBy = user.id;
return this.UserDetailsDAO.save(userDetails);
}

async fetch(searchQuery: SearchQuery) {
const methodName = ‘[fetch]’;
logger.debug(
UserDetailsService.CLASS_NAME +
methodName +
‘start: Fetch Task for search query :: ‘ +
JSON.stringify(searchQuery)
);

const limit = searchQuery.pageSize || 10;
const pageNo = searchQuery.pageNo || 0;
const sort = searchQuery.sortBy || ‘’;
const order = searchQuery.sortOrder || SortOrder.ASC;
const skip = pageNo * limit;

const [users, count] = await this.UserDetailsDAO.findByPage(skip, limit, {
sort,
order,
});
const totalPages = Math.ceil(count / limit);
const hasNext = pageNo + 1 !== totalPages;
const hasPrev = pageNo + 1 > 1;
return {
users,
total: count,
hasNext,
hasPrev,
};
}
}

  • Now, create user.details.dao.ts file in dao folder and paste the below code,

import { getRepository, SelectQueryBuilder } from ‘typeorm’;
import { SortAndOrderType } from ‘../type/sql.sort.order.by.type’;
import { UserDetails } from ‘./entity/user.details.entity’;

const logger = require(‘../logger’);

const className = ‘[UserDetailsDAO]’;

export class UserDetailsDAO {
static createInstance() {
return new UserDetailsDAO();
}

private userDetailsRepository = getRepository(UserDetails);

async save(userDetails: UserDetails) {
const methodName = ‘[save]’;
logger.debug(
className +
methodName +
‘start: save user details :: ‘ +
JSON.stringify(userDetails)
);

return this.userDetailsRepository.save(userDetails);
}

async update(userDetails: UserDetails) {
const methodName = ‘[update]’;
logger.debug(
className +
methodName +
‘start: update user details for id :: ‘ +
userDetails.id
);

return this.userDetailsRepository.save(userDetails);
}

async findByPage(
skip: number,
limit: number,
sortAndOrder: SortAndOrderType
) {
const methodName = ‘[findByPage]’;
logger.debug(
className +
methodName +
‘start: find user details with limit:: ‘ +
limit +
‘ , skip users :: ‘ +
skip +
‘ , sort by :: ‘ +
sortAndOrder.sort +
‘ and order by :: ‘ +
sortAndOrder.order
);

const sqb: SelectQueryBuilder<UserDetails> = this.userDetailsRepository
.createQueryBuilder(‘userDetails’)
.orderBy(‘userDetails.createdAt’, sortAndOrder.order);

return sqb
.skip(skip)
.take(limit)
.getManyAndCount();
}
}

  • Create sql.sort.order.by.type.ts file inside type folder and paste the below code,

export interface SortAndOrderType {
sort: string;
order: ‘ASC’ | ‘DESC’;
}

  • Create search.query.type.ts inside type/requests folder and paste the below code,

import { SortOrder } from ‘../../enum/sort.order.enum’;

export interface SearchQuery {
pageNo: number | undefined;
pageSize: number | undefined;
sortBy: string | undefined;
sortOrder: SortOrder.ASC | SortOrder.DESC | undefined;
fromDate: string | undefined;
toDate: string | undefined;
}

  • Create sort.order.enum.ts in enum folder and paste the below code,

export enum SortOrder {
ASC = ‘ASC’,
DESC = ‘DESC’,
}

  • Now try to run the service again and check.
  • You should be able to perform create/update or fetch user list operations with this code.

Thanks for reading.

You can add comments if you are facing any issues in resolving the code.

Will be happy to help!!!

--

--