├── .eslintignore ├── .gitignore ├── development.env ├── src ├── routes │ ├── index.ts │ ├── customer.ts │ └── employee.ts ├── models │ ├── IPerson.ts │ └── employee.ts ├── services │ ├── customer.ts │ ├── employee.ts │ └── store.ts ├── app.ts ├── controllers │ └── employes.ts ├── config.ts └── swagger.json ├── .eslintrc ├── Dockerfile ├── tsconfig.json ├── webpack.config.js ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .env 4 | production.env 5 | *.log 6 | db 7 | data -------------------------------------------------------------------------------- /development.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | HOST=127.0.0.1 3 | PORT=5000 4 | LOG_LEVEL=info 5 | DATA_DIR=data 6 | DB_INMEM=false 7 | DB_URL= -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import employee from './employee' 2 | import customer from './customer' 3 | 4 | export default { 5 | employee, 6 | customer 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/models/IPerson.ts: -------------------------------------------------------------------------------- 1 | export interface IPerson{ 2 | id:string; 3 | 4 | firstName: string; 5 | lastName: string; 6 | email: string; 7 | address: string; 8 | dob: string; 9 | 10 | getAge(): number; 11 | getFullName(): string; 12 | toString(): string; 13 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ] 12 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:current-alpine3.13 2 | 3 | RUN mkdir -p /usr/src/app/dist 4 | WORKDIR /usr/src/app 5 | 6 | # copy bundled code base 7 | COPY dist/api.bundle.js dist 8 | 9 | # copy env var 10 | COPY production.env . 11 | 12 | # env 13 | ENV NODE_ENV=production 14 | 15 | # command to run when the image is initiated 16 | CMD ["node", "dist/api.bundle.js"] 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "resolveJsonModule": true, 6 | "esModuleInterop": true, 7 | "outDir": "dist", 8 | //"sourceMap": true 9 | }, 10 | "files": [ 11 | "./node_modules/@types/node/index.d.ts" 12 | ], 13 | "include": [ 14 | "src/**/*.ts" 15 | ], 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | entry: ["./src/app.ts"], 5 | output: { 6 | filename: 'api.bundle.js', 7 | path: path.resolve(__dirname,"dist") 8 | }, 9 | module: { 10 | rules: [ 11 | { test: /\.tsx?$/, loader: 'ts-loader',exclude: /node_modules/,} 12 | ] 13 | }, 14 | resolve: { 15 | extensions: [".tsx", ".ts", ".js",".json"] 16 | }, 17 | target: 'node', 18 | node: { 19 | __dirname: true 20 | }, 21 | plugins: [] 22 | }; -------------------------------------------------------------------------------- /src/services/customer.ts: -------------------------------------------------------------------------------- 1 | ////// This is just for an example 2 | 3 | import { IPerson } from '../models/IPerson'; 4 | export class Customer implements IPerson{ 5 | custId: string; 6 | firstName: string; 7 | lastName: string; 8 | email: string; 9 | address: string; 10 | dob: string; 11 | id: string; 12 | constructor(custId: string){ 13 | this.custId = custId; 14 | } 15 | toString(){ 16 | return "" 17 | } 18 | 19 | getAge(){ 20 | return 0; 21 | } 22 | 23 | getFullName(){ 24 | return this.firstName + ' ' + this.lastName; 25 | } 26 | } -------------------------------------------------------------------------------- /src/routes/customer.ts: -------------------------------------------------------------------------------- 1 | ////// This is just for an example 2 | 3 | import {Router} from 'express' 4 | 5 | const router = Router(); 6 | 7 | // Create 8 | router.post('/add', (req, res) => { 9 | res.json({message: 'Hello World'}) 10 | }) 11 | 12 | // Update 13 | router.put('/update', (req, res) => { 14 | res.json({message: 'Hello World'}) 15 | }) 16 | 17 | // Retrive 18 | router.get('/', (req, res) => { 19 | res.json({message: 'Hello World'}) 20 | }) 21 | 22 | // Delete 23 | router.delete('/delete', (req, res) => { 24 | res.json({message: 'Hello World'}) 25 | }) 26 | 27 | 28 | export default router; -------------------------------------------------------------------------------- /src/routes/employee.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import EmployeeController from "../controllers/employes"; 3 | 4 | const router = Router(); 5 | 6 | router.post("/", EmployeeController.addEmployee); 7 | 8 | // Update 9 | router.put("/", (req, res) => { 10 | res.json({ message: "Hello World" }); 11 | }); 12 | 13 | // Retrive 14 | router.get("/", EmployeeController.getAllEmployee); 15 | // Retrive 16 | router.get("/:id", EmployeeController.getEmployeeById); 17 | 18 | // Delete 19 | router.delete("/", (req, res) => { 20 | res.json({ message: "Hello World" }); 21 | }); 22 | 23 | export default router; 24 | -------------------------------------------------------------------------------- /src/models/employee.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, Document } from "mongoose"; 2 | 3 | export enum Role { 4 | MANAGER = "MANAGER", 5 | DEVELOPER = "DEVELOPER", 6 | TESTER = "TESTER", 7 | RESEARCHE = "RESEARCHER", 8 | } 9 | 10 | export interface IEmployee extends Document { 11 | empId: string; 12 | id: string; 13 | firstName: string; 14 | lastName: string; 15 | email: string; 16 | address: string; 17 | dob: string; 18 | role: Role; 19 | } 20 | 21 | const EmployeeSchema = new Schema({ 22 | empId: { type: String, required: true }, 23 | id: { type: String, required: false }, 24 | firstName: { type: String, required: true }, 25 | lastName: { type: String, required: true }, 26 | email: { type: String, required: true }, 27 | address: { type: String, required: true }, 28 | dob: { type: String, required: true }, 29 | role: { type: String, required: true, enum: Object.values(Role) }, 30 | }); 31 | 32 | export default mongoose.model("Employees", EmployeeSchema); 33 | -------------------------------------------------------------------------------- /src/services/employee.ts: -------------------------------------------------------------------------------- 1 | import EmployeeModel, {IEmployee, Role} from '../models/employee'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | 4 | export class Employee{ 5 | 6 | empId: string; 7 | id: string; 8 | 9 | firstName: string; 10 | lastName: string; 11 | email: string; 12 | address: string; 13 | dob: string; 14 | role:Role; 15 | 16 | constructor({ firstName = "", lastName = "", email = "", role = "", address = "", dob = "" } = {}){ 17 | this.firstName = firstName ? firstName.trim(): ""; 18 | this.lastName = lastName ? lastName.trim(): ""; 19 | this.email = email; 20 | this.address = address; 21 | this.role = role? Role[role]: ""; 22 | this.dob = dob; 23 | this.empId = uuidv4(); 24 | } 25 | 26 | async addEmployee(): Promise{ 27 | const newEmployee:IEmployee = await EmployeeModel.create({ 28 | ...this 29 | }) 30 | return newEmployee; 31 | } 32 | 33 | async fetchAllEmployees(): Promise>{ 34 | const employeeList:Array = await EmployeeModel.find({}); 35 | return employeeList; 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import routes from './routes'; 3 | import swaggerJsDoc = require('./swagger.json'); 4 | import swaggerUi from 'swagger-ui-express'; 5 | import { PORT, baseUrl, logger, db } from './config'; 6 | import helmet from 'helmet'; 7 | import rateLimit from 'express-rate-limit'; 8 | import xss from 'xss-clean'; 9 | 10 | const app = express(); 11 | 12 | const limiter = rateLimit({ 13 | windowMs: 15 * 60 * 1000, // 15 minutes 14 | max: 100, // limit each IP to 100 requests per windowMs 15 | message: 'Too many requests' // message to send 16 | }); 17 | 18 | 19 | app.use(helmet()) 20 | app.use(limiter) 21 | app.use(xss()) 22 | app.use(express.json({ limit: '10kb' })); 23 | 24 | app.use('/api/v1/docs', swaggerUi.serve, swaggerUi.setup(swaggerJsDoc)) 25 | app.use('/api/v1/emp', routes.employee); 26 | app.use('/api/v1/cust', routes.customer); 27 | 28 | app.listen(PORT, () => console.log('Server is running @ ' + baseUrl)); 29 | 30 | 31 | // process.on('SIGTERM', () => { 32 | // console.log(""); 33 | 34 | // logger.error('SIGTERM signal received.'); 35 | // logger.error('Gracefully closing the database connection') 36 | // db.close(); 37 | // }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-prod-boilerplate", 3 | "version": "1.0.0", 4 | "description": "Node express API production ready boilerplate", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production node dist/app.js", 8 | "prod": "NODE_ENV=production node dist/api.bundle.js", 9 | "dev": "NODE_ENV=development nodemon src/app.ts", 10 | "build:dev": "rimraf dist && tsc -p .", 11 | "build:prod": "rimraf dist && webpack", 12 | "lint": "eslint . --ext .ts", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "author": "Vishwas Anand Bhushan", 16 | "license": "MIT", 17 | "dependencies": { 18 | "dotenv": "^8.2.0", 19 | "express": "^4.17.1", 20 | "express-rate-limit": "^5.2.6", 21 | "helmet": "^4.4.1", 22 | "mongoose": "^5.12.3", 23 | "swagger-ui-express": "^4.1.6", 24 | "uuid": "^8.3.2", 25 | "winston": "^3.3.3", 26 | "xss-clean": "^0.1.1" 27 | }, 28 | "devDependencies": { 29 | "@types/body-parser": "^1.17.0", 30 | "@types/cors": "^2.8.5", 31 | "@types/method-override": "0.0.31", 32 | "@types/morgan": "^1.7.35", 33 | "@types/node": "^12.6.1", 34 | "@typescript-eslint/eslint-plugin": "^4.18.0", 35 | "@typescript-eslint/parser": "^4.18.0", 36 | "eslint": "^7.22.0", 37 | "nodemon": "^1.19.1", 38 | "rimraf": "^2.6.3", 39 | "ts-loader": "^8.0.18", 40 | "ts-node": "^9.1.1", 41 | "typescript": "^4.2.3", 42 | "webpack": "^5.27.1", 43 | "webpack-cli": "^4.5.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/controllers/employes.ts: -------------------------------------------------------------------------------- 1 | import { Employee } from "../services/employee"; 2 | import { logger } from "../config"; 3 | import { Request, Response } from 'express'; 4 | 5 | async function addEmployee(req: Request, res: Response){ 6 | try { 7 | const { firstName, lastName, email, role, dob } = req.body; 8 | if (firstName == "" || lastName == "" || email == "" || role == "" || dob == "") 9 | res 10 | .status(400) 11 | .send("firstName, lastName, email, role fields are mandatory"); 12 | const empObj = new Employee(req.body); 13 | const newEmp = await empObj.addEmployee(); 14 | res.send(newEmp); 15 | } catch (e) { 16 | logger.error('EmployeeCtrl:: addEmployee(): Error ' + e); 17 | res.status(500).send(e.message); 18 | } 19 | } 20 | 21 | async function getAllEmployee(req: Request, res: Response) { 22 | try { 23 | const empObj = new Employee({}); 24 | const listOfEmployee = await empObj.fetchAllEmployees(); 25 | res.send(listOfEmployee); 26 | } catch (e) { 27 | logger.error('EmployeeCtrl:: getAllEmployee(): Error ' + e); 28 | res.status(500).send(e.message); 29 | } 30 | } 31 | 32 | async function getEmployeeById(req: Request, res: Response) { 33 | try { 34 | const { id } = req.params; 35 | if (!id) res.status(400).send("Item id is mandatory"); 36 | 37 | const empObj = new Employee({}); 38 | const listOfEmployee = await empObj.fetchAllEmployees(); 39 | const emp = listOfEmployee.filter((x) => x.id === id); 40 | res.send(emp); 41 | } catch (e) { 42 | logger.error('EmployeeCtrl:: getEmployeeById(): Error ' + e); 43 | res.status(500).send(e.message); 44 | } 45 | } 46 | 47 | export default { 48 | addEmployee, 49 | getAllEmployee, 50 | getEmployeeById, 51 | }; 52 | -------------------------------------------------------------------------------- /src/services/store.ts: -------------------------------------------------------------------------------- 1 | import { Employee } from './employee'; 2 | import { Customer } from './customer'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | import { logger } from "../config"; 5 | export default class Store { 6 | employeeList: Map; 7 | customerList: Map; 8 | 9 | constructor(){ 10 | this.employeeList = new Map(); 11 | this.customerList = new Map(); 12 | } 13 | 14 | add(item: Employee | Customer) : Promise{ 15 | logger.info('Store:: add() called') 16 | return new Promise((resolve, reject) => { 17 | item.id = uuidv4(); 18 | if(item instanceof Employee){ 19 | logger.info('Store:: add(): item is an instance of Employee') 20 | logger.info('Store:: add(): adding item to employee list. id = ' + item.id); 21 | this.employeeList.set(item.id, item) 22 | logger.info('Store:: add(): iteam = ', item); 23 | resolve(item); 24 | } 25 | else if(item instanceof Customer){ 26 | logger.info('Store:: add(): item is an instance of Customer') 27 | logger.info('Store:: add(): adding item to Customer list. id = ' + item.id); 28 | this.customerList.set(item.id, item) 29 | resolve(item); 30 | } 31 | else{ 32 | reject(); 33 | } 34 | }) 35 | } 36 | 37 | findAll(type: Employee | Customer): Promise>{ 38 | logger.info('Store:: findAll() called') 39 | return new Promise((resolve, reject) => { 40 | if(type instanceof Employee){ 41 | logger.info('Store:: findAll(): type is an instance of Employee') 42 | resolve(Array.from(this.employeeList.values())); 43 | } 44 | else if(type instanceof Customer){ 45 | logger.info('Store:: findAll(): type is an instance of Customer') 46 | resolve(Array.from(this.customerList.values())); 47 | } 48 | else { 49 | logger.info('Store:: findAll(): type is an instance of neither Customer nor Emplooyee. rejecting') 50 | reject(); 51 | } 52 | }) 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import path from "path"; 3 | import winston from "winston"; 4 | import fs from "fs"; 5 | import { homedir } from "os"; 6 | import mongoose from 'mongoose'; 7 | 8 | class Configuration { 9 | private static instace: Configuration; 10 | public db: any; 11 | public NODE_ENV: string; 12 | public HOST: string; 13 | public PORT: string; 14 | public baseUrl: string; 15 | public logger: winston.Logger; 16 | private LOG_LEVEL: string; 17 | public dataDIR: string; 18 | private dbConnUrl: string; 19 | 20 | private constructor() {} 21 | 22 | public static getInstance(): Configuration { 23 | if (!Configuration.instace) { 24 | Configuration.instace = new Configuration(); 25 | Configuration.instace.setupEnvVar(); 26 | Configuration.instace.setup(); 27 | Configuration.instace.setupLogger(); 28 | Configuration.instace.setupDb(); 29 | } 30 | return Configuration.instace; 31 | } 32 | 33 | private setup(){ 34 | this.HOST = process.env.HOST ? process.env.HOST : "localhost"; 35 | this.PORT = process.env.PORT ? process.env.PORT : "4006"; 36 | this.LOG_LEVEL = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : "info"; 37 | this.NODE_ENV = process.env.NODE_ENV ? process.env.NODE_ENV : "development"; 38 | this.dbConnUrl = process.env.DB_URL; 39 | this.baseUrl = "http://" + this.HOST + ":" + this.PORT; 40 | 41 | this.dataDIR = process.env.DATA_DIR 42 | ? process.env.DATA_DIR 43 | : path.join(homedir(), "boilerplate"); 44 | if (!fs.existsSync(this.dataDIR)) fs.mkdirSync(this.dataDIR); 45 | } 46 | 47 | private setupEnvVar(){ 48 | // Enviroment variable 49 | //////////////////////// 50 | const envPath = path.resolve( 51 | __dirname, 52 | "../", 53 | process.env.NODE_ENV + ".env" 54 | ); 55 | 56 | console.log(envPath); 57 | 58 | if (fs.existsSync(envPath)) { 59 | dotenv.config({ 60 | path: envPath, 61 | }); 62 | } else { 63 | dotenv.config(); 64 | } 65 | } 66 | 67 | private setupLogger() { 68 | const logDIR = path.join(this.dataDIR, "./log"); 69 | if (!fs.existsSync(logDIR)) fs.mkdirSync(logDIR); 70 | 71 | const { combine, timestamp, printf } = winston.format; 72 | const customLogFormat = printf(({ level, message, timestamp }) => { 73 | return `${timestamp} [${level}] ${message}`; 74 | }); 75 | const logFilePath = path.join(logDIR, "boilerplate.log"); 76 | this.logger = winston.createLogger({ 77 | level: this.LOG_LEVEL || "info", 78 | format: combine(timestamp(), customLogFormat), 79 | transports: [ 80 | new winston.transports.File({ 81 | filename: path.join(logDIR, "boilerplate-error.log"), 82 | level: "error", 83 | }), 84 | new winston.transports.File({ filename: logFilePath }), 85 | ], 86 | }); 87 | if (this.NODE_ENV !== "production") { 88 | this.logger.add( 89 | new winston.transports.Console({ 90 | format: winston.format.simple(), 91 | }) 92 | ); 93 | } 94 | 95 | this.logger.info(`Log filepath is set to ${logFilePath}`); 96 | } 97 | 98 | private async setupDb(){ 99 | await mongoose.connect(this.dbConnUrl, 100 | {useNewUrlParser: true, useUnifiedTopology: true }) 101 | this.db = mongoose.connection; 102 | } 103 | 104 | // private setupDb() { 105 | // const dbDIR = path.join(this.dataDIR, "./db"); 106 | // if (!fs.existsSync(dbDIR)) fs.mkdirSync(dbDIR); 107 | 108 | // let dbFileName = 109 | // process.env.DB_INMEM === "true" ? ":memory:" : "boilerplate.db"; 110 | // let dbFilePath; 111 | 112 | // if (!dbFileName || dbFileName == "") { 113 | // this.logger.info("No dbfilepath provided, using in-memory database"); 114 | // dbFilePath = ":memory:"; 115 | // } else { 116 | // dbFilePath = path.join(dbDIR, dbFileName); 117 | // } 118 | 119 | // // Creating a new db if it does not exists 120 | // this.db = new sqlite.Database(dbFilePath, (err) => { 121 | // if (err) { 122 | // return this.logger.error(err.message); 123 | // } 124 | // this.logger.info( 125 | // `Connected to SQLLite database, dbFilePath = ${dbFilePath}` 126 | // ); 127 | // }); 128 | // } 129 | } 130 | 131 | const { 132 | db, 133 | NODE_ENV, 134 | HOST, 135 | PORT, 136 | baseUrl, 137 | logger, 138 | } = Configuration.getInstance(); 139 | export { db, NODE_ENV, HOST, PORT, baseUrl, logger }; 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Express API boilerplate for production 2 | 3 | When building REST APIs using node express, it is very easy to get confused as there are so many packages/tools available for different purposes - you do not know which to use and when! In this repository, I tried to consolidate them in this boilerplate repository and use them for example apis with CURD operations. I tried to keep this project (or boilerplate) as close to production as possible, but yeah we know nothing is perfect when it comes to prod deployment but atleast we can give our best. I also believe that, I might have missed out something for sure, which I request you to create an issue, if you find one. 4 | 5 | ### Some thoughts 6 | 7 | Building REST apis are fun, but what I have observed is, when you think from start to end, meaning right from planning about these apis, till the deployment of apis (which ofcouse includes security and other important aspects), it becomes more complex and at the same time more fun. So the question is how do we start? 8 | 9 | First we plan, in the planning phase you are looking at the whole product without even building it. Writing APIs specification (ofcoures when you have passed the design phase) could be good starting point. You design the API specifications so that: 10 | 11 | - frontend guy start developing the UI, 12 | - testers start wrting the test cases 13 | - and backend folks, start working on developing APIs. 14 | 15 | All these process happens parallely. This process is called [API first approach](https://developers.redhat.com/blog/2019/01/14/building-a-node-js-service-using-the-api-first-approach/). I love this approach since it gives me visibility of the whole product before writing too much code! Once done, you repeat the whole process if needed. 16 | 17 | ## Features 18 | 19 | 1. **Planning** 20 | 21 | - Swagger to implement OpenAPI specification 3.0.0 22 | - Approach - API first (`swagger`) 23 | 24 | 2. **Development** 25 | 26 | - Framework 27 | - Nodejs express 28 | - MVC 29 | - Typescript 30 | - Node v14.16.0 31 | - Debugging (`nodemon`, `ts-node`) 32 | - Storage 33 | - mongo db 34 | - Logging (`winston`) 35 | - Environment variable (`dotenv`) 36 | - Linting (`eslint`) 37 | - Security 38 | - Authentication ? 39 | - Authorization ? 40 | - Prevents DOS attack 41 | - limit body payload 42 | - express rate limit dependency (`express-rate-limit`) 43 | - Prevents XSS attacks 44 | - Appropriate headers (`helmet`) 45 | - Data Sanitization against XSS (`xss-clean`) 46 | 47 | 3. **Testing** 48 | 49 | 4. **Deployment** 50 | 51 | - Bundling (`webpack`) 52 | - Use process manager (`PM2`) 53 | - Containerization (`docker`) 54 | 55 | 56 | ## Install and usage 57 | 58 | Install dependencies 59 | ```bash 60 | npm i 61 | ``` 62 | 63 | Development 64 | 65 | ```bash 66 | npm run dev 67 | ``` 68 | 69 | ``` 70 | npm run build:dev 71 | npm run start 72 | ``` 73 | 74 | Production 75 | 76 | ```bash 77 | npm run build:prod 78 | npm run prod #production 79 | ``` 80 | Note: Create `production.env` for production run 81 | 82 | ### Dockerization 83 | 84 | Build docker image 85 | ``` 86 | npm run build:prod 87 | docker build -t ts-boilerplate . # build an image 88 | ``` 89 | 90 | Run container 91 | ``` 92 | docker run -p 5000:5000 -d ts-boilerplate 93 | ``` 94 | Note: Create `production.env` for production run 95 | 96 | ## References 97 | 98 | [Production deployment - by mozilla](https://developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs/deployment) | [API First approach](https://developers.redhat.com/blog/2019/01/14/building-a-node-js-service-using-the-api-first-approach/) | [production-ready-node-and-express-app-](https://www.freecodecamp.org/news/how-to-write-a-production-ready-node-and-express-app-f214f0b17d8c/) 99 | | [production-ready-node-js-rest-api-typescrip - blog](https://medium.com/bb-tutorials-and-thoughts/how-to-write-production-ready-node-js-rest-api-typescript-version-94e993b368c0) | [Airbnb JavaScript Style Guide()](https://github.com/airbnb/javascript) | [Linting in Typescript](https://khalilstemmler.com/blogs/typescript/eslint-for-typescript/) | [security-on-your-nodejs-api](https://itnext.io/make-security-on-your-nodejs-api-the-priority-50da8dc71d68) | [configuring-middleware-for-authentication](https://thinkster.io/tutorials/node-json-api/configuring-middleware-for-authentication) | [About helmet](https://helmetjs.github.io/) | [Good repo to follow](https://github.com/microverseinc/project-nodejs-rest-api) | [Docker publish vs expose](https://www.whitesourcesoftware.com/free-developer-tools/blog/docker-expose-port/) | [make your NodeJS application or API secure](https://itnext.io/make-security-on-your-nodejs-api-the-priority-50da8dc71d68) | [Gracefully shutdown handling](https://hackernoon.com/graceful-shutdown-in-nodejs-2f8f59d1c357) | [Using sqeuelize](https://sequelize.org/master/manual/model-basics.html) | [Learn Typescript](https://www.typescriptlang.org/docs/) | [setup-typecript-sequelize](https://vivacitylabs.com/setup-typescript-sequelize/) | [strongly-typed-models-with-mongoose-and-typescript](https://tomanagle.medium.com/strongly-typed-models-with-mongoose-and-typescript-7bc2f7197722) -------------------------------------------------------------------------------- /src/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "version": "1.0.0", 5 | "title": "ts-express-api-boilerplate", 6 | "description": "APIs for CURD", 7 | "license": { 8 | "name": "MIT", 9 | "url": "https://opensource.org/licenses/MIT" 10 | }, 11 | "contact": { 12 | "email": "vishwasbhushan001@gmail.com" 13 | } 14 | }, 15 | "host": "localhost:5000", 16 | "schemes": [ 17 | "http" 18 | ], 19 | "consumes": [ 20 | "application/json" 21 | ], 22 | "produces": [ 23 | "application/json" 24 | ], 25 | "tags": [ 26 | { 27 | "name": "Employee", 28 | "description": "APIs for employess" 29 | } 30 | ], 31 | "basePath": "/api/v1", 32 | "paths": { 33 | "/emp": { 34 | "get": { 35 | "summary": "Fetches list of employess", 36 | "tags": [ 37 | "Employee" 38 | ], 39 | "responses": { 40 | "200": { 41 | "description": "OK", 42 | "schema": { 43 | "type": "array", 44 | "items": { 45 | "$ref": "#/definitions/Employees" 46 | } 47 | } 48 | }, 49 | "400": { 50 | "description": "Bad request" 51 | }, 52 | "500": { 53 | "description": "Internal server error" 54 | } 55 | } 56 | }, 57 | "post": { 58 | "summary": "Creates a new employee", 59 | "tags": [ 60 | "Employee" 61 | ], 62 | "parameters": [ 63 | { 64 | "in": "body", 65 | "name": "body", 66 | "description": "", 67 | "required": true, 68 | "schema": { 69 | "$ref": "#/definitions/Employee" 70 | } 71 | } 72 | ], 73 | "responses": { 74 | "200": { 75 | "description": "OK", 76 | "schema": { 77 | "type": "array", 78 | "items": { 79 | "$ref": "#/definitions/Employee" 80 | } 81 | } 82 | }, 83 | "400": { 84 | "description": "Error" 85 | }, 86 | "500": { 87 | "description": "Internal server error" 88 | } 89 | } 90 | } 91 | }, 92 | "/emp/{id}": { 93 | "get": { 94 | "summary": "Fetches an employess by id", 95 | "tags": [ 96 | "Employee" 97 | ], 98 | "parameters": [ 99 | { 100 | "in": "path", 101 | "name": "id", 102 | "required": true, 103 | "description": "Id of employee", 104 | "type": "string" 105 | } 106 | ], 107 | "responses": { 108 | "200": { 109 | "description": "OK", 110 | "schema": { 111 | "type": "array", 112 | "items": { 113 | "$ref": "#/definitions/Employee" 114 | } 115 | } 116 | }, 117 | "400": { 118 | "description": "Bad request" 119 | }, 120 | "500": { 121 | "description": "Internal server error" 122 | } 123 | } 124 | } 125 | } 126 | }, 127 | "definitions": { 128 | "id": { 129 | "properties": { 130 | "uuid": { 131 | "type": "string" 132 | } 133 | } 134 | }, 135 | "Employee": { 136 | "type": "object", 137 | "properties": { 138 | "id": { 139 | "type": "string" 140 | }, 141 | "firstName": { 142 | "type": "string" 143 | }, 144 | "lastName": { 145 | "type": "string" 146 | }, 147 | "email": { 148 | "type": "string" 149 | }, 150 | "address": { 151 | "type": "string" 152 | }, 153 | "dob": { 154 | "type": "string" 155 | }, 156 | "role": { 157 | "type": "string", 158 | "description": "role of the employee", 159 | "enum": [ 160 | "MANAGER", 161 | "DEVELOPER", 162 | "TESTER" 163 | ] 164 | }, 165 | "fullName": { 166 | "type": "string" 167 | } 168 | } 169 | }, 170 | "Employees": { 171 | "type": "array", 172 | "items": { 173 | "$ref": "#/definitions/Employee" 174 | } 175 | } 176 | } 177 | } --------------------------------------------------------------------------------