├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── config └── paths.js ├── package-lock.json ├── package.json ├── pm2 └── prod.json ├── src ├── config │ └── secrets.ts ├── connection │ ├── connection-factory.ts │ ├── connection.ts │ ├── mongodb.ts │ └── mysql.ts ├── global.ts ├── helpers │ ├── backup-db.ts │ ├── emails.ts │ ├── logs.ts │ ├── schedule-job.ts │ ├── times.ts │ └── uuid.ts ├── middleware │ ├── check-auth.ts │ ├── file-upload.ts │ └── validate-input.ts ├── models │ ├── http-error.model.ts │ ├── index.ts │ ├── product.model.ts │ └── user.model.ts ├── modules │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.route.ts │ │ └── auth.service.ts │ └── production │ │ ├── order │ │ ├── order.controller.ts │ │ ├── order.route.ts │ │ └── order.service.ts │ │ └── product │ │ ├── product.controller.ts │ │ ├── product.route.ts │ │ └── product.service.ts ├── server.ts └── types │ └── types.d.ts ├── tsconfig.json └── webpack.config.js /.env.example: -------------------------------------------------------------------------------- 1 | PORT=8080 2 | NODE_ENV=development 3 | 4 | DB_AUTHENTICATE=false 5 | DB_HOST=127.0.0.1 6 | DB_PORT=27017 7 | DB_NAME=ExTyDB 8 | DB_USERNAME= 9 | DB_PASSWORD= 10 | 11 | JWT_KEY=secret_key_not_share -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | dist/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | jest: true, 7 | }, 8 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | ecmaFeatures: { 12 | jsx: true, 13 | }, 14 | ecmaVersion: 13, 15 | sourceType: 'module', 16 | }, 17 | plugins: ['@typescript-eslint'], 18 | rules: { 19 | 'array-bracket-spacing': ['warn'], 20 | 'object-curly-spacing': ['warn', 'always'], 21 | // the variable is not reassigned. Change let to const 22 | 'prefer-const': 'off', 23 | 'no-console': 'off', 24 | 'no-useless-escape': 'off', 25 | // I dont know. I use any :) 26 | '@typescript-eslint/no-explicit-any': ['off', { ignoreRestArgs: true }], 27 | '@typescript-eslint/no-empty-function': 'off', 28 | '@typescript-eslint/no-var-requires': 'off', 29 | '@typescript-eslint/no-inferrable-types': 'off', 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | 107 | .husky 108 | 109 | build 110 | 111 | # Backup database folder 112 | backups -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | package-lock.json 4 | yarn.lock -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Phuong Anh Nguyen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-typescript-webpack 2 | 3 | Express server boilerplate with typescript and webpack. The project helps developers quickly create new backend project in the future. 4 | 5 | ## Install package 6 | 7 | - Node 8 | - Npm 9 | - MongoDb 10 | - Pm2 11 | 12 | ## Set up evironment variables 13 | 14 | ```sh 15 | cp .env.example .env 16 | ``` 17 | 18 | Edit .env file: 19 | 20 | - PORT=8080 (port of server) 21 | - NODE_ENV=development (node enviroment) 22 | - DB_AUTHENTICATE=false (is database authenticated?) 23 | - DB_HOST=127.0.0.1 (database host) 24 | - DB_PORT=27017 (database port) 25 | - DB_NAME=ExTyDB (database name) 26 | - DB_USERNAME= (database username) 27 | - DB_PASSWORD= (database password) 28 | - JWT_KEY=secret_key_not_share (secret key for authenticating user) 29 | - EMAIL_ACCOUNT= (your google account to send the mail - send email using [nodemailer](https://nodemailer.com/about/)) 30 | - EMAIL_PASSWORD= (password of account) 31 | 32 | ## Set up dev environment 33 | 34 | ```sh 35 | npm install 36 | ``` 37 | 38 | ```sh 39 | npm run postinstall 40 | ``` 41 | 42 | ```sh 43 | npm run dev 44 | ``` 45 | 46 | ## Set up production environment 47 | 48 | ### Build project and test production mode 49 | 50 | ```sh 51 | npm run build 52 | ``` 53 | 54 | ### Run production mode with pm2 55 | 56 | ```sh 57 | npm run start-pm2 58 | ``` 59 | 60 | The copyright belongs to [Adrien Nguyen](https://adriennguyen.github.io/portfolio/) 61 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const fs = require('fs') 5 | 6 | const appDirectory = fs.realpathSync(process.cwd()) 7 | 8 | const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath) 9 | 10 | module.exports = { 11 | appBuild: resolveApp('build'), 12 | appServerTs: resolveApp('src/server.ts'), 13 | apPackageJson: resolveApp('package.json'), 14 | appNodemodule: resolveApp('node_modules'), 15 | appLog: resolveApp('logs'), 16 | appBackup: resolveApp('backups'), 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-typescript-webpack", 3 | "version": "1.0.0", 4 | "description": "Express server boilerplate with typescript and webpack. The project helps developers quickly create new backend project in the future.", 5 | "main": "src/server.ts", 6 | "scripts": { 7 | "postinstall": "rm -rf .husky && husky install && npx husky add .husky/pre-commit \"npx lint-staged\"", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "dev": "concurrently \"npm run watch-ts\" \"npm run watch-node\"", 10 | "watch-node": "node --trace-warnings dist/server.js", 11 | "watch-ts": "tsc --watch", 12 | "build": "webpack && node build/server.js", 13 | "start-pm2": "pm2 start ./pm2/prod.json", 14 | "lint": "eslint .", 15 | "format": "prettier --write \"**/*.+(js|ts|json|md)\"" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/AdrienNguyen/express-typescript-webpack.git" 20 | }, 21 | "keywords": [], 22 | "author": { 23 | "name": "Adrien Nguyen", 24 | "email": "phuong.na163228@gmail.com" 25 | }, 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/AdrienNguyen/express-typescript-webpack/issues" 29 | }, 30 | "homepage": "https://github.com/AdrienNguyen/express-typescript-webpack#readme", 31 | "lint-staged": { 32 | "*.+(js|ts)": "eslint --max-warnings 0 --fix", 33 | "*.+(js|ts|json|md)": "prettier --write" 34 | }, 35 | "dependencies": { 36 | "bcryptjs": "^2.4.3", 37 | "body-parser": "^1.19.1", 38 | "child_process": "^1.0.2", 39 | "crypto": "^1.0.1", 40 | "dotenv": "^10.0.0", 41 | "express": "^4.17.2", 42 | "express-validator": "^6.14.0", 43 | "jsonwebtoken": "^8.5.1", 44 | "mongoose": "^6.1.4", 45 | "mongoose-paginate-v2": "^1.4.2", 46 | "node-schedule": "^2.1.0", 47 | "nodemailer": "^6.7.2", 48 | "winston": "^3.3.3" 49 | }, 50 | "devDependencies": { 51 | "@types/express": "^4.17.13", 52 | "@types/node": "^17.0.2", 53 | "@typescript-eslint/eslint-plugin": "^5.8.0", 54 | "babel-loader": "^8.2.3", 55 | "concurrently": "^6.5.1", 56 | "eslint": "^8.5.0", 57 | "husky": "^7.0.4", 58 | "lint-staged": "^12.1.3", 59 | "nodemon": "^2.0.15", 60 | "pre-commit": "^1.2.2", 61 | "prettier": "^2.5.1", 62 | "terser-webpack-plugin": "^5.3.0", 63 | "ts-loader": "^9.2.6", 64 | "ts-node": "^10.4.0", 65 | "typescript": "^4.5.4", 66 | "webpack": "^5.65.0", 67 | "webpack-cli": "^4.9.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pm2/prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "express-typescript-webpack", 5 | "cwd": "./", 6 | "kill_timeout": 3000, 7 | "restart_delay": 3000, 8 | "script": "node", 9 | "args": "build/main.js" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/config/secrets.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | import fs from 'fs' 3 | 4 | if (fs.existsSync('.env')) { 5 | dotenv.config({ 6 | path: '.env', 7 | }) 8 | } else { 9 | dotenv.config({ 10 | path: '.env.example', 11 | }) 12 | } 13 | 14 | export const { 15 | NODE_ENV, 16 | PORT, 17 | DB_AUTHENTICATE, 18 | DB_HOST, 19 | DB_PORT, 20 | DB_NAME, 21 | DB_USERNAME, 22 | DB_PASSWORD, 23 | JWT_KEY, 24 | EMAIL_ACCOUNT, 25 | EMAIL_PASSWORD, 26 | } = process.env 27 | -------------------------------------------------------------------------------- /src/connection/connection-factory.ts: -------------------------------------------------------------------------------- 1 | import MongoDb from './mongodb' 2 | import MySQL from './mysql' 3 | 4 | export default class ConnectionFactory { 5 | constructor() {} 6 | 7 | public static getConnection(connectionType: DBType): Connection { 8 | switch (connectionType) { 9 | case DBType.MongoDb: 10 | return new MongoDb() 11 | case DBType.MySQL: 12 | return new MySQL() 13 | default: 14 | throw new Error( 15 | 'Get database connection failed. Please try again!', 16 | ) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/connection/connection.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | interface Connection { 3 | connect: () => void 4 | } 5 | -------------------------------------------------------------------------------- /src/connection/mongodb.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { 3 | DB_AUTHENTICATE, 4 | DB_HOST, 5 | DB_PORT, 6 | DB_NAME, 7 | DB_USERNAME, 8 | DB_PASSWORD, 9 | } from '../config/secrets' 10 | 11 | export default class MongoDb implements Connection { 12 | uri: string 13 | connectOptions: object 14 | 15 | constructor() { 16 | this.uri = `mongodb://${DB_HOST}:${DB_PORT || '27017'}/${DB_NAME}` 17 | this.connectOptions = 18 | DB_AUTHENTICATE === 'true' 19 | ? { 20 | useNewUrlParser: true, 21 | useUnifiedTopology: true, 22 | user: DB_USERNAME, 23 | pass: DB_PASSWORD, 24 | } 25 | : { 26 | useNewUrlParser: true, 27 | useUnifiedTopology: true, 28 | } 29 | } 30 | 31 | async connect() { 32 | mongoose 33 | .connect(this.uri, this.connectOptions) 34 | .then(() => { 35 | console.log('Connect Database MongoDb successfully') 36 | }) 37 | .catch((error) => { 38 | console.log(error.message) 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/connection/mysql.ts: -------------------------------------------------------------------------------- 1 | export default class MySQL implements Connection { 2 | async connect() { 3 | console.log('Connect Database MySQL here.') 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/global.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrienNguyen/express-typescript-webpack/7052b0d6c9eb2f0c09bafcd99550d9ccf156fce6/src/global.ts -------------------------------------------------------------------------------- /src/helpers/backup-db.ts: -------------------------------------------------------------------------------- 1 | // using node-schedule to set up job backup database every week 2 | 3 | import { 4 | DB_HOST, 5 | DB_PORT, 6 | DB_NAME, 7 | DB_USERNAME, 8 | DB_PASSWORD, 9 | DB_AUTHENTICATE, 10 | } from '../config/secrets' 11 | import { getLocalDateTimeString } from './times' 12 | import { appBackup } from '../../config/paths' 13 | import fs from 'fs' 14 | import { exec } from 'child_process' 15 | 16 | export const backupDB = (): void => { 17 | try { 18 | const version: string = getLocalDateTimeString() 19 | 20 | const createBackupFolder = (): string => { 21 | const path: string = appBackup + '/' + version 22 | if (!fs.existsSync(path)) { 23 | fs.mkdirSync(path, { 24 | recursive: true, 25 | }) 26 | } 27 | return path 28 | } 29 | 30 | const backupFolder: string = createBackupFolder() 31 | const backupDescription: string = `Backup database ${DB_NAME} at ${version}` 32 | 33 | // 1. create version.txt in folderback up 34 | fs.appendFile( 35 | backupFolder + `/README.txt`, 36 | backupDescription, 37 | (err) => { 38 | if (err) throw err 39 | }, 40 | ) 41 | // 2. backup database 42 | const commandBackup = 43 | DB_AUTHENTICATE === 'true' 44 | ? `mongodump --host="${DB_HOST}" --port="${DB_PORT}" --db="${DB_NAME}" --username="${DB_USERNAME}" --password="${DB_PASSWORD}" --out="${backupFolder}"` 45 | : `mongodump --host="${DB_HOST}" --port="${DB_PORT}" --db="${DB_NAME}" --out="${backupFolder}"` 46 | exec(commandBackup, (err) => { 47 | if (err) throw err 48 | }) 49 | } catch (error) { 50 | console.log(error.message) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/helpers/emails.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer' 2 | import { EMAIL_ACCOUNT, EMAIL_PASSWORD } from '../config/secrets' 3 | 4 | export const sendEmailForCreatedAccount = async (email: string) => { 5 | try { 6 | console.log(email) 7 | const transporter = nodemailer.createTransport({ 8 | service: 'Gmail', 9 | auth: { 10 | user: EMAIL_ACCOUNT, 11 | pass: EMAIL_PASSWORD, 12 | }, 13 | }) 14 | 15 | const mainOptions = { 16 | from: EMAIL_ACCOUNT, 17 | to: email, 18 | subject: 'Register account successfully!', 19 | text: 20 | 'You have registered account successfully in out system with email!: ' + 21 | email, 22 | html: ` 23 | 24 | 64 | 65 | 66 | 67 |
68 |
69 |

WEBSITE-DEMO

70 |
71 |
72 |

your account information:

73 |
74 |
  • Account: ${email}
  • 75 |
    76 |
    77 | 82 |
    83 | 84 | `, 85 | } 86 | 87 | return await transporter.sendMail(mainOptions) 88 | } catch (error) { 89 | console.log(error.message) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/helpers/logs.ts: -------------------------------------------------------------------------------- 1 | import { format, createLogger, transports, Logger } from 'winston' 2 | 3 | const { splat, printf, combine, label, timestamp } = format 4 | 5 | import { appLog } from '../../config/paths' 6 | 7 | const Log = async (title: string): Promise => { 8 | let options = { 9 | format: combine( 10 | splat(), 11 | timestamp({ 12 | format: 'YYYY-MM-DD HH:mm:ss', 13 | }), 14 | label({ 15 | label: title, 16 | }), 17 | printf((log) => { 18 | return `${log.timestamp} | ${log.label} | ${log.level} | ${log.message}` 19 | }), 20 | ), 21 | transports: [ 22 | new transports.File({ 23 | filename: appLog + `/info.log`, 24 | level: 'info', 25 | }), 26 | new transports.File({ 27 | filename: appLog + `/error.log`, 28 | level: 'error', 29 | }), 30 | ], 31 | } 32 | 33 | return createLogger(options) 34 | } 35 | 36 | export const LogInfo = async ( 37 | email: string, 38 | content: string, 39 | ): Promise => { 40 | try { 41 | const Logger = await Log(content) 42 | Logger.info(email) 43 | } catch (error) { 44 | console.trace(error.message) 45 | } 46 | } 47 | 48 | export const LogError = async ( 49 | email: string, 50 | content: string, 51 | ): Promise => { 52 | try { 53 | const Logger = await Log(content) 54 | Logger.error(email) 55 | } catch (error) { 56 | console.trace(error.message) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/helpers/schedule-job.ts: -------------------------------------------------------------------------------- 1 | import schedule from 'node-schedule' 2 | import { uuid } from './uuid' 3 | 4 | export default class ScheduleJob { 5 | id: string 6 | ruleTime: object 7 | action: any 8 | 9 | constructor(ruleTime: object, action: any) { 10 | this.id = uuid() 11 | this.ruleTime = ruleTime 12 | this.action = action 13 | } 14 | 15 | start = (): void => { 16 | schedule.scheduleJob(this.id, this.ruleTime, this.action) 17 | } 18 | 19 | cancel = (): void => { 20 | schedule.scheduledJobs[this.id].cancel() 21 | } 22 | 23 | reschedule = (newRule: object): void => { 24 | schedule.scheduledJobs[this.id].reschedule(newRule) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/helpers/times.ts: -------------------------------------------------------------------------------- 1 | export const getLocalDateTimeString = (): string => { 2 | const dateTime = new Date() 3 | const year = dateTime.getFullYear() 4 | let month: string | number = dateTime.getMonth() + 1 5 | month = month < 9 ? '0' + month : month 6 | let date: string | number = dateTime.getDate() 7 | date = date < 9 ? '0' + date : date 8 | const hour = dateTime.getHours() 9 | const minute = dateTime.getMinutes() 10 | const second = dateTime.getSeconds() 11 | 12 | return `${year}-${month}-${date}_${hour}h${minute}m${second}s` 13 | } 14 | -------------------------------------------------------------------------------- /src/helpers/uuid.ts: -------------------------------------------------------------------------------- 1 | const { randomBytes } = require('crypto') 2 | 3 | let SIZE = 4096, 4 | IDX = 0, 5 | BUFFER, 6 | HEX = [] 7 | 8 | // HEX = [00 01 02 .... ff] 9 | for (let i = 0; i < 256; i++) { 10 | HEX[i] = (i + 256).toString(16).substring(1) 11 | } 12 | 13 | export const uuid = (): string => { 14 | if (!BUFFER || IDX + 16 > SIZE) { 15 | // SIZE bytes 16 | BUFFER = randomBytes(SIZE) 17 | IDX = 0 18 | } 19 | 20 | let i = 0, 21 | tmp, 22 | out = '' 23 | for (i = 0; i < 16; i++) { 24 | tmp = BUFFER[i + IDX] 25 | 26 | if (i == 6) { 27 | out += HEX[(tmp & 15) | 64] 28 | } else if (i == 8) { 29 | out += HEX[(tmp & 63) | 128] 30 | } else { 31 | out += HEX[tmp] 32 | } 33 | 34 | if (i === 3 || i === 5 || i === 7 || i == 9) { 35 | out += '-' 36 | } 37 | } 38 | IDX++ 39 | return out 40 | } 41 | -------------------------------------------------------------------------------- /src/middleware/check-auth.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | 3 | import { HttpError } from '../models' 4 | import jwt from 'jsonwebtoken' 5 | import { JWT_KEY } from '../config/secrets' 6 | 7 | const checkAuth = (req, res, next) => { 8 | if (req.method === 'OPTIONS') { 9 | next() 10 | } 11 | 12 | try { 13 | const token = req.headers.authorization.split(' ')[1] 14 | if (!token) { 15 | throw new HttpError('Authentication failed!', 401) 16 | } 17 | 18 | const decodedToken = jwt.verify(token, JWT_KEY) 19 | 20 | req.user = decodedToken 21 | 22 | next() 23 | } catch (error) { 24 | return next(new HttpError('Authentication failed!', 401)) 25 | } 26 | } 27 | 28 | export default checkAuth 29 | -------------------------------------------------------------------------------- /src/middleware/file-upload.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrienNguyen/express-typescript-webpack/7052b0d6c9eb2f0c09bafcd99550d9ccf156fce6/src/middleware/file-upload.ts -------------------------------------------------------------------------------- /src/middleware/validate-input.ts: -------------------------------------------------------------------------------- 1 | import { validationResult } from 'express-validator' 2 | import { LogError } from '../helpers/logs' 3 | import { HttpError } from '../models' 4 | 5 | const validateInput = (message) => { 6 | return async (req, res, next) => { 7 | const error = validationResult(req) 8 | if (!error.isEmpty()) { 9 | res.message = message + '_FAILED' 10 | 11 | req.user && (await LogError(req.user.email, message)) 12 | 13 | return next( 14 | new HttpError( 15 | 'Invalid inputs passed, please check your data', 16 | 422, 17 | ), 18 | ) 19 | } 20 | next() 21 | } 22 | } 23 | 24 | export default validateInput 25 | -------------------------------------------------------------------------------- /src/models/http-error.model.ts: -------------------------------------------------------------------------------- 1 | class HttpError extends Error { 2 | code: number 3 | 4 | constructor(message, errorCode) { 5 | super(message) 6 | this.code = errorCode 7 | } 8 | } 9 | 10 | export default HttpError 11 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | import User from './user.model' 2 | import HttpError from './http-error.model' 3 | import Product from './product.model' 4 | 5 | export { User, HttpError, Product } 6 | -------------------------------------------------------------------------------- /src/models/product.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import mongoosePaginate from 'mongoose-paginate-v2' 3 | const Schema = mongoose.Schema 4 | 5 | const productSchema = new Schema( 6 | { 7 | name: { 8 | type: String, 9 | required: true, 10 | }, 11 | price: { 12 | type: Number, 13 | required: true, 14 | }, 15 | description: { 16 | type: String, 17 | }, 18 | image: { 19 | type: String, 20 | }, 21 | }, 22 | { 23 | timestamps: true, 24 | }, 25 | ) 26 | 27 | productSchema.plugin(mongoosePaginate) 28 | 29 | const Product = mongoose.model('Product', productSchema) as any 30 | export default Product 31 | -------------------------------------------------------------------------------- /src/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | const Schema = mongoose.Schema 3 | 4 | const userSchema = new Schema( 5 | { 6 | name: { 7 | type: String, 8 | required: true, 9 | }, 10 | email: { 11 | type: String, 12 | required: true, 13 | }, 14 | password: { 15 | type: String, 16 | required: true, 17 | }, 18 | image: { 19 | type: String, 20 | }, 21 | }, 22 | { 23 | timestamps: true, 24 | }, 25 | ) 26 | 27 | const User = mongoose.model('User', userSchema) 28 | export default User 29 | -------------------------------------------------------------------------------- /src/modules/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { LogInfo, LogError } from '../../helpers/logs' 2 | import authService from './auth.service' 3 | 4 | const register = async (req, res) => { 5 | try { 6 | const user = await authService.register(req.body) 7 | 8 | req.body.email && LogInfo(req.body.email, 'REGISTER_SUCCESSFULLY') 9 | 10 | res.status(201).json({ 11 | success: true, 12 | message: 'REGISTER_SUCCESSFULLY', 13 | content: user, 14 | }) 15 | } catch (error) { 16 | req.body.email && LogError(req.body.email, 'REGISTER_FAILED') 17 | 18 | res.status(error.code || 400).json({ 19 | success: false, 20 | message: 'REGISTER_FAILED', 21 | content: error.message, 22 | }) 23 | } 24 | } 25 | 26 | const login = async (req, res) => { 27 | try { 28 | const user = await authService.login(req.body) 29 | 30 | req.body.email && LogInfo(req.body.email, 'LOGIN_SUCCESSFULLY') 31 | 32 | res.status(200).json({ 33 | success: true, 34 | message: 'LOGIN_SUCCESSFULLY', 35 | content: user, 36 | }) 37 | } catch (error) { 38 | req.body.email && LogError(req.body.email, 'REGISTER_FAILED') 39 | 40 | res.status(error.code || 400).json({ 41 | success: false, 42 | message: 'LOGIN_FAILED', 43 | content: error.message, 44 | }) 45 | } 46 | } 47 | 48 | export default { 49 | register, 50 | login, 51 | } 52 | -------------------------------------------------------------------------------- /src/modules/auth/auth.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { check } from 'express-validator' 3 | 4 | import validateInput from '../../middleware/validate-input' 5 | import authController from './auth.controller' 6 | 7 | const router = express.Router() 8 | 9 | router.post( 10 | '/register', 11 | [ 12 | check('name').not().isEmpty(), 13 | check('email').isEmail(), 14 | check('password').isLength({ min: 6 }), 15 | ], 16 | validateInput('REGISTER_FAILED'), 17 | authController.register, 18 | ) 19 | 20 | router.post('/login', authController.login) 21 | 22 | export default router 23 | -------------------------------------------------------------------------------- /src/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { User, HttpError } from '../../models' 2 | import bcrypt from 'bcryptjs' 3 | import jwt from 'jsonwebtoken' 4 | import { JWT_KEY } from '../../config/secrets' 5 | import { sendEmailForCreatedAccount } from '../../helpers/emails' 6 | 7 | export const register = async (data) => { 8 | const { name, email, password } = data 9 | 10 | const existingUser = await User.findOne({ 11 | email: email, 12 | }) 13 | 14 | if (existingUser) { 15 | throw new HttpError('User exists already, please login instead', 422) 16 | } 17 | 18 | const hashedPassword = await bcrypt.hash(password, 12) 19 | 20 | const newUser = await User.create({ 21 | email: email, 22 | password: hashedPassword, 23 | name: name, 24 | }) 25 | 26 | const token = jwt.sign( 27 | { 28 | userId: newUser.id, 29 | email: newUser.email, 30 | }, 31 | JWT_KEY, 32 | { 33 | expiresIn: '1h', 34 | }, 35 | ) 36 | 37 | return { 38 | user: { 39 | userId: newUser.id, 40 | email: newUser.email, 41 | token: token, 42 | }, 43 | } 44 | } 45 | 46 | const login = async (data) => { 47 | const { email, password } = data 48 | 49 | const existingUser = await User.findOne({ 50 | email: email, 51 | }) 52 | 53 | if (!existingUser) { 54 | throw new HttpError('Invalid credentials, could not log you in.', 403) 55 | } 56 | 57 | const isValidPassword = await bcrypt.compare( 58 | password, 59 | existingUser.password, 60 | ) 61 | 62 | if (!isValidPassword) { 63 | throw new HttpError('Invalid credentials, could not log you in.', 403) 64 | } 65 | 66 | const token = jwt.sign( 67 | { 68 | userId: existingUser.id, 69 | email: existingUser.email, 70 | }, 71 | JWT_KEY, 72 | { 73 | expiresIn: '1h', 74 | }, 75 | ) 76 | 77 | await sendEmailForCreatedAccount(email) 78 | 79 | return { 80 | user: { 81 | userId: existingUser.id, 82 | email: existingUser.email, 83 | token: token, 84 | }, 85 | } 86 | } 87 | 88 | export default { 89 | register, 90 | login, 91 | } 92 | -------------------------------------------------------------------------------- /src/modules/production/order/order.controller.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrienNguyen/express-typescript-webpack/7052b0d6c9eb2f0c09bafcd99550d9ccf156fce6/src/modules/production/order/order.controller.ts -------------------------------------------------------------------------------- /src/modules/production/order/order.route.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrienNguyen/express-typescript-webpack/7052b0d6c9eb2f0c09bafcd99550d9ccf156fce6/src/modules/production/order/order.route.ts -------------------------------------------------------------------------------- /src/modules/production/order/order.service.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdrienNguyen/express-typescript-webpack/7052b0d6c9eb2f0c09bafcd99550d9ccf156fce6/src/modules/production/order/order.service.ts -------------------------------------------------------------------------------- /src/modules/production/product/product.controller.ts: -------------------------------------------------------------------------------- 1 | import { LogInfo, LogError } from '../../../helpers/logs' 2 | import productService from './product.service' 3 | 4 | const createProduct = async (req, res) => { 5 | try { 6 | const product = await productService.createProduct(req.body) 7 | 8 | req.user && (await LogInfo(req.user.email, 'CREATE_PRODUCT')) 9 | 10 | res.status(201).json({ 11 | success: true, 12 | message: 'CREATE_PRODUCT_SUCCESSFULLY', 13 | content: product, 14 | }) 15 | } catch (error) { 16 | req.user && (await LogError(req.user.email, 'CREATE_PRODUCT')) 17 | 18 | res.status(error.code || 400).json({ 19 | success: false, 20 | message: 'CREATE_PRODUCT_FAILED', 21 | content: error.message, 22 | }) 23 | } 24 | } 25 | 26 | const getProducts = async (req, res) => { 27 | try { 28 | const products = await productService.getProducts(req.query) 29 | 30 | res.status(201).json({ 31 | success: true, 32 | message: 'GET_PRODUCTS_SUCCESSFULLY', 33 | content: products, 34 | }) 35 | } catch (error) { 36 | res.status(error.code || 400).json({ 37 | success: false, 38 | message: 'GET_PRODUCTS_FAILED', 39 | content: error.message, 40 | }) 41 | } 42 | } 43 | 44 | export default { 45 | createProduct, 46 | getProducts, 47 | } 48 | -------------------------------------------------------------------------------- /src/modules/production/product/product.route.ts: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { check } from 'express-validator' 3 | import validateInput from '../../../middleware/validate-input' 4 | 5 | import checkAuth from '../../../middleware/check-auth' 6 | import productController from './product.controller' 7 | 8 | const router = express.Router() 9 | 10 | router.get('/', productController.getProducts) 11 | 12 | router.use(checkAuth) 13 | 14 | router.post( 15 | '/', 16 | [check('name').isLength({ min: 6, max: 100 }), check('price').isNumeric()], 17 | validateInput('CREATE_PRODUCT'), 18 | productController.createProduct, 19 | ) 20 | 21 | export default router 22 | -------------------------------------------------------------------------------- /src/modules/production/product/product.service.ts: -------------------------------------------------------------------------------- 1 | import { Product } from '../../../models' 2 | 3 | const createProduct = async (data) => { 4 | const { name, price, description } = data 5 | 6 | const newProduct = await Product.create({ 7 | name, 8 | price, 9 | description, 10 | }) 11 | 12 | return { product: newProduct.toObject({ getters: true }) } 13 | } 14 | 15 | interface IQueryProducts { 16 | name?: RegExp 17 | } 18 | 19 | const getProducts = async (data) => { 20 | let { name, page, limit } = data 21 | 22 | page = parseInt(page) 23 | limit = parseInt(limit) 24 | 25 | const options: IQueryProducts = {} 26 | if (name) { 27 | options.name = new RegExp(name, 'i') 28 | } 29 | 30 | let products: Array 31 | 32 | if (!page || !limit) { 33 | products = await Product.find(options).sort({ updatedAt: -1 }) 34 | } else { 35 | // products = await Product.find(options) 36 | // .sort({ updatedAt: -1 }) 37 | // .skip((page - 1) * limit) 38 | // .limit(limit) 39 | products = await Product.paginate(options, { 40 | page: page, 41 | limit: limit, 42 | sort: { 43 | createdAt: -1, 44 | }, 45 | }) 46 | } 47 | 48 | return { products } 49 | } 50 | 51 | export default { 52 | createProduct, 53 | getProducts, 54 | } 55 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | 3 | import express from 'express' 4 | import bodyParser from 'body-parser' 5 | import fs from 'fs' 6 | 7 | import { NODE_ENV, PORT } from './config/secrets' 8 | import ConnectionFactory from './connection/connection-factory' 9 | import authRouter from './modules/auth/auth.route' 10 | import productRouter from './modules/production/product/product.route' 11 | import { HttpError } from './models' 12 | import { backupDB } from './helpers/backup-db' 13 | import ScheduleJob from './helpers/schedule-job' 14 | class ExpressServer { 15 | app = express() 16 | 17 | async runApp() { 18 | this.app.listen(PORT || 8080, () => { 19 | console.info( 20 | 'Server is running at http://localhost: %d in %s mode', 21 | PORT, 22 | NODE_ENV, 23 | ) 24 | console.info('Press CTRL-C to stop') 25 | }) 26 | 27 | this.initDatebase() 28 | this.initBackupDatabase() 29 | this.initMiddleware() 30 | this.initRouter() 31 | this.handleError() 32 | } 33 | 34 | initDatebase() { 35 | const connection: Connection = ConnectionFactory.getConnection( 36 | DBType.MongoDb, 37 | ) 38 | connection.connect() 39 | } 40 | 41 | initBackupDatabase() { 42 | const timeRule = { 43 | hour: 21, 44 | minute: 5, 45 | dayOfWeek: 0, 46 | } 47 | const action = backupDB 48 | const backupSchedule = new ScheduleJob(timeRule, action) 49 | backupSchedule.start() 50 | } 51 | 52 | initMiddleware() { 53 | // handle body parser 54 | this.app.use(bodyParser.json()) 55 | // handle cors error 56 | this.app.use((req, res, next) => { 57 | res.setHeader('Access-Control-Allow-Origin', '*') 58 | res.setHeader( 59 | 'Access-Control-Allow-Headers', 60 | 'Origin, X-Requested-With, Content-Type, Accept, Authorization', 61 | ) 62 | res.setHeader( 63 | 'Access-Control-Allow-Methods', 64 | 'GET, POST, PATCH, DELETE', 65 | ) 66 | next() 67 | }) 68 | } 69 | 70 | initRouter() { 71 | this.app.use('/api/user', authRouter) 72 | this.app.use('/api/product', productRouter) 73 | this.app.get('/', (req, res) => { 74 | res.send('

    Welcome to express server with typescript

    ') 75 | }) 76 | } 77 | 78 | handleError() { 79 | // handle route not found 80 | this.app.use((req, res, next) => { 81 | throw new HttpError('Could not find this route', 404) 82 | }) 83 | // handle error 84 | this.app.use((error, req, res, next) => { 85 | if (req.file) { 86 | fs.unlink(req.file.path, (err) => { 87 | console.log(err) 88 | }) 89 | } 90 | if (res.headerSent) { 91 | return next(error) 92 | } 93 | 94 | res.status(error.code || 500).json({ 95 | success: false, 96 | message: res.message || '', 97 | content: error.message || 'An unknow error occurred!', 98 | }) 99 | }) 100 | } 101 | } 102 | 103 | const expressServer = new ExpressServer() 104 | 105 | expressServer.runApp().catch((err) => { 106 | console.trace('App shutdown due to a problem', err.message) 107 | process.exit(1) 108 | }) 109 | -------------------------------------------------------------------------------- /src/types/types.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | const enum DBType { 3 | MongoDb, 4 | MySQL, 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "esnext", 5 | "moduleResolution": "node", 6 | "sourceMap": false, 7 | "outDir": "dist", 8 | "baseUrl": "./src", 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true, 11 | "noImplicitAny": false, 12 | "lib": ["esnext"], 13 | "skipLibCheck": true, 14 | "typeRoots": ["src/config/types", "node_modules/@types"], 15 | "paths": { 16 | "*": ["node_modules/*", "src/types/*"] 17 | } 18 | }, 19 | "include": ["src/**/*"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const webpack = require('webpack') 3 | const TerserPlugin = require('terser-webpack-plugin') 4 | 5 | const paths = require('./config/paths') 6 | const { appNodemodule, appServerTs, appBuild } = paths 7 | 8 | var nodeModules = {} 9 | 10 | // note the path.resolve(__dirname, ...) part 11 | // without it, eslint-import-resolver-webpack fails 12 | // since eslint might be invoked with different cwd 13 | fs.readdirSync(appNodemodule) 14 | .filter((x) => ['.bin'].indexOf(x) === -1) 15 | .forEach((mod) => { 16 | nodeModules[mod] = `commonjs ${mod}` 17 | }) 18 | 19 | module.exports = { 20 | mode: 'production', 21 | entry: { server: appServerTs }, 22 | target: 'node', 23 | externals: nodeModules, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.js$/, 28 | exclude: [/node_modules/], 29 | use: { 30 | loader: 'babel-loader', 31 | }, 32 | }, 33 | { 34 | test: /\.ts$/, 35 | exclude: [/node_modules/], 36 | use: { 37 | loader: 'ts-loader', 38 | }, 39 | }, 40 | ], 41 | }, 42 | output: { 43 | path: appBuild, 44 | publicPath: '', 45 | filename: '[name].js', 46 | chunkFilename: '[name].[chunkhash].chunk.js', 47 | clean: true, 48 | }, 49 | resolve: { 50 | extensions: ['.ts', '.js'], 51 | }, 52 | plugins: [ 53 | new webpack.ProgressPlugin({ 54 | modulesCount: 5000, 55 | }), 56 | ], 57 | optimization: { 58 | nodeEnv: 'production', 59 | minimize: true, 60 | splitChunks: { 61 | chunks: 'all', 62 | cacheGroups: { 63 | vendors: { 64 | test: /[\\/]node_modules[\\/]/, 65 | priority: -10, 66 | }, 67 | default: { 68 | minChunks: 2, 69 | priority: -20, 70 | reuseExistingChunk: true, 71 | }, 72 | }, 73 | }, 74 | // Keep the runtime chunk separated to enable long term caching 75 | // https://twitter.com/wSokra/status/969679223278505985 76 | // https://github.com/facebook/create-react-app/issues/5358 77 | // runtimeChunk: { 78 | // name: (entrypoint) => `runtime-${entrypoint.name}`, 79 | // }, 80 | minimizer: [ 81 | // For webpack@5 you can use the `...` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line 82 | // `...`, 83 | new TerserPlugin({ 84 | terserOptions: { 85 | parse: { 86 | // We want terser to parse ecma 8 code. However, we don't want it 87 | // to apply any minification steps that turns valid ecma 5 code 88 | // into invalid ecma 5 code. This is why the 'compress' and 'output' 89 | // sections only apply transformations that are ecma 5 safe 90 | // https://github.com/facebook/create-react-app/pull/4234 91 | ecma: 8, 92 | }, 93 | compress: { 94 | ecma: 5, 95 | warnings: false, 96 | // Disabled because of an issue with Uglify breaking seemingly valid code: 97 | // https://github.com/facebook/create-react-app/issues/2376 98 | // Pending further investigation: 99 | // https://github.com/mishoo/UglifyJS2/issues/2011 100 | comparisons: false, 101 | // Disabled because of an issue with Terser breaking valid code: 102 | // https://github.com/facebook/create-react-app/issues/5250 103 | // Pending further investigation: 104 | // https://github.com/terser-js/terser/issues/120 105 | inline: 2, 106 | }, 107 | mangle: { 108 | safari10: true, 109 | }, 110 | // Added for profiling in devtools 111 | // keep_classnames: isEnvProductionProfile, 112 | // keep_fnames: isEnvProductionProfile, 113 | output: { 114 | comments: true, 115 | ecma: 5, 116 | // Turned on because emoji and regex is not minified properly using default 117 | // https://github.com/facebook/create-react-app/issues/2488 118 | // eslint-disable-next-line camelcase 119 | ascii_only: true, 120 | }, 121 | }, 122 | }), 123 | ], 124 | }, 125 | performance: { 126 | maxEntrypointSize: 512000, 127 | maxAssetSize: 512000, 128 | }, 129 | } 130 | --------------------------------------------------------------------------------