├── src ├── models │ ├── index.ts │ └── config.model.ts ├── routes │ ├── ping │ │ ├── index.ts │ │ └── ping.route.ts │ ├── upload │ │ ├── index.ts │ │ └── upload.route.ts │ └── index.ts ├── services │ ├── index.ts │ ├── localdb.ts │ ├── upload.ts │ └── logger.ts ├── index.ts └── server.ts ├── config ├── default.json ├── custom-environment-variables.json ├── test.json ├── development.json └── production.json ├── nodemon.json ├── .editorconfig ├── README.md ├── .prettierrc ├── pm2 ├── processes.dev.json └── processes.prod.json ├── tsconfig.json ├── .gitignore ├── tests ├── integration │ └── routes │ │ └── ping.spec.ts └── unit │ └── services │ └── upload.spec.ts ├── wallaby.config.js ├── LICENSE ├── webpack.config.js ├── tslint.json └── package.json /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config.model'; 2 | -------------------------------------------------------------------------------- /src/routes/ping/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ping.route'; 2 | -------------------------------------------------------------------------------- /src/routes/upload/index.ts: -------------------------------------------------------------------------------- 1 | export * from './upload.route'; 2 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "port": 3001 4 | } 5 | -------------------------------------------------------------------------------- /config/custom-environment-variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "password": "API_DB_PASSWORD" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './logger'; 2 | export * from './localdb'; 3 | export * from './upload'; 4 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "delay": "0", 3 | "execMap": { 4 | "ts": "ts-node" 5 | }, 6 | "events": { 7 | "start": "tslint -c ./tslint.json -t stylish 'src/**/*.ts'" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as config from 'config'; 2 | import { Server } from './server'; 3 | 4 | // create http server 5 | export const app = Server.bootstrap().app; 6 | export const server = app.listen(config.get('port')); 7 | -------------------------------------------------------------------------------- /config/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "database": "", 4 | "password": "", 5 | "requestTimeout": 60000, 6 | "server": "", 7 | "user": "" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "database": "", 4 | "password": "", 5 | "requestTimeout": 60000, 6 | "server": "", 7 | "user": "" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /config/production.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": { 3 | "database": "", 4 | "password": "", 5 | "requestTimeout": 60000, 6 | "server": "", 7 | "user": "" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeScript + Express + Node.js 2 | 3 | This is a repository to go with my article on creating an Express web application using TypeScript. 4 | 5 | ## Install 6 | 7 | Install the node packages via: 8 | 9 | `$ npm install` 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "jsxBracketSameLine": true, 5 | "printWidth": 120, 6 | "semi": true, 7 | "singleQuote": true, 8 | "tabWidth": 2, 9 | "trailingComma": "all", 10 | "useTabs": false 11 | } 12 | -------------------------------------------------------------------------------- /pm2/processes.dev.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "script": "index.js", 4 | "name": "api-dev", 5 | "exec_mode": "cluster", 6 | "node_args": "--harmony", 7 | "instances": 0, 8 | "env": { 9 | "NODE_ENV": "development", 10 | "PORT": 5001 11 | } 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /pm2/processes.prod.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "script": "index.js", 4 | "name": "api-prod", 5 | "exec_mode": "cluster", 6 | "node_args": "--harmony", 7 | "instances": 0, 8 | "env": { 9 | "NODE_ENV": "production", 10 | "PORT": 5000 11 | } 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /src/models/config.model.ts: -------------------------------------------------------------------------------- 1 | export interface IDatabaseConfig { 2 | database: string; 3 | password: string; 4 | requestTimeout: number; 5 | server: string; 6 | user: string; 7 | } 8 | 9 | export interface IConfig { 10 | db: IDatabaseConfig; 11 | env: string; 12 | name: string; 13 | port: number; 14 | version?: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/services/localdb.ts: -------------------------------------------------------------------------------- 1 | import * as Loki from 'lokijs'; 2 | 3 | export const loadLocalDB = (colName, db: Loki): Promise> => { 4 | return new Promise((resolve: any) => { 5 | db.loadDatabase({}, () => { 6 | const collection = db.getCollection(colName) || db.addCollection(colName); 7 | resolve(collection); 8 | }); 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /src/services/upload.ts: -------------------------------------------------------------------------------- 1 | import * as del from 'del'; 2 | 3 | export const fileFilter = (req, file, cb) => { 4 | // accept image only 5 | if (!file.originalname.match(/\.(jpg|jpeg|png|gif|xlsx|xls|csv|zip)$/)) { 6 | return cb(new Error('File format not allowed!'), false); 7 | } 8 | cb(undefined, true); 9 | }; 10 | 11 | export const cleanFolder = (folderPath: string) => { 12 | // delete files inside folder but not the folder itself 13 | del.sync([`${folderPath}/**`, `!${folderPath}`]); 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "esnext", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "outDir": "dist", 8 | "allowJs": true, 9 | "lib": ["dom", "es6"], 10 | "baseUrl": "./src", 11 | "paths": { 12 | "@/*": [ 13 | "./*" 14 | ] 15 | } 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | "dist" 20 | ], 21 | "include": [ 22 | "src/**/*.ts", 23 | "tests/**/*.spec.ts" 24 | ], 25 | "files": [ 26 | "src/index.ts" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Compiled binary addons (http://nodejs.org/api/addons.html) 7 | build/Release 8 | dist/ 9 | public/ 10 | 11 | # Dependency directory 12 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 13 | node_modules/ 14 | 15 | # OS 16 | .DS_Store 17 | 18 | # IDEs and editors 19 | /.idea 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | *.sublime-workspace 26 | 27 | # IDE - VSCode 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | -------------------------------------------------------------------------------- /tests/integration/routes/ping.spec.ts: -------------------------------------------------------------------------------- 1 | import { app, server } from '@/index'; 2 | import * as supertest from 'supertest'; 3 | 4 | describe('ping route', () => { 5 | afterEach(async () => { 6 | await server.close(); 7 | }); 8 | 9 | it('should return pong', (done) => { 10 | supertest(app) 11 | .get('/api/ping') 12 | .end((err: any, res: supertest.Response) => { 13 | if (err) { 14 | done(err); 15 | } else { 16 | expect(res.status).toBe(200); 17 | expect(res.body).toBe('pong'); 18 | done(); 19 | } 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /wallaby.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (webpack) => ({ 2 | files: [ 3 | { pattern: './src/**/*.ts', load: false }, 4 | ], 5 | 6 | tests: [ 7 | './tests/**/*.spec.ts', 8 | ], 9 | 10 | env: { 11 | type: 'node', 12 | }, 13 | 14 | compilers: { 15 | '**/*.ts': webpack.compilers.typeScript({ useStandardDefaults: true }), 16 | }, 17 | 18 | setup (wallaby) { 19 | const jestConfig = require('./package').jest || require('./jest.config'); 20 | delete jestConfig.transform['^.+\\.tsx?$']; 21 | wallaby.testFramework.configure(jestConfig); 22 | }, 23 | 24 | testFramework: 'jest', 25 | 26 | debug: true, 27 | }); 28 | -------------------------------------------------------------------------------- /tests/unit/services/upload.spec.ts: -------------------------------------------------------------------------------- 1 | import { fileFilter } from '@/services/upload'; 2 | 3 | describe('Upload Service', () => { 4 | const req = {}; 5 | const cb = jest.fn(); 6 | 7 | it('should check if the format is valid', () => { 8 | const file = { 9 | originalname: '.zip', 10 | }; 11 | 12 | fileFilter(req, file, cb); 13 | 14 | expect(cb).toBeCalledWith(undefined, true); 15 | }); 16 | 17 | it('should check if the format is invalid', () => { 18 | const file = { 19 | originalname: '.html', 20 | }; 21 | 22 | fileFilter(req, file, cb); 23 | 24 | expect(cb).toBeCalledWith(new Error('File format not allowed!'), false); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/routes/ping/ping.route.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@/services'; 2 | import { NextFunction, Request, Response, Router } from 'express'; 3 | 4 | export class PingRoute { 5 | public static path = '/ping'; 6 | private static instance: PingRoute; 7 | private router = Router(); 8 | 9 | private constructor() { 10 | logger.info('[PingRoute] Creating ping route.'); 11 | 12 | this.router.get('/', this.get); 13 | } 14 | 15 | static get router() { 16 | if (!PingRoute.instance) { 17 | PingRoute.instance = new PingRoute(); 18 | } 19 | return PingRoute.instance.router; 20 | } 21 | 22 | private get = async (req: Request, res: Response, next: NextFunction) => { 23 | res.json('pong'); 24 | next(); 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '@/services'; 2 | import { NextFunction, Request, Response, Router } from 'express'; 3 | import { PingRoute } from './ping'; 4 | import { UploadRoute } from './upload'; 5 | 6 | export class ApiRoutes { 7 | public static path = '/api'; 8 | private static instance: ApiRoutes; 9 | private router = Router(); 10 | 11 | private constructor() { 12 | logger.info('[ApiRoute] Creating api routes.'); 13 | 14 | this.router.get('/', this.get); 15 | this.router.use(PingRoute.path, PingRoute.router); 16 | this.router.use(UploadRoute.path, UploadRoute.router); 17 | } 18 | 19 | static get router() { 20 | if (!ApiRoutes.instance) { 21 | ApiRoutes.instance = new ApiRoutes(); 22 | } 23 | return ApiRoutes.instance.router; 24 | } 25 | 26 | private get = async (req: Request, res: Response, next: NextFunction) => { 27 | res.status(200).json({ online: true }); 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sul Aga 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 | 23 | -------------------------------------------------------------------------------- /src/services/logger.ts: -------------------------------------------------------------------------------- 1 | import * as debug from 'debug'; 2 | import * as fs from 'fs'; 3 | import * as winston from 'winston'; 4 | 5 | const PATHS = { 6 | LOG: `${process.cwd()}/logs`, 7 | LOG_ERROR: `${process.cwd()}/logs/_error.log`, 8 | LOG_INFO: `${process.cwd()}/logs/_info.log`, 9 | }; 10 | // ensure log directory exists 11 | (() => fs.existsSync(PATHS.LOG) || fs.mkdirSync(PATHS.LOG))(); 12 | 13 | export const dbg = debug('express:server'); 14 | 15 | export const logger = winston.createLogger({ 16 | exitOnError: false, 17 | format: winston.format.combine(winston.format.splat(), winston.format.simple()), 18 | transports: [ 19 | new winston.transports.File({ 20 | filename: PATHS.LOG_INFO, 21 | handleExceptions: true, 22 | level: 'info', 23 | maxFiles: 2, 24 | maxsize: 5242880, // 5MB 25 | }), 26 | new winston.transports.File({ 27 | filename: PATHS.LOG_ERROR, 28 | handleExceptions: true, 29 | level: 'error', 30 | maxFiles: 2, 31 | maxsize: 5242880, // 5MB 32 | }), 33 | new winston.transports.Console({ 34 | handleExceptions: true, 35 | level: 'debug', 36 | }), 37 | ], 38 | }); 39 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const NodemonPlugin = require('nodemon-webpack-plugin'); 2 | const merge = require('webpack-merge'); 3 | const path = require('path'); 4 | const env = process.env.NODE_ENV || 'development'; 5 | const fs = require('fs'); 6 | 7 | const externals = {}; 8 | fs.readdirSync('node_modules') 9 | .filter(x => ['.bin'].indexOf(x) === -1) 10 | .forEach(mod => { 11 | externals[mod] = `commonjs ${mod}`; 12 | }); 13 | 14 | const baseConfig = { 15 | externals, 16 | context: path.resolve('./src'), 17 | target: 'node', 18 | mode: env, 19 | entry: { 20 | index: './index.ts', 21 | }, 22 | output: { 23 | path: path.resolve('./dist'), 24 | filename: '[name].js', 25 | sourceMapFilename: '[name].map', 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.tsx?$/, 31 | use: 'ts-loader', 32 | exclude: /node_modules/, 33 | }, 34 | ], 35 | }, 36 | resolve: { 37 | extensions: ['.ts', '.js'], 38 | modules: [path.resolve('./src'), 'node_modules'], 39 | alias: { 40 | '@': path.resolve(__dirname, './src'), 41 | }, 42 | }, 43 | }; 44 | 45 | const developmentConfig = { 46 | devtool: 'cheap-eval-source-map', 47 | plugins: [new NodemonPlugin()], 48 | }; 49 | 50 | const productionConfig = { 51 | plugins: [], 52 | }; 53 | 54 | module.exports = merge(baseConfig, env === 'development' ? developmentConfig : productionConfig); 55 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended" 4 | ], 5 | "rules": { 6 | "class-name": true, 7 | "comment-format": [ 8 | true, 9 | "check-space" 10 | ], 11 | "indent": [ 12 | true, 13 | "spaces" 14 | ], 15 | "one-line": [ 16 | true, 17 | "check-open-brace", 18 | "check-whitespace" 19 | ], 20 | "no-var-keyword": true, 21 | "quotemark": [ 22 | true, 23 | "single", 24 | "avoid-escape" 25 | ], 26 | "semicolon": [ 27 | true, 28 | "always", 29 | "ignore-bound-class-methods" 30 | ], 31 | "whitespace": [ 32 | true, 33 | "check-branch", 34 | "check-decl", 35 | "check-operator", 36 | "check-module", 37 | "check-separator", 38 | "check-type" 39 | ], 40 | "typedef-whitespace": [ 41 | true, 42 | { 43 | "call-signature": "nospace", 44 | "index-signature": "nospace", 45 | "parameter": "nospace", 46 | "property-declaration": "nospace", 47 | "variable-declaration": "nospace" 48 | }, 49 | { 50 | "call-signature": "onespace", 51 | "index-signature": "onespace", 52 | "parameter": "onespace", 53 | "property-declaration": "onespace", 54 | "variable-declaration": "onespace" 55 | } 56 | ], 57 | "max-line-length": [false], 58 | "no-internal-module": true, 59 | "no-trailing-whitespace": true, 60 | "no-null-keyword": true, 61 | "space-before-function-paren": false, 62 | "prefer-const": true, 63 | "jsdoc-format": true 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from 'body-parser'; 2 | import * as compression from 'compression'; 3 | import * as cors from 'cors'; 4 | import * as errorHandler from 'errorhandler'; 5 | import * as express from 'express'; 6 | import * as expressStatusMonitor from 'express-status-monitor'; 7 | import * as helmet from 'helmet'; 8 | import * as methodOverride from 'method-override'; 9 | import * as morgan from 'morgan'; 10 | import * as path from 'path'; 11 | 12 | import { ApiRoutes } from './routes'; 13 | import { logger } from './services'; 14 | 15 | export class Server { 16 | public static bootstrap(): Server { 17 | return new Server(); 18 | } 19 | 20 | public app: express.Application; 21 | 22 | constructor() { 23 | // create expressjs application 24 | this.app = express(); 25 | 26 | // configure application 27 | this.config(); 28 | 29 | // add routes 30 | this.routes(); 31 | } 32 | 33 | public config() { 34 | // add static paths 35 | this.app.use(express.static(path.join(__dirname, 'public'))); 36 | 37 | // mount logger 38 | this.app.use( 39 | morgan('tiny', { 40 | stream: { 41 | write: (message: string) => logger.info(message.trim()), 42 | }, 43 | } as morgan.Options), 44 | ); 45 | 46 | // mount json form parser 47 | this.app.use(bodyParser.json({ limit: '50mb' })); 48 | 49 | // mount query string parser 50 | this.app.use( 51 | bodyParser.urlencoded({ 52 | extended: true, 53 | }), 54 | ); 55 | 56 | // mount override? 57 | this.app.use(helmet()); 58 | this.app.use(cors()); 59 | this.app.use(compression()); 60 | this.app.use(methodOverride()); 61 | this.app.use(expressStatusMonitor()); 62 | 63 | // catch 404 and forward to error handler 64 | this.app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { 65 | err.status = 404; 66 | next(err); 67 | }); 68 | 69 | // error handling 70 | this.app.use(errorHandler()); 71 | } 72 | 73 | private routes() { 74 | // use router middleware 75 | this.app.use(ApiRoutes.path, ApiRoutes.router); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "a seed project for creating http service using express written in typescript", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "jest", 8 | "start": "npm run dev", 9 | "prod": "cross-env NODE_ENV=production webpack --watch", 10 | "dev": "webpack --watch", 11 | "build": "npm run build:dev", 12 | "build:dev": "webpack --config ./webpack.config.js --progress --profile --color --display-error-details --display-cached && npm run copy-files", 13 | "build:prod": "cross-env NODE_ENV=production webpack --config ./webpack.config.js --progress --profile --color --display-error-details --display-cached --bail && npm run copy-files", 14 | "copy-files": "node build/copyFiles.js", 15 | "clean": "npm cache clear && rimraf -- dist" 16 | }, 17 | "keywords": [], 18 | "author": "Helmuth Saatkamp", 19 | "license": "MIT", 20 | "dependencies": { 21 | "body-parser": "^1.19.0", 22 | "compression": "^1.7.4", 23 | "config": "^3.2.5", 24 | "cookie-parser": "^1.4.4", 25 | "cors": "^2.8.5", 26 | "debug": "^4.1.1", 27 | "del": "^5.1.0", 28 | "errorhandler": "^1.5.1", 29 | "express": "^4.17.1", 30 | "express-session": "^1.17.0", 31 | "express-status-monitor": "^1.2.8", 32 | "helmet": "^3.21.2", 33 | "lodash": "^4.17.15", 34 | "lokijs": "^1.5.8", 35 | "method-override": "^3.0.0", 36 | "morgan": "^1.9.1", 37 | "multer": "^1.4.2", 38 | "rxjs": "^6.5.4", 39 | "winston": "^3.2.1" 40 | }, 41 | "devDependencies": { 42 | "@reactivex/rxjs": "^6.5.4", 43 | "@types/body-parser": "^1.17.1", 44 | "@types/compression": "^1.0.1", 45 | "@types/config": "^0.0.36", 46 | "@types/cookie-parser": "^1.4.2", 47 | "@types/debug": "^4.1.5", 48 | "@types/del": "^4.0.0", 49 | "@types/errorhandler": "^0.0.32", 50 | "@types/express": "^4.17.2", 51 | "@types/express-session": "^1.15.16", 52 | "@types/helmet": "^0.0.45", 53 | "@types/jest": "^25.1.2", 54 | "@types/lodash": "^4.14.149", 55 | "@types/lokijs": "^1.5.3", 56 | "@types/method-override": "^0.0.31", 57 | "@types/morgan": "^1.7.37", 58 | "@types/multer": "^1.4.2", 59 | "@types/node": "^12.12.26", 60 | "@types/supertest": "^2.0.8", 61 | "@types/webpack": "^4.41.5", 62 | "cross-env": "^7.0.0", 63 | "jest": "^25.1.0", 64 | "nodemon": "^2.0.2", 65 | "nodemon-webpack-plugin": "^4.2.2", 66 | "shelljs": "^0.8.3", 67 | "supertest": "^4.0.2", 68 | "ts-jest": "^25.2.0", 69 | "ts-loader": "^6.2.1", 70 | "ts-node": "^8.6.2", 71 | "tslint": "^6.0.0", 72 | "typescript": "^3.7.5", 73 | "webpack": "^4.41.5", 74 | "webpack-cli": "^3.3.10", 75 | "webpack-merge": "^4.2.2" 76 | }, 77 | "jest": { 78 | "moduleFileExtensions": [ 79 | "ts", 80 | "js", 81 | "json" 82 | ], 83 | "moduleNameMapper": { 84 | "^@/(.*)$": "/src/$1" 85 | }, 86 | "transform": { 87 | "^.+\\.(ts|js)x?$": "ts-jest" 88 | }, 89 | "testMatch": [ 90 | "/tests/**/*.spec.(ts|js)" 91 | ], 92 | "testEnvironment": "node" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/routes/upload/upload.route.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response, Router } from 'express'; 2 | import * as fs from 'fs'; 3 | import * as Loki from 'lokijs'; 4 | import * as multer from 'multer'; 5 | import * as path from 'path'; 6 | import { fileFilter, loadLocalDB, logger } from '../../services'; 7 | 8 | const DB_NAME = 'db.json'; 9 | const COLLECTION_NAME = 'files'; 10 | const UPLOAD_PATH = 'public/uploads'; 11 | const upload = multer({ dest: `${UPLOAD_PATH}/`, fileFilter }); 12 | const db = new Loki(`${UPLOAD_PATH}/${DB_NAME}`, { persistenceMethod: 'fs' }); 13 | 14 | export class UploadRoute { 15 | public static path = '/upload'; 16 | private static instance: UploadRoute; 17 | private router = Router(); 18 | 19 | private constructor() { 20 | logger.info('[UploadRoute] Creating Upload route.'); 21 | 22 | this.router.get('/file/:id', this.getFile); 23 | this.router.get('/files', this.getFiles); 24 | this.router.post('/file', upload.single(''), this.addFile); 25 | this.router.post('/files', upload.array(''), this.addFiles); 26 | } 27 | 28 | static get router() { 29 | if (!UploadRoute.instance) { 30 | UploadRoute.instance = new UploadRoute(); 31 | } 32 | return UploadRoute.instance.router; 33 | } 34 | 35 | private getFile = async (req: Request, res: Response, next: NextFunction) => { 36 | try { 37 | const col = await loadLocalDB(COLLECTION_NAME, db); 38 | const id: any = req.params.id; 39 | 40 | const result = col.get(id); 41 | 42 | if (!result) { 43 | res.sendStatus(404); 44 | return; 45 | } 46 | 47 | res.setHeader('Content-Type', result.mimetype); 48 | fs.createReadStream(path.join(UPLOAD_PATH, result.filename)).pipe(res); 49 | } catch (err) { 50 | logger.error(err); 51 | res.sendStatus(400); 52 | } 53 | }; 54 | 55 | private getFiles = async (req: Request, res: Response, next: NextFunction) => { 56 | try { 57 | const col = await loadLocalDB(COLLECTION_NAME, db); 58 | res.send(col.data); 59 | } catch (err) { 60 | logger.error(err); 61 | res.sendStatus(400); 62 | } 63 | }; 64 | 65 | private addFile = async (req: Request, res: Response, next: NextFunction) => { 66 | try { 67 | const col = await loadLocalDB(COLLECTION_NAME, db); 68 | const data = col.insert(req.file); 69 | 70 | db.saveDatabase(); 71 | res.send({ 72 | fileName: data.filename, 73 | id: data.$loki, 74 | originalName: data.originalname, 75 | }); 76 | } catch (err) { 77 | logger.error(err); 78 | res.sendStatus(400); 79 | } 80 | }; 81 | 82 | private addFiles = async (req: Request, res: Response, next: NextFunction) => { 83 | try { 84 | const col = await loadLocalDB(COLLECTION_NAME, db); 85 | const data = [].concat(col.insert(req.files)); 86 | 87 | db.saveDatabase(); 88 | res.send( 89 | data.map((x: any) => ({ 90 | fileName: x.filename, 91 | id: x.$loki, 92 | originalName: x.originalname, 93 | })), 94 | ); 95 | } catch (err) { 96 | logger.error(err); 97 | res.sendStatus(400); 98 | } 99 | }; 100 | } 101 | --------------------------------------------------------------------------------