In this article I will be explaining how to write a simple express server that is of Production Grade.
As you might know that a simple Express server running a Hello World code looks something like this –
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => res.send('Hello World!'))
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
Putting everything in the
index.js
file is alright for a small hobby project and for development in a local environment, however this is not how the professionals work.Instead a Hello World server written by a professional might look something like this –
Yes, this huge project structure only to run a simple Hello World !
In this article I will be explaining how you can too write something like this, that is Production ready, and thus level up on your NodeJS skills. I will be using Typescript for this project, and I encourage you all to do the same! Because, it makes our code Object Oriented as well as easy to understand!
1. Create a
package.json
file by running the command npm init
2. Install Typescript on your system globally
npm i typescript -g
npm i ts-node -g
3. Create a tsconfig.json file for the project by running the command
tsc --init
4. Install the Other Packages –
npm i express cors @types/express
npm i winston @types/winston
npm i module-alias
The package.json file looks something like this –
{
"name": "productionserver",
"version": "1.0.0",
"description": "A Production Ready Server",
"main": "./build/index.js",
"scripts": {
"test": "mocha -r ts-node/register tests/**/*.test.ts",
"start": "ts-node index.ts",
"coverage": "nyc -r lcov -e .ts -x \"*.test.ts\" npm run test"
},
"author": "Dhairya Gada",
"license": "ISC",
"dependencies": {
"@types/express": "^4.17.3",
"@types/winston": "^2.4.4",
"cors": "^2.8.5",
"express": "^4.17.1",
"module-alias": "^2.2.2",
"winston": "^3.2.1"
},
"_moduleAliases": {
"@root": ".",
"@utils": "build/src/utils",
"@configs": "build/configs"
},
"devDependencies": {
}
}
And tsconfig.json file is as follows –
{
"compilerOptions": {
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": [
"es5",
"es6",
"dom"
],
"sourceMap": true, /* Generates corresponding '.map' file. */
"outDir": "./build", /* Redirect output structure to the directory. */
"strict": true, /* Enable all strict type-checking options. */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
"resolveJsonModule": true,
"paths": {
"@utils/*": [
"src/utils/*"
],
"@configs/*": [
"configs/*"
],
}
},
}
Before we start developing the application it is always recommended to plan the structure of our code. The code should be organized in such a fashion that it enables you to tackle the following two “if’s” gracefully –
1. IF the requirements change
You should be able to make revisions to the program quite easily. That is without disturbing the harmony of the existing code and with minimal changes !
2. IF new requirements are introduced
You should be able to build on top of the prevailing code, re-using components wherever possible. Without making any modifications to the already existing code . That is extending without modifying !
I know that it is quite impossible to be build a structure with such robustness and flexibility, but we should always aim for it. Thus making our code as neat as possible.
1. S.O.L.I.D PRINCIPLES
We can plan our code structure to be clutter-free by following the S.O.L.I.D. principles of Software Design.
S – Single Responsibility
The Functions and classes should have only one responsibility. That is they should have only a single reason to exist.
O – Open to Extension Closed to Modification
A Class should be Open to Extension but closed to modification. This can be achieved by the use of Abstract Classes. But there is a catch here! The parent class and the derived class should be such that they satisfy the next principle i.e The Liskov Substitution Principle. Otherwise interfaces should be used.
L – Liskov Substitution Principle
The statement of Liskov Substitution Principle says that the Instance of a Base Class should be replaceable by the instance of its Derived class. This basically means that the derived class should not introduce a behavioral change in any of it’s methods !
Honestly I found this principle a little tough to understand, however the following YouTube video made it clear to me!
I – Interface Segregation
The interfaces should not have any elements that their clients do not use. This can be achieved my making multiple smaller meaningful interfaces, rather than a huge one that is only partially helpful.
D – Dependency Inversion
The interaction between the higher level class and the lower level class should be abstract. And the details of interaction should depend on the abstraction.
You can read more on the SOLID Principles from the following resources –
Brutally Solid TypeScript
Solid Principles Made Easy
Is Your Code Solid Enough
2 . APPLICATION LOGIC AND BUSINESS LOGIC SHOULD NEVER BE MIXED
.
├── docs
│ └── Readme.md
├── logs
│ └── app.log
├── node_modules
│ └── …
├── src
│ ├── constants
│ │ └── StatusConstants.ts
│ ├── core
│ │ ├── InitializeMiddleware.ts
│ │ ├── InitializeRoutes.ts
│ │ └── Server.ts
│ ├── middleware
│ │ ├── CommonMiddleware.ts
│ │ └── ErrorHandlingMiddleware.ts
│ ├── routes
│ │ ├── helloworld
│ │ │ └── HelloWorldRouteController.ts
│ │ └── AbstractRouteController.ts
│ ├── serviceclasses
│ │ └── helloworld
│ │ └── HelloWorld.ts
│ └── utils
│ └── logger
│ └── Logger.ts
├── tests
│ ├── integration
│ └── unit
│
├── configs
│ ├── LoggerConfig.json
│ └── ServerConfig.json
│
├── index.ts
├── package-lock.json
├── package.json
└── tsconfig.json
└── ecosystem.config.json
Basically there are three main folder –
└──src
All the Code related to project is maintained inside the src folder
└──test
All the Code related to the testing is maintained inside the tests folder
└──configs
All the Configurations related to the project are maintained inside the configs folder.
Apart from these three the build folder is where the compiled JavaScript Code resides. This Folder and its contents are automatically created by the typescript compiler.
We want the host and port numbers of our Web Server to be configurable and not hard-coded. Therefore we maintain a JSON file with host and port numbers, from where these two values are loaded.
Here is the ServerConfig JSON that I have created for our webserver.
{
"host":"localhost",
"port":4000
}
The folder structure of
src
folder is something like –├── core
│
├── middleware
│
├── routes
│
├── serviceclasses
│
├── utils
│ └── logger
│
├── constants
The Code inside src folder can be distinguished into two categories – Application Logic Code and Business Logic Code.
BUSINESS LOGIC CODE
Business Logic Code resides in the folder serviceclasses
In serviceclasses folder we have all the code pertaining to the actual processes that need to be carried out . These logic keeps changing as per the business requirements.
For the purpose of our Hello World Server let us assume that the business activity that needs to be carried out is returning a message string of Hello World to the user.
Therefore I have created a class and a method for the same. I create a folder called helloworld and inside it a class HelloWorld.ts.
HelloWorld.ts
export class HelloWorld{
public static async wishHello():Promise<string>{
let resp = `Hello World!`
return Promise.resolve(resp)
}
}
Note that how the above code follows the principle of the Single Responsibility from the Solid Principles. The above piece of code has no relation to starting and running the Web-Server or the routes whatsoever. It only exists for a single purpose i.e. to return the string Hello World.
The route that calls this business activity is a part of the application logic. Therefore even if the business activity is modified to wishing Hello Universe instead of Hello World the application logic will remain untouched!
Application Logic Code
core , middleware, routes ,utils and constants are the folders among which the Application Logic Code is distributed.
In this folder we will be storing constants that will be used throughout the project. The advantage of creating a separate space for the constants is that they can be used repetitively in the entire project and if any rare scenario occurs where the value needs to be changed, you do not need to go to every class that has used it and separately change it .
For the purpose of this project I have created only a single class of constants i.e. status constants –
StatusConstants.ts
export class StatusConstants{
public static code404 = 404
public static code404Message = "Method Not Found"
public static code200 = 200
}
Now imagine that we have directly used the string "Method Not Found" as a 404 Error Message at several places in our code. And then due to some reason you need to change the message to something else. For that you will need to go to every single code location where you have written "Method Not Found" and change it to your new message ! This all efforts are saved by using a constants file and invoking the message wherever required.
Utils folder is where Commonly Used utilities can be placed. Such as String Utilities, Numerical Functions, Error Handling Utilities, Logging Utilities etc
I have created a Logging Utility that we will be using in this project. For that I have created a Logger Class inside the logger folder in utils.
logger/Logger.ts
import winston from 'winston'
import {options} from '@configs/LoggerConfig.json'
export class Logger {
private logger: winston.Logger
private static instance: Logger
private constructor() {
this.logger = winston.createLogger({
transports: [
new winston.transports.Console(options.console),
new winston.transports.File(options.file)
]
})
}
public static getLoggerInstance(){
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance
}
public static getLogger(){
let _logger = Logger.getLoggerInstance()
return _logger.logger
}
}
I have used an NPM package for Winston Logger to create this utility. This package can be installed by running the command
npm i winston @types/winston
.The configuration that this logger needs is stored in the configs folder that I had discussed above.
configs/LoggerConfig.json
{
"options": {
"file": {
"level": "info",
"filename": "../logs/app.log",
"handleExceptions": true,
"json": true,
"maxsize": 5242880,
"maxFiles": 5,
"colorize": true
},
"console": {
"level": "debug",
"handleExceptions": true,
"json": false,
"colorize": true
}
}
}
One more Important thing to note about the Logger Class is that it is Singleton. Because we can use the same instance every time it is called, there is no reason for multiple instances to exist!
You can read more about the Singleton Classes from here –
https://refactoring.guru/design-patterns/singleton
This is the folder where all the routes of our express server are declared. Note that they are just declared here and not initialized. Initialization of this routes is a different responsibility, hence it is placed in a different folder in a new class. I will be explaining the initialization of the routes when I explain the core folder.
The route declarations basically points an API path to it’s corresponding business activity logic which is placed in the serviceclasses folder.
Here we have an Abstract Base Class known as AbstactRouteController.ts , all the Specific Route Controllers are derived from this Base Class.
AbstractRouteController.ts
import express = require("express");
export abstract class AbstractRouteController {
router = express.Router();
path!: string;
public async InitializeController(link: string) {
console.log(link + this.path)
await this.InitializeGet()
await this.InitializePost()
}
public async runService(req: express.Request, resp: express.Response): Promise<any> {
resp.send('runService Method for ' + this.path + 'does not exist !')
}
public async InitializeGet(){
this.router.get(this.path, this.runService.bind(this)).bind(this)
}
public async InitializePost(){
this.router.post(this.path, this.runService.bind(this)).bind(this)
}
}
I create a new folder called helloworld inside which I set up the Route Controller for the helloworld API.
helloworld /HelloWorldRouteController.ts
import { AbstractRouteController } from "../AbstractRouteController";
import {Response,Request} from 'express'
import { HelloWorld } from "../../serviceclasses/helloworld/HelloWorld";
import { StatusConstants } from "../../constants/StatusConstants";
export class HelloWorldRouteController extends AbstractRouteController {
constructor(link:string){
super();
this.path = '/helloworld';
this.InitializeController(link);
}
public async runService(req: Request, resp: Response):Promise<any>{
let response = await HelloWorld.wishHello()
resp.status(StatusConstants.code200).send(response)
}
}
As it can be seen that when the API /helloworld is called, using either get or post method, the corresponding Route Controller i.e . the HelloWorldRouteController calls it’s respective business activity i.e. HelloWorld.wishHello() and returns its response.
Also, note that how the code follows Open-Closed principle. The code for a new route controller is created by extending an existing abstract base class, without modifying any prevailing code. The parent class and the child classes satisfy the Liskov Substitution principle because the base class and the derived class have the same behavior and can be substituted for each other!
As the name of the folder suggests, this is where the middleware components are defined.
Following is the code for common middleware –
CommonMiddleware.ts
import { Express } from 'express'
import { Logger } from '../utils/logger/Logger';
let bodyParser = require('body-parser')
let cors = require('cors');
export class CommonMiddleware {
app: Express
constructor(_app: Express) {
this.app = _app
}
public async useBodyParser() {
this.app.use(bodyParser.json());
}
public async useURLencoded() {
this.app.use(
bodyParser.urlencoded({
extended: true
})
);
}
public async useCors() {
this.app.use(cors());
}
public async logRequests() {
let logger = Logger.getLogger()
this.app.use((req, res, done) => {
logger.info(req.originalUrl);
done();
});
}
}
Note that how the logger class is used to log all the incoming requests with the help of express middleware!
I have also created a separate class for the ErrorHandlingMiddleware –
ErrorHandlingMiddleware.ts
import { Express } from 'express'
import { Response, Request } from 'express'
import { StatusConstants } from '../constants/StatusConstants'
export class ErrorHandlingMiddleware {
app: Express
constructor(_app: Express) {
this.app = _app
}
public async handle404Error() {
this.app.use((req: Request, resp: Response) => {
resp.status(StatusConstants.code404).send(StatusConstants.code404Message)
})
}
}
Core folder is where everything starts to come together. It is where the most important classes with respect to the starting the application are stored.
Initializing the middlewares –
InitializeMiddleware.ts
import { Express } from 'express'
import { CommonMiddleware } from '../middleware/CommonMiddleware'
import { ErrorHandlingMiddleware } from '../middleware/ErrorHandlingMiddleware'
export class InitializeMiddleWare{
public static async InitializeCommonMiddleware(app :Express){
let middleware = new CommonMiddleware(app)
await middleware.useBodyParser()
await middleware.useURLencoded()
await middleware.useCors()
}
public static async InitializeErrorHandlingMiddleware(app :Express){
let errorMiddleware = new ErrorHandlingMiddleware(app)
await errorMiddleware.handle404Error()
}
}
Initializing the Route Controllers that we had written in the Routes folder –
import { Express } from 'express'
import { HelloWorldRouteController } from '../routes/helloworld/HelloWorldRouteController'
import { AbstractRouteController } from '../routes/AbstractRouteController'
export class InitializeRoutes {
public static async Initialize(app: Express, link: string) {
let routes = await this.getRoutes(link)
routes.forEach(rc => {
app.use("/", rc.router)
})
}
public static async getRoutes(link: string): Promise<Array<AbstractRouteController>> {
let routes: Array<AbstractRouteController> = []
routes.push(new HelloWorldRouteController(link))
return Promise.resolve(routes)
}
}
Note that in this class how Dependency Inversion principle comes into picture. Here details regarding the routes is passed to a Higher Class (Express Module) in the form of an abstraction
rc.router
where rc
is an instance of AbstractRouteController
!Finally the code to start all the middleware, routes and the server itself is written in Server.ts
server.ts
var express = require ('express')
import {Express} from 'express'
import { InitializeMiddleWare } from './InitializeMiddleware';
import { InitializeRoutes } from './InitializeRoutes';
import * as ServerConfig from '@configs/ServerConfig.json'
export async function server() {
let app :Express= express();
let host = ServerConfig.host
let port = ServerConfig.port
let link = "http://" +host+ ":" + port.toString()
await InitializeMiddleWare.InitializeCommonMiddleware(app)
await InitializeRoutes.Initialize(app,link)
await InitializeMiddleWare.InitializeErrorHandlingMiddleware(app)
app.listen(port, host, () => {
console.log(
`Server started listening at ${host} on ${port} port.`
)
})
}
Finally the Index.ts looks in the root folder looks something like this –
require ('module-alias/register')
import { server } from "./src/core/Server";
server()
You might have noticed that at some places the files have been imported with an @ in front of them. Those are Module Aliases.
Module Aliases save us from writing longer paths like ../../configs/ServerConfig.json by simply using @configs/ServerConfig.json
To use Module Aliases we use a package called module-alias which can be installed by the command npm i module-alias.
After Installing this package, the aliases are declared in the tsconfig.json file with the following declarations –
"paths": {
"@utils/*": [
"src/utils/*"
],
"@configs/*": [
"configs/*"
],
}
And in package.json file as –
"_moduleAliases": {
"@root": ".",
"@utils": "build/src/utils",
"@configs": "build/configs"
}
And by adding the following line on the top of the index.ts file –
require ('module-alias/register')
Look up a detailed tutorial on setting up the module aliases here –
https://dev.to/larswaechter/path-aliases-with-typescript-in-nodejs-4353 or
https://medium.com/@caludio/how-to-use-module-path-aliases-in-visual-studio-typescript-and-javascript-e7851df8eeaa
In case you found this post to be informative or helpful do leave a like on it, this encourages me a lot. Also, if you have any suggestions or any questions feel free to leave a comment below!
Click here to visit the code used in this post on GitHub
Previously published at https://dev.dhairyag.com/writing-a-production-ready-express-server-like-a-pro-1/