├── README.md ├── setupJest.ts ├── .gitattributes ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── src ├── shared │ ├── types │ │ └── json.d.ts │ ├── middlewares │ │ ├── index.ts │ │ ├── rest-logger.middleware.ts │ │ └── auth.middleware.ts │ ├── exceptions │ │ ├── index.ts │ │ ├── auth-exceptions.filter.ts │ │ ├── database-exceptions.filter.ts │ │ ├── http-exceptions.spec.ts │ │ └── http-exceptions.ts │ ├── environments.spec.ts │ ├── shared.module.ts │ ├── environments.ts │ └── mongoose │ │ ├── mongoose.confg.ts │ │ └── mongoose.service.ts ├── app │ ├── app.module.ts │ ├── hero │ │ ├── heros.module.ts │ │ ├── heros.service.ts │ │ ├── heros.spec.ts │ │ ├── heros.controller.ts │ │ └── heros.model.ts │ ├── app.config.ts │ ├── app.component.ts │ └── app.bootstrap.ts └── server.ts ├── .ssl ├── README.md ├── selfsigned.js ├── server.crt └── server.key ├── nodemon.json ├── .editorconfig ├── process.yml ├── tsconfig.spec.json ├── tsconfig.json ├── .env.example ├── Dockerfile ├── .gitignore ├── .circleci └── config.yml ├── tslint.json ├── package.json └── LICENSE /README.md: -------------------------------------------------------------------------------- 1 | # nest-typescript-starter -------------------------------------------------------------------------------- /setupJest.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | package-lock.json -diff 4 | bin/* eol=lf 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /src/shared/types/json.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | let json: any; 3 | export default json; 4 | } 5 | -------------------------------------------------------------------------------- /src/shared/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.middleware'; 2 | export * from './rest-logger.middleware'; 3 | -------------------------------------------------------------------------------- /src/shared/exceptions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth-exceptions.filter'; 2 | export * from './database-exceptions.filter'; 3 | export * from './http-exceptions'; 4 | -------------------------------------------------------------------------------- /.ssl/README.md: -------------------------------------------------------------------------------- 1 | ## Development only SSL certificates ( Never deploy these to the web, ever! ) 2 | 3 | #### Generating certificates 4 | 5 | - Change localhostnames in `selfsigned.js` 6 | 7 | - execute `node selfsigned.js` 8 | 9 | - Accept certificate in keychain -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "delay": "0", 3 | "execMap": { 4 | "ts": "ts-node" 5 | }, 6 | "events": { 7 | "start": "tslint --project tsconfig.json -c ./tslint.json 'src/**/*.ts' --exclude 'src/**/*.d.ts' --exclude 'src/**/*.spec.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 | 11 | [*.md] 12 | insert_final_newline = false 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "npm", 4 | "isShellCommand": true, 5 | "suppressTaskName": true, 6 | "tasks": [ 7 | { 8 | "taskName": "build", 9 | "isBuildCommand": true, 10 | "args": ["run", "build"] 11 | }, 12 | { 13 | "taskName": "test", 14 | "isTestCommand": true, 15 | "args": ["test"] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /process.yml: -------------------------------------------------------------------------------- 1 | 2 | apps: 3 | - script: ./nest-server/server.js 4 | name: 'nest-server' 5 | instances: 1 6 | exec_mode: cluster 7 | out_file: '/tmp/nestServer.log' 8 | log_date_format: 'MM/DD/YYYY HH:mm:ss' 9 | max_restarts: 5 10 | node_args: '--max-old-space-size=3072' 11 | env: 12 | NODE_ENV: development 13 | env_production: 14 | NODE_ENV: production 15 | -------------------------------------------------------------------------------- /src/shared/environments.spec.ts: -------------------------------------------------------------------------------- 1 | import { Environments } from './environments'; 2 | 3 | describe('Environment', () => { 4 | test('getName() should return the test env', () => { 5 | expect(Environments.getEnv()).toBe('test'); 6 | }); 7 | 8 | test('isTest() should be true', () => { 9 | expect(Environments.isTest()).toBeTruthy(); 10 | }); 11 | 12 | test('isDevelopment() should be false', () => { 13 | expect(Environments.isDev()).toBeFalsy(); 14 | }); 15 | 16 | test('isProduction() should be false', () => { 17 | expect(Environments.isProd()).toBeFalsy(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HerosModule } from './hero/heros.module'; 2 | import { Module } from '@nestjs/common'; 3 | 4 | import { SharedModule } from '../shared/shared.module'; 5 | 6 | import { AppBootstrap } from './app.bootstrap'; 7 | import { AppComponent } from './app.component'; 8 | import { AppConfiguration } from './app.config'; 9 | 10 | 11 | @Module({ 12 | modules: [ 13 | SharedModule, 14 | HerosModule, 15 | ], 16 | controllers: [], 17 | components: [AppComponent, AppBootstrap, AppConfiguration], 18 | exports: [AppComponent, AppBootstrap, AppConfiguration], 19 | }) 20 | export class AppModule {} 21 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "declaration": false, 6 | "importHelpers": true, 7 | "noImplicitAny": false, 8 | "noImplicitThis": true, 9 | "noUnusedLocals": true, 10 | "removeComments": true, 11 | "noLib": false, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "target": "es6", 15 | "lib": [ 16 | "es6", 17 | "es2016.array.include" 18 | ], 19 | "sourceMap": true, 20 | "allowJs": true, 21 | "pretty": true 22 | }, 23 | "files": ["setupJest.ts"], 24 | "include": ["**/*.spec.ts", "**/*.d.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": false, 5 | "importHelpers": true, 6 | "noImplicitAny": false, 7 | "noImplicitThis": true, 8 | "noUnusedLocals": true, 9 | "removeComments": true, 10 | "noLib": false, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "target": "es6", 14 | "lib": ["es6", "es2016.array.include"], 15 | "sourceMap": true, 16 | "allowJs": true, 17 | "pretty": true, 18 | "outDir": "./dist" 19 | }, 20 | "typeRoots": ["node_modules/@types", "src/core/types/*"], 21 | "include": ["src/**/*"], 22 | "exclude": ["node_modules", "**/*.spec.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /src/shared/middlewares/rest-logger.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Logger, Middleware, NestMiddleware } from '@nestjs/common'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | import * as chalk from 'chalk'; 4 | 5 | @Middleware() 6 | export class RestLoggerMiddleware implements NestMiddleware { 7 | private logger = new Logger('Request'); 8 | public resolve() { 9 | return (req: Request, res: Response, next: NextFunction) => { 10 | this.logger.log( 11 | `[${chalk.white(req.method)}] ${chalk.cyan(res.statusCode.toString())} ` + 12 | `${chalk.white('|')} ${chalk.cyan(req.httpVersion)} ${chalk.white('|')} ${chalk.cyan(req.ip)} ` + 13 | `[${chalk.white('route:', req.path)}]`); 14 | next(); 15 | }; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/shared/middlewares/auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware, NestMiddleware } from '@nestjs/common'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | 4 | const jwt = require('express-jwt'); 5 | const jwksRsa = require('jwks-rsa'); 6 | 7 | @Middleware() 8 | export class AuthMiddleware implements NestMiddleware { 9 | public resolve(): (req: Request, res: Response, next: NextFunction) => void { 10 | const secret = jwksRsa.expressJwtSecret({ 11 | cache: true, 12 | rateLimit: true, 13 | jwksRequestsPerMinute: 60, 14 | jwksUri: process.env.AUTH_JWKS_URL, 15 | }); 16 | return jwt({ 17 | secret: secret, 18 | aud: process.env.AUTH_AUDIENCE, 19 | issuer: process.env.AUTH_ISSUER, 20 | algorithms: ['RS256'], 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/hero/heros.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, RequestMethod, MiddlewaresConsumer } from '@nestjs/common'; 2 | 3 | import { SharedModule } from '../../shared/shared.module'; 4 | import { AuthMiddleware } from '../../shared/middlewares'; 5 | 6 | import { HerosController } from './heros.controller'; 7 | import { HerosModel } from './heros.model'; 8 | import { HerosService } from './heros.service'; 9 | 10 | @Module({ 11 | components: [HerosService, HerosModel], 12 | controllers: [HerosController], 13 | modules: [SharedModule], 14 | exports: [HerosService, HerosModel], 15 | }) 16 | export class HerosModule { 17 | public configure(consumer: MiddlewaresConsumer) { 18 | consumer 19 | .apply(AuthMiddleware) 20 | .with([]) 21 | .forRoutes({ path: '*', method: RequestMethod.ALL }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@nestjs/common'; 2 | 3 | import * as bodyParser from 'body-parser'; 4 | import * as compression from 'compression'; 5 | import * as cors from 'cors'; 6 | import * as express from 'express'; 7 | import * as helmet from 'helmet'; 8 | 9 | @Component() 10 | export class AppConfiguration { 11 | public configure(express: express.Application) { 12 | express 13 | .options('*', cors()) 14 | .use(cors()) 15 | .use(helmet()) 16 | .use(helmet.noCache()) 17 | .use( 18 | helmet.hsts({ 19 | maxAge: 15768000, 20 | includeSubdomains: true, 21 | }), 22 | ) 23 | .use(compression()) 24 | .use(bodyParser.json()) 25 | .use( 26 | bodyParser.urlencoded({ 27 | extended: true, 28 | }), 29 | ); 30 | return express; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 2 | # Main Application Configuration 3 | # 4 | APP_NAME="nestjs-mongoose" 5 | APP_HOST="https://localhost" 6 | APP_URL_PREFIX="/api/v1" 7 | APP_PORT=4433 8 | TLS_KEY_PATH="./.ssl/server.key" 9 | TLS_CERT_PATH="./.ssl/server.crt" 10 | TLS_CA_PATH="" 11 | 12 | # 13 | # Auth0 14 | # 15 | AUTH_ISSUER="" 16 | AUTH_AUDIENCE="" 17 | AUTH_JWKS_URL="" 18 | 19 | # 20 | # Redis 21 | # 22 | REDIS_HOST="localhost" 23 | REDIS_PORT=6379 24 | 25 | # 26 | # Database 27 | # 28 | # MongoDB Replica Set 29 | MONGO_HOST0="someUrl-rs0-shard-00-00.mongodb.net" 30 | MONGO_HOST1="someUrl-rs0-shard-00-01.mongodb.net" 31 | MONGO_HOST2="someUrl-rs0-shard-00-02.mongodb.net" 32 | MONGO_PORT=27017 33 | MONGO_USER= 34 | MONGO_PASS= 35 | MONGO_DB="nest_db" 36 | MONGO_REPLICA_SET="nest-db-rs0-shard-0" 37 | 38 | # MongoDB Instance 39 | #MONGO_HOST0="localhost" 40 | #MONGO_PORT=27017 41 | #MONGO_USER= 42 | #MONGO_PASS= 43 | #MONGO_DB="nest_db" 44 | -------------------------------------------------------------------------------- /src/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { MiddlewaresConsumer, Module, RequestMethod } from '@nestjs/common'; 2 | 3 | import { Environments } from './environments'; 4 | import { DatabaseExceptionFilter } from './exceptions'; 5 | import { RestLoggerMiddleware, AuthMiddleware } from './middlewares'; 6 | import { MongooseService } from './mongoose/mongoose.service'; 7 | 8 | @Module({ 9 | modules: [], 10 | controllers: [], 11 | components: [ 12 | AuthMiddleware, 13 | DatabaseExceptionFilter, 14 | RestLoggerMiddleware, 15 | Environments, 16 | MongooseService 17 | ], 18 | exports: [ 19 | AuthMiddleware, 20 | DatabaseExceptionFilter, 21 | RestLoggerMiddleware, 22 | Environments, 23 | MongooseService 24 | ], 25 | }) 26 | export class SharedModule { 27 | public configure(consumer: MiddlewaresConsumer) { 28 | consumer.apply(RestLoggerMiddleware).forRoutes({ path: '*', method: RequestMethod.ALL }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.ssl/selfsigned.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const selfsigned = require('selfsigned'); 3 | const attrs = [{ name: 'commonName', value: 'deviantjs.local' }]; 4 | const pems = selfsigned.generate(attrs, { 5 | algorithm: 'sha256', 6 | keySize: 2048, 7 | extensions: [ 8 | { 9 | name: 'subjectAltName', 10 | altNames: [ 11 | { 12 | type: 2, // DNS 13 | value: 'localhost', 14 | }, 15 | { 16 | type: 2, // DNS 17 | value: 'deviantjs.local', // local hostname 18 | }, 19 | { 20 | type: 6, // URI 21 | value: 'https://deviantjs.local', // and again 22 | }, 23 | { 24 | type: 7, // IP 25 | ip: '127.0.0.1', 26 | }, 27 | ], 28 | }, 29 | ], 30 | }); 31 | 32 | fs.writeFileSync('./server.crt', pems.cert, { encoding: 'utf-8' }); 33 | fs.writeFileSync('./server.key', pems.private, { encoding: 'utf-8' }); 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM d3vaint0ne/docker-node8-lts:latest 2 | MAINTAINER d3viant0ne 3 | 4 | ENV APP_ENV production 5 | 6 | # Set environment variables 7 | RUN mkdir -p /var/www/app/current/nest-server 8 | ENV appDir /var/www/app/current 9 | 10 | COPY package.json /var/www/app/current 11 | 12 | WORKDIR ${appDir} 13 | RUN npm i --production 14 | 15 | # Copy production build files 16 | # ... 17 | COPY ./dist /var/www/app/current/nest-server 18 | 19 | # PM2 Configuration 20 | # ... 21 | COPY ./process.yml /var/www/app/current 22 | 23 | # DotEnv Configuration 24 | # ... 25 | COPY ./.env.example /var/www/app/current/.env 26 | 27 | #ENV KEYMETRICS_SECRET 28 | #ENV KEYMETRICS_PUBLIC 29 | #ENV INSTANCE_NAME "" 30 | 31 | #Expose the ports ( Nest http2/s, socket.io, keymetrics ) 32 | EXPOSE 4433 43554 80 33 | 34 | CMD ["pm2-docker", "start", "--auto-exit", "--env", "production", "process.yml"] 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE - VSCode 2 | .vscode/* 3 | !.vscode/settings.json 4 | !.vscode/tasks.json 5 | !.vscode/launch.json 6 | !.vscode/extensions.json 7 | 8 | # IDE - Other 9 | /.idea 10 | .project 11 | .classpath 12 | .c9/ 13 | *.launch 14 | .settings/ 15 | 16 | # Dependency directories 17 | node_modules/ 18 | jspm_packages/ 19 | /false 20 | 21 | # Typescript v1 declaration files 22 | typings/ 23 | 24 | # Optional npm cache directory 25 | .npm 26 | 27 | # Optional eslint cache 28 | .eslintcache 29 | 30 | # Compiled output 31 | /dist 32 | /tmp 33 | /output/ 34 | /compiled/ 35 | .awcache/ 36 | dll/ 37 | .ssl/certs/* 38 | 39 | # Logs 40 | logs 41 | *.log 42 | npm-debug.log* 43 | yarn-debug.log* 44 | yarn-error.log* 45 | 46 | # Runtime data 47 | pids 48 | *.pid 49 | *.seed 50 | *.pid.lock 51 | 52 | # System Files 53 | .DS_Store 54 | Thumbs.db 55 | 56 | # Coverage directory used by tools like istanbul 57 | coverage 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # dotenv environment variables file 63 | .env 64 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/nest-typescript-starter 5 | docker: 6 | - image: d3viant0ne/circleci-node8-base:latest 7 | branches: 8 | only: 9 | - master 10 | - develop 11 | steps: 12 | - checkout 13 | - setup_remote_docker 14 | - restore_cache: 15 | key: dependency-cache-{{ checksum "package.json" }} 16 | - run: 17 | name: install-npm 18 | command: npm install 19 | - save_cache: 20 | key: dependency-cache-{{ checksum "package.json" }} 21 | paths: 22 | - ./node_modules 23 | - run: 24 | name: Unit Tests 25 | command: npm run test:coverage 26 | - run: 27 | name: Code Linting 28 | command: npm run lint 29 | - run: 30 | name: Security Scan 31 | command: npm run security 32 | - run: 33 | name: Report Code Coverage 34 | command: npm run report-codeclimate 35 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { Component } from '@nestjs/common'; 3 | 4 | import * as dotenv from 'dotenv'; 5 | import * as express from 'express'; 6 | 7 | import { Environments } from '../shared/environments'; 8 | import { AppBootstrap } from './app.bootstrap'; 9 | import { AppConfiguration } from './app.config'; 10 | 11 | export interface Configuration { 12 | configure(app: AppComponent): void; 13 | } 14 | 15 | @Component() 16 | export class AppComponent { 17 | private readonly logger = new Logger(AppComponent.name); 18 | private express: express.Application = express(); 19 | private appBootstrap = new AppBootstrap(); 20 | private appConfig = new AppConfiguration(); 21 | 22 | constructor() { 23 | Environments.isDev() ? dotenv.config() : null; // tslint:disable-line 24 | } 25 | 26 | public bootstrap() { 27 | this.logger.log('Configuring Express Options'); 28 | this.appBootstrap.expressAppDefinition(this.express); 29 | this.appConfig.configure(this.express); 30 | return this.express; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/shared/environments.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@nestjs/common'; 2 | 3 | import * as packageInfo from '../../package.json'; 4 | 5 | @Component() 6 | export class Environments { 7 | public static getEnv(): string { 8 | return process.env.NODE_ENV || 'development'; 9 | } 10 | 11 | public static getPackageInfo(): any { 12 | return packageInfo; 13 | } 14 | 15 | public static isTest(): boolean { 16 | return this.getEnv() === 'test'; 17 | } 18 | 19 | public static isDev(): boolean { 20 | return this.getEnv() === 'development'; 21 | } 22 | 23 | public static isProd(): boolean { 24 | return this.getEnv() === 'production'; 25 | } 26 | 27 | public static getRedisHost(): string { 28 | return process.env.REDIS_HOST || 'localhost'; 29 | } 30 | 31 | public static getRedisPort(): string { 32 | return process.env.REDIS_HOST || '6379'; 33 | } 34 | 35 | public static isEnabled(bool: string): boolean { 36 | try { 37 | return bool.toLowerCase() === 'true'; 38 | } catch (e) { 39 | return false; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/hero/heros.service.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@nestjs/common'; 2 | 3 | import { Hero, HerosModel } from './heros.model'; 4 | 5 | @Component() 6 | export class HerosService { 7 | constructor(private herosModel: HerosModel) {} 8 | 9 | public async findAll(): Promise { 10 | return await this.repository.find(); 11 | } 12 | 13 | public async findById(id: string): Promise { 14 | return await this.repository.findById(id); 15 | } 16 | 17 | public async findByName(name: string): Promise { 18 | return await this.repository.findByName(name); 19 | } 20 | 21 | public async findByAlignment(alignment: string): Promise { 22 | return await this.repository.findByAlignment(alignment); 23 | } 24 | 25 | public async create(hero: Hero) { 26 | const newHero = await this.repository.create(hero); 27 | return newHero; 28 | } 29 | 30 | public async delete(id: string) { 31 | return this.repository.findByIdAndRemove(id); 32 | } 33 | 34 | private get repository() { 35 | return this.herosModel.herosRepository(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.ssl/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC/zCCAeegAwIBAgIJQaS50zgGn941MA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNV 3 | BAMTD2RldmlhbnRqcy5sb2NhbDAeFw0xNzA5MjIwODM5MzNaFw0xODA5MjIwODM5 4 | MzNaMBoxGDAWBgNVBAMTD2RldmlhbnRqcy5sb2NhbDCCASIwDQYJKoZIhvcNAQEB 5 | BQADggEPADCCAQoCggEBAKkdyuoSg5sOa4stxKvNwGbCWM/YD2cgZWu1SSOwJBC/ 6 | FXe6RdSKO5CcPBlT9oik2bx62D2RsJzxaNu/xfYL1HKldOUHxu/nTOKAUJLDkEwq 7 | rGBoyb5V9cWmb8OPVbSBAKjRc1KAl/QunJMvzUMypAfmA44zAbQcyc7yj1YggOis 8 | 6PVkDcdnpX0DDO6pfP/ohFAaKHqbFeATwln7++IR558fnmbUtaeH56dAi+AT0uot 9 | k3/rvh2EZ03wB639vi78KaL5JpFEBjC35luAUT7OIebv6qzaQFkYUwbFFqhbEv7/ 10 | d7OY8c9ppDweVVtolZR0WOr05doXrf+3zzV8MunddtkCAwEAAaNIMEYwRAYDVR0R 11 | BD0wO4IJbG9jYWxob3N0gg9kZXZpYW50anMubG9jYWyGF2h0dHBzOi8vZGV2aWFu 12 | dGpzLmxvY2FshwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQADIWzrczZNvOz0KiDR 13 | BKXpaGXivOhWYbkz5j98z0dK8m17uL+cy0Pm+q0GoeGP6b1asACpcPsnUARmW5gt 14 | Yrj62ZzhVwLYqTH6xQCc4aQc/OCE/kF3zyzgnWUOmN7ElQfsvjcpRXiM37N8Fbqj 15 | raRBmw8STcuGroAV7oNA66ZBzT5ArRZGZ/E9/bVH/bCCNScNRw7/+BKlCJ948GEW 16 | 2C8CDIWr1kQqVDHprx4GzSQLg9gwTmLgpUM0clWv1uAvvrV9rPKFadfKUDWcqV99 17 | MOXzfPRwFFW6pwCAdEfeiFPI7+qg8dM75+y9jGUkF6QPvB1+UXtg3/cd8W64Tm5N 18 | 9bZr 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /src/app/app.bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@nestjs/common'; 2 | 3 | import * as express from 'express'; 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | 7 | import { Environments } from '../shared/environments'; 8 | 9 | @Component() 10 | export class AppBootstrap { 11 | private isProd: any = false; 12 | public expressAppDefinition(app: express.Application): express.Application { 13 | app.set('prefix', process.env.APP_URL_PREFIX); 14 | app.set('host', process.env.APP_HOST); 15 | app.set('port', this.normalizedPort(process.env.APP_PORT || '3000')); 16 | app.set('key', fs.readFileSync(path.resolve(`${process.env.TLS_KEY_PATH}`))); 17 | app.set('cert', fs.readFileSync(path.resolve(`${process.env.TLS_CERT_PATH}`))); 18 | this.isProd = Environments.isProd() ? app.set('ca', fs.readFileSync(path.resolve(`${process.env.TLS_CA_PATH}`))) : true; 19 | return app; 20 | } 21 | 22 | public normalizedPort(port: string): number | string { 23 | const portAsNumber = parseInt(port, 10); 24 | if (isNaN(portAsNumber)) { 25 | return port; 26 | } 27 | if (portAsNumber >= 0) { 28 | return portAsNumber; 29 | } 30 | return; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/shared/mongoose/mongoose.confg.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable max-line-length 2 | import { Component } from '@nestjs/common'; 3 | import { Logger } from '@nestjs/common' 4 | 5 | @Component() 6 | export class MongooseConfig { 7 | private readonly logger = new Logger(MongooseConfig.name); 8 | 9 | public configure(): string { 10 | let connectionString = ''; 11 | const mongoUser: string = process.env.MONGO_USER; 12 | const mongoPass: string = process.env.MONGO_PASS; 13 | const mongoRepSetUri = `${process.env.MONGO_HOST0}:${process.env.MONGO_PORT},${process.env.MONGO_HOST1}:${process.env.MONGO_PORT},${process.env.MONGO_HOST2}:${process.env.MONGO_PORT}`; 14 | const mongoDevUri = `${process.env.MONGO_HOST0}:${process.env.MONGO_PORT}`; 15 | const mongoDbName: string = process.env.MONGO_DB; 16 | const mongoReplicaSet: string = process.env.MONGO_REPLICA_SET; 17 | 18 | this.logger.log('Configuring Mongoose Options'); 19 | return (connectionString = mongoReplicaSet 20 | ? `mongodb://${mongoUser}:${mongoPass}@${mongoRepSetUri}/${mongoDbName}?ssl=true&replicaSet=${mongoReplicaSet}&authSource=admin` 21 | : `mongodb://${mongoDevUri}/${mongoDbName}?authSource=admin`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/shared/mongoose/mongoose.service.ts: -------------------------------------------------------------------------------- 1 | import { Component, Logger } from '@nestjs/common'; 2 | import * as mongoose from 'mongoose'; 3 | 4 | import { Environments } from '../environments'; 5 | 6 | import { MongooseConfig } from './mongoose.confg'; 7 | 8 | @Component() 9 | export class MongooseService { 10 | private readonly logger = new Logger(MongooseService.name); 11 | 12 | private instance: mongoose.Connection; 13 | 14 | constructor() { 15 | (mongoose as any).Promise = global.Promise; 16 | } 17 | 18 | get connection() { 19 | if (Environments.isTest()) { return this.instance = mongoose.connection; }; 20 | if (this.instance) { 21 | return this.instance; 22 | } else { 23 | mongoose.connect(this.setConfig(), { useMongoClient: true }); 24 | this.instance = mongoose.connection; 25 | this.instance.on('error', (e: Error) => { 26 | this.logger.error('MongoDB conenction error:' + e); 27 | }); 28 | this.instance.once('open', () => { 29 | this.logger.log('Successful MongoDB Connection!'); 30 | }); 31 | return this.instance; 32 | } 33 | } 34 | 35 | private setConfig() { 36 | const mongooseConfig: MongooseConfig = new MongooseConfig(); 37 | 38 | return mongooseConfig.configure(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Debug", 8 | "program": "${workspaceRoot}/dist/server.js", 9 | "smartStep": true, 10 | "outFiles": [ 11 | "../dist/**/*.js" 12 | ], 13 | "preLaunchTask": "build", 14 | "protocol": "inspector", 15 | "env": { 16 | "NODE_ENV": "development" 17 | } 18 | }, 19 | { 20 | "name": "TypeScript Debug - File", 21 | "type": "node", 22 | "request": "launch", 23 | "program": "${workspaceRoot}/node_modules/ts-node/dist/_bin.js", 24 | "args": ["${relativeFile}"], 25 | "cwd": "${workspaceRoot}", 26 | "protocol": "inspector" 27 | }, 28 | { 29 | "name": "Debug Jest Tests", 30 | "type": "node", 31 | "request": "launch", 32 | "program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js", 33 | "stopOnEntry": false, 34 | "args": ["--runInBand"], 35 | "cwd": "${workspaceRoot}", 36 | "preLaunchTask": "tsc", 37 | "runtimeExecutable": null, 38 | "runtimeArgs": ["--harmony"], 39 | "env": { 40 | "NODE_ENV": "development" 41 | }, 42 | "console": "internalConsole", 43 | "sourceMaps": true, 44 | "outFiles": [""], 45 | "port": 9070 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /src/app/hero/heros.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpException } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import { Mockgoose } from '@easymetrics/mockgoose'; 4 | import { Mongoose } from 'mongoose'; 5 | 6 | import { SharedModule } from '../../shared/shared.module'; 7 | 8 | import { HerosController } from './heros.controller'; 9 | import { HerosModel, Hero } from './heros.model'; 10 | import { HerosService } from './heros.service'; 11 | 12 | 13 | const mongoose = new Mongoose(); 14 | const mockgoose = new Mockgoose(mongoose); 15 | const db = null; 16 | 17 | beforeAll(async () => { 18 | await mockgoose.prepareStorage(); 19 | this.db = mongoose.connect('mongodb://localhost/test'); 20 | const instance = new HerosModel(this.db); 21 | const heroRepo = instance.herosRepository(); 22 | return this.db; 23 | }, 20000); 24 | 25 | afterAll(async () => { 26 | await mockgoose.helper.reset(); 27 | return this.db.disconnect(); 28 | }); 29 | 30 | 31 | describe('Module: HerosModule', () => { 32 | describe('HerosService', () => { 33 | let herosService: HerosService; 34 | let herosController: HerosController; 35 | 36 | beforeEach(async () => { 37 | const module = await Test.createTestingModule({ 38 | modules: [SharedModule], 39 | controllers: [HerosController], 40 | components: [HerosService, HerosModel], 41 | }).compile(); 42 | 43 | herosService = module.get(HerosService); 44 | herosController = module.get(HerosController); 45 | }); 46 | it('should find Heros0 by ObjectId', async () => { 47 | expect(1).toEqual(1); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /.ssl/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAqR3K6hKDmw5riy3Eq83AZsJYz9gPZyBla7VJI7AkEL8Vd7pF 3 | 1Io7kJw8GVP2iKTZvHrYPZGwnPFo27/F9gvUcqV05QfG7+dM4oBQksOQTCqsYGjJ 4 | vlX1xaZvw49VtIEAqNFzUoCX9C6cky/NQzKkB+YDjjMBtBzJzvKPViCA6Kzo9WQN 5 | x2elfQMM7ql8/+iEUBooepsV4BPCWfv74hHnnx+eZtS1p4fnp0CL4BPS6i2Tf+u+ 6 | HYRnTfAHrf2+LvwpovkmkUQGMLfmW4BRPs4h5u/qrNpAWRhTBsUWqFsS/v93s5jx 7 | z2mkPB5VW2iVlHRY6vTl2het/7fPNXwy6d122QIDAQABAoIBAHzfIcA6UXDBcgk/ 8 | jzOoZgO6RyeZCq6EDj88ibfPTKIM5gYUxZENTLQXmIy/IY994cQ5FMhJdhY8bser 9 | z7lAvOq2Xxzp5CuqJ2wrgNMNkdObwtIhLa3b/PCnV2kXwhnZEyqUFUn657iGXliT 10 | +EVA9DtczI1H/l8GzsB++/WFZInoRyrzz1kylLxFdMoLPC7fLSyB63JhDqiOEGxa 11 | sqDq2kloUhHZjKSSqIa59wcAGU+soUJGbZtl1HXnvAIEcYR7GTxQLivoUFgZAlHi 12 | W8qN2//8Y9R/iSgj+NX6jXtx1kCJeiPhBmPAuBUNX5qBNl/X5oUc+k1geFyr03zv 13 | FzwpCO0CgYEA2nzKE1PiszArIL+zL1uJSmJejec5yzBoTRQ6WToy5fZOq4ZCn+/6 14 | S9fLP3xZdT5JMWaOjOJNiF2DYHFuH4HlXYqgSXzAlNxTKA77pxITOYdt6COIQw7a 15 | Q62HCz4fFsFYKp/N0FHQWHszzf+fLD4FDKn4YRbF0QXyIQfv4H+ib58CgYEAxib9 16 | M1I/1ybEwXenueVMFKZmE6WEgjhMBbRTubZwGc7Zja3odLqkHN/UMi6Q0eueYLgF 17 | HaBotgEy4QdJMX3DvJevsJKoGkw5OT/7Z+jeZvw2mTgKAccg4Q3RPjpNlQ7aKcS0 18 | GdpJ3SQjMGSXN+O0MNRE80udCfPZ9lqbekJ9JocCgYEACwIalFq3o1ub8Kx39Lzh 19 | 7/OA3Bl4Wfp4ZtnMDs7V3axJTm8XUEOhEs0ummZDg5q9yVVnfUWxrls30VYlvESp 20 | L8taRBmbAmUPc4c9uq84dL+UFAwmQ2quKJbHpRNeMaFQNeWTUxmsK6kZdRmaBXqi 21 | en7d2tZw5RUtm+hwd5k2r/UCgYEArD5oCpyMcfF5RtNEMQtovuqGAL34GVnme27s 22 | +JnliAmOguGRFybGUXMfeR+RM2ilG13a1I4Dd3JDT+iNbz/rTZxtVAenqHRpqaI8 23 | X27FJLBIpZdY24LEydzA7l6v4covSe96vp5JZrlq/T3zVNnSYD+kT/iCYYxfAw5v 24 | 6C6zqzECgYArqydZEq5R20riA7WZH9pKZSb9w/SpYsZCHW0YXBIzwqZ+uU7sFBiy 25 | NjuyxQZLGjMwGhP21qXj6JgLw/mE985nmjZMSe2bA+Mdy2M+CrkB8sHpcS5U6cs2 26 | A1XUQcWWclJeDCjACvYdIy6NYlI3y95ombngnNhFh9Gjv9smw4kOWw== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { Transport } from '@nestjs/microservices'; 4 | import * as https from 'https'; 5 | 6 | import { AppComponent } from './app/app.component'; 7 | import { AppModule } from './app/app.module'; 8 | import { Environments } from './shared/environments'; 9 | import { DatabaseExceptionFilter, AuthExceptionFilter } from './shared/exceptions'; 10 | 11 | 12 | const logger = new Logger('HttpsServer'); 13 | const appInstance = new AppComponent(); 14 | const app = appInstance.bootstrap(); 15 | 16 | async function bootstrap() { 17 | const server = await NestFactory.create(AppModule, app); 18 | server.connectMicroservice({ 19 | transport: Transport.REDIS, 20 | url: `redis://${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`, 21 | }); 22 | server.setGlobalPrefix(app.get('prefix')); 23 | server.useGlobalFilters(new AuthExceptionFilter()); 24 | server.useGlobalFilters(new DatabaseExceptionFilter()); 25 | server.init(); 26 | await server.startAllMicroservicesAsync(); 27 | } 28 | 29 | bootstrap(); 30 | 31 | const options = { 32 | key: app.get('key'), 33 | cert: app.get('cert'), 34 | ca: app.get('ca'), 35 | }; 36 | 37 | const httpsInstance = https.createServer(options, app).listen(app.get('port')); 38 | httpsInstance.on('listening', () => { 39 | logger.log(''); 40 | logger.log(''); 41 | logger.log(`Nest Server ready and running on ${app.get('host')}:${app.get('port')}${app.get('prefix')}`); 42 | logger.log(``); 43 | logger.log(`-------------------------------------------------------`); 44 | logger.log(`Environment : ${Environments.getEnv()}`); 45 | logger.log(`Version : ${Environments.getPackageInfo().version}`); 46 | logger.log(``); 47 | logger.log(`-------------------------------------------------------`); 48 | logger.log(``); 49 | }); 50 | -------------------------------------------------------------------------------- /src/app/hero/heros.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Delete, 5 | Get, 6 | HttpStatus, 7 | Param, 8 | Post, 9 | Res 10 | } from '@nestjs/common'; 11 | import { Response } from 'express'; 12 | 13 | import { NotFoundException } from '../../shared/exceptions'; 14 | 15 | import { Hero } from './heros.model'; 16 | import { HerosService } from './heros.service'; 17 | 18 | @Controller('heros') 19 | export class HerosController { 20 | constructor(private herosService: HerosService) {} 21 | 22 | @Get() 23 | public async getAll(@Res() res: Response) { 24 | const heros = await this.herosService.findAll(); 25 | res.status(HttpStatus.OK).json(heros); 26 | } 27 | 28 | @Get(':id') 29 | public async getById(@Res() res: Response, @Param('id') heroId: string) { 30 | const hero = await this.herosService.findById(heroId); 31 | res.status(HttpStatus.OK).json(hero); 32 | } 33 | 34 | @Get('/name/:name') 35 | public async getByName(@Res() res: Response, @Param('name') heroName: string) { 36 | const heros = await this.herosService.findByName(heroName); 37 | res.status(HttpStatus.OK).json(heros); 38 | } 39 | 40 | @Get('/alignment/:alignment') 41 | public async getByAlignment(@Res() res: Response, @Param('alignment') heroAlignment: string) { 42 | const heros = await this.herosService.findByAlignment(heroAlignment); 43 | res.status(HttpStatus.OK).json(heros); 44 | } 45 | 46 | @Post() 47 | public async create(@Res() res: Response, @Body() hero: Hero) { 48 | const newHero = await this.herosService.create(hero); 49 | res.status(HttpStatus.OK).json(newHero); 50 | } 51 | 52 | @Delete(':id') 53 | public async delete(@Res() res: Response, @Param('id') heroId: string) { 54 | const hero = await this.herosService.delete(heroId); 55 | if (hero) { 56 | res.status(HttpStatus.NO_CONTENT).send(); 57 | } else { 58 | throw new NotFoundException(''); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/shared/exceptions/auth-exceptions.filter.ts: -------------------------------------------------------------------------------- 1 | import { Catch, ExceptionFilter, HttpStatus, Logger } from '@nestjs/common'; 2 | import { JsonWebTokenError, NotBeforeError, TokenExpiredError } from 'jsonwebtoken'; 3 | import { UnauthorizedError } from 'express-jwt'; 4 | 5 | interface AuthErrorMessage { 6 | message: string; 7 | status: number; 8 | } 9 | 10 | @Catch(Error) 11 | export class AuthExceptionFilter implements ExceptionFilter { 12 | private logger = new Logger(AuthExceptionFilter.name); 13 | private readonly EXPIRED_ERROR = 'TokenExpiredError'; 14 | private readonly TOKEN_ERROR = 'JsonWebTokenError'; 15 | 16 | public catch(exception, response) { 17 | this.logErrorMessage(exception); 18 | let errorMessage: AuthErrorMessage = { 19 | message: 'Authorization Exception', 20 | status: HttpStatus.UNAUTHORIZED, 21 | }; 22 | 23 | if (exception instanceof JsonWebTokenError || TokenExpiredError || NotBeforeError || UnauthorizedError) { 24 | errorMessage = this.jwtParseError(exception); 25 | } 26 | 27 | response.status(errorMessage.status).json({ message: errorMessage.message }); 28 | } 29 | 30 | private jwtParseError(exception): AuthErrorMessage { 31 | const errorMessage: AuthErrorMessage = { 32 | message: 'Authorization Exception', 33 | status: HttpStatus.UNAUTHORIZED, 34 | }; 35 | switch (exception.name) { 36 | case this.EXPIRED_ERROR: { 37 | errorMessage.message = exception.message; 38 | errorMessage.status = HttpStatus.UNAUTHORIZED; 39 | break; 40 | } 41 | case this.TOKEN_ERROR: { 42 | errorMessage.message = exception.message; 43 | errorMessage.status = HttpStatus.UNAUTHORIZED; 44 | break; 45 | } 46 | } 47 | 48 | return errorMessage; 49 | } 50 | 51 | private logErrorMessage(exception) { 52 | let errorMessage = ''; 53 | Reflect.ownKeys(exception).forEach(k => { 54 | errorMessage += `${k}: ${exception[k]}/n`; 55 | }); 56 | 57 | this.logger.log(errorMessage); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/shared/exceptions/database-exceptions.filter.ts: -------------------------------------------------------------------------------- 1 | import { Catch, ExceptionFilter, HttpStatus, Logger } from '@nestjs/common'; 2 | import { MongoError } from 'mongodb'; 3 | 4 | interface DatabaseErrorMessage { 5 | message: string; 6 | status: number; 7 | } 8 | 9 | @Catch(Error) 10 | export class DatabaseExceptionFilter implements ExceptionFilter { 11 | private logger = new Logger(DatabaseExceptionFilter.name); 12 | private readonly duplicateKey = 11000; 13 | 14 | public catch(exception, response) { 15 | this.logErrorMessage(exception); 16 | let errorMessage: DatabaseErrorMessage = { 17 | message: 'Unknow Exception', 18 | status: HttpStatus.INTERNAL_SERVER_ERROR, 19 | }; 20 | 21 | if (exception instanceof MongoError) { 22 | errorMessage = this.mongodError(exception); 23 | } 24 | 25 | if (exception.name === 'ValidationError') { 26 | errorMessage = this.formatMongooseError(exception); 27 | } 28 | 29 | response.status(errorMessage.status).json({ message: errorMessage.message }); 30 | } 31 | 32 | private formatMongooseError(exception) { 33 | const errorMessage: DatabaseErrorMessage = { 34 | message: 'Validation Error', 35 | status: HttpStatus.BAD_REQUEST, 36 | }; 37 | 38 | if ('errors' in exception) { 39 | errorMessage.message = Object.keys(exception.errors) 40 | .map(key => exception.errors[key].message).join(' '); 41 | } 42 | 43 | return errorMessage; 44 | } 45 | 46 | private mongodError(exception): DatabaseErrorMessage { 47 | const errorMessage: DatabaseErrorMessage = { 48 | message: 'Unknow Exception', 49 | status: HttpStatus.INTERNAL_SERVER_ERROR, 50 | }; 51 | switch (exception.code) { 52 | case this.duplicateKey: 53 | errorMessage.message = exception.message; 54 | errorMessage.status = HttpStatus.CONFLICT; 55 | break; 56 | } 57 | 58 | return errorMessage; 59 | } 60 | 61 | private logErrorMessage(exception) { 62 | let errorMessage = ''; 63 | Reflect.ownKeys(exception).forEach(k => { 64 | errorMessage += `${k}: ${exception[k]}/n`; 65 | }); 66 | 67 | this.logger.log(errorMessage); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/app/hero/heros.model.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@nestjs/common'; 2 | import { Document, Model } from 'mongoose'; 3 | import { DocumentQuery, Schema } from 'mongoose'; 4 | 5 | import { MongooseService } from './../../shared/mongoose/mongoose.service'; 6 | 7 | export interface Hero extends Document { 8 | id?: string; 9 | name: string; 10 | alignment: string; 11 | } 12 | 13 | export interface HeroModel extends Model { 14 | findByName(name: string): DocumentQuery; 15 | findByAlignment(alignment: string): DocumentQuery; 16 | } 17 | 18 | @Component() 19 | export class HerosModel { 20 | private model: HeroModel; 21 | private schema: Schema; 22 | private readonly collection = 'heros'; 23 | 24 | constructor(private mongooseService: MongooseService) { 25 | this.verifySchema(); 26 | this.addStatics(); 27 | this.herosRepository(); 28 | } 29 | 30 | public herosRepository() { 31 | const models = this.mongooseService.connection.modelNames(); 32 | if (models.includes(this.collection)) { 33 | this.model = this.mongooseService.connection.model(this.collection) as HeroModel; 34 | } else { 35 | this.model = this.mongooseService.connection.model(this.collection, this.schema) as HeroModel; 36 | } 37 | return this.model; 38 | } 39 | 40 | private verifySchema() { 41 | this.schema = new Schema({ 42 | name: { type: String, required: 'Name field is requied' }, 43 | alignment: { type: String, index: true }, 44 | }); 45 | } 46 | 47 | /** 48 | * Binds custom static methods to the schema 49 | * 50 | * @private 51 | * @memberof HerosModel 52 | */ 53 | private addStatics() { 54 | this.schema.static('findByName', this.findByName.bind(this)); 55 | this.schema.static('findByAlignment', this.findByAlignment.bind(this)); 56 | } 57 | 58 | /** 59 | * Fake static methods for demo purposes ( must be bound above ) 60 | * 61 | * @private 62 | * @param {string} name 63 | * @returns {DocumentQuery} 64 | * @memberof HerosModel 65 | */ 66 | private findByName(name: string): DocumentQuery { 67 | return this.model.find({ name: name }); 68 | } 69 | 70 | private findByAlignment(alignment: string): DocumentQuery { 71 | return this.model.find({ alignment: alignment }); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/shared/exceptions/http-exceptions.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | UnauthorizedException, 4 | ForbiddenException, 5 | NotFoundException, 6 | NotAllowedException, 7 | ConflictException, 8 | GoneException, 9 | UnsupportedMediaException, 10 | TooManyRequestsException, 11 | } from '../../shared/exceptions'; 12 | 13 | describe('HTTP Exceptions', () => { 14 | test('BAD_REQUEST: Should have the correct properties', () => { 15 | const exception = new BadRequestException('message'); 16 | expect(exception.getStatus()).toBe(400); 17 | expect(exception.getResponse()).toBe('message'); 18 | }); 19 | 20 | test('UNAUTHORIZED: Should have the correct properties', () => { 21 | const exception = new UnauthorizedException('message'); 22 | expect(exception.getStatus()).toBe(401); 23 | expect(exception.getResponse()).toBe('message'); 24 | }); 25 | 26 | test('FORBIDDEN: Should have the correct properties', () => { 27 | const exception = new ForbiddenException('message'); 28 | expect(exception.getStatus()).toBe(403); 29 | expect(exception.getResponse()).toBe('message'); 30 | }); 31 | 32 | test('NOT_FOUND: Should have the correct properties', () => { 33 | const exception = new NotFoundException('message'); 34 | expect(exception.getStatus()).toBe(404); 35 | expect(exception.getResponse()).toBe('message'); 36 | }); 37 | 38 | test('NOT_ALLOWED: Should have the correct properties', () => { 39 | const exception = new NotAllowedException('message'); 40 | expect(exception.getStatus()).toBe(405); 41 | expect(exception.getResponse()).toBe('message'); 42 | }); 43 | 44 | test('CONFLICT: Should have the correct properties', () => { 45 | const exception = new ConflictException('message'); 46 | expect(exception.getStatus()).toBe(409); 47 | expect(exception.getResponse()).toBe('message'); 48 | }); 49 | 50 | test('GONE: Should have the correct properties', () => { 51 | const exception = new GoneException('message'); 52 | expect(exception.getStatus()).toBe(410); 53 | expect(exception.getResponse()).toBe('message'); 54 | }); 55 | 56 | test('UNSUPPORTED_MEDIA_TYPE: Should have the correct properties', () => { 57 | const exception = new UnsupportedMediaException('message'); 58 | expect(exception.getStatus()).toBe(415); 59 | expect(exception.getResponse()).toBe('message'); 60 | }); 61 | 62 | test('UNSUPPORTED_MEDIA_TYPE: Should have the correct properties', () => { 63 | const exception = new TooManyRequestsException('message'); 64 | expect(exception.getStatus()).toBe(429); 65 | expect(exception.getResponse()).toBe('message'); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "arrow-return-shorthand": true, 4 | "callable-types": true, 5 | "class-name": true, 6 | "comment-format": [true, "check-space"], 7 | "curly": true, 8 | "eofline": true, 9 | "forin": true, 10 | "import-blacklist": [true, "rxjs"], 11 | "import-spacing": true, 12 | "indent": [true, "spaces"], 13 | "interface-over-type-literal": true, 14 | "label-position": true, 15 | "max-line-length": [true, 160], 16 | "member-access": false, 17 | "member-ordering": [ 18 | true, 19 | { 20 | "order": [ 21 | "public-static-field", 22 | "public-static-method", 23 | "protected-static-field", 24 | "protected-static-method", 25 | "private-static-field", 26 | "private-static-method", 27 | "public-instance-field", 28 | "protected-instance-field", 29 | "private-instance-field", 30 | "public-constructor", 31 | "protected-constructor", 32 | "private-constructor", 33 | "public-instance-method", 34 | "protected-instance-method", 35 | "private-instance-method" 36 | ] 37 | } 38 | ], 39 | "no-arg": true, 40 | "no-bitwise": false, 41 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 42 | "no-construct": true, 43 | "no-debugger": true, 44 | "no-duplicate-super": true, 45 | "no-empty": false, 46 | "no-empty-interface": true, 47 | "no-eval": true, 48 | "no-inferrable-types": [true, "ignore-params"], 49 | "no-misused-new": true, 50 | "no-non-null-assertion": true, 51 | "no-shadowed-variable": false, 52 | "no-string-literal": false, 53 | "no-string-throw": true, 54 | "no-switch-case-fall-through": true, 55 | "no-trailing-whitespace": true, 56 | "no-unnecessary-initializer": true, 57 | "no-unused-expression": true, 58 | "no-use-before-declare": true, 59 | "no-var-keyword": true, 60 | "object-literal-sort-keys": false, 61 | "one-line": [true, "check-open-brace", "check-catch", "check-else", "check-whitespace"], 62 | "prefer-const": true, 63 | "quotemark": [true, "single"], 64 | "radix": true, 65 | "semicolon": ["always"], 66 | "triple-equals": [true, "allow-null-check"], 67 | "typedef-whitespace": [ 68 | true, 69 | { 70 | "call-signature": "nospace", 71 | "index-signature": "nospace", 72 | "parameter": "nospace", 73 | "property-declaration": "nospace", 74 | "variable-declaration": "nospace" 75 | } 76 | ], 77 | "typeof-compare": true, 78 | "unified-signatures": true, 79 | "variable-name": false, 80 | "whitespace": [true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type"] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/shared/exceptions/http-exceptions.ts: -------------------------------------------------------------------------------- 1 | import { HttpStatus, HttpException } from '@nestjs/common'; 2 | 3 | /** 4 | * 400: Bad Request 5 | * https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400 6 | * 7 | * @export 8 | * @class BadRequestException 9 | * @extends {HttpException} 10 | */ 11 | export class BadRequestException extends HttpException { 12 | constructor(msg: string | object) { 13 | super(msg, HttpStatus.BAD_REQUEST); 14 | } 15 | } 16 | 17 | /** 18 | * 401: Unauthorized 19 | * https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401 20 | * 21 | * @export 22 | * @class UnauthorizedException 23 | * @extends {HttpException} 24 | */ 25 | export class UnauthorizedException extends HttpException { 26 | constructor(msg: string | object) { 27 | super(msg, HttpStatus.UNAUTHORIZED); 28 | } 29 | } 30 | 31 | /** 32 | * 403: Forbidden 33 | * https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403 34 | * 35 | * @export 36 | * @class ForbiddenException 37 | * @extends {HttpException} 38 | */ 39 | export class ForbiddenException extends HttpException { 40 | constructor(msg: string | object) { 41 | super(msg, HttpStatus.FORBIDDEN); 42 | } 43 | } 44 | 45 | /** 46 | * 404: Not Found 47 | * https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404 48 | * 49 | * @export 50 | * @class NotFoundException 51 | * @extends {HttpException} 52 | */ 53 | export class NotFoundException extends HttpException { 54 | constructor(msg: string | object) { 55 | super(msg, HttpStatus.NOT_FOUND); 56 | } 57 | } 58 | 59 | /** 60 | * 405: Method Not Allowed 61 | * https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 62 | * 63 | * @export 64 | * @class NotAllowedException 65 | * @extends {HttpException} 66 | */ 67 | export class NotAllowedException extends HttpException { 68 | constructor(msg: string | object = 'Method Not Allowed') { 69 | super(msg, 405); 70 | } 71 | } 72 | 73 | /** 74 | * 409: Conflict 75 | * https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409 76 | * 77 | * @export 78 | * @class ConflictException 79 | * @extends {HttpException} 80 | */ 81 | export class ConflictException extends HttpException { 82 | constructor(msg: string | object) { 83 | super(msg, HttpStatus.CONFLICT); 84 | } 85 | } 86 | 87 | /** 88 | * 410: Gone 89 | * https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410 90 | * 91 | * @export 92 | * @class GoneException 93 | * @extends {HttpException} 94 | */ 95 | export class GoneException extends HttpException { 96 | constructor(msg: string | object) { 97 | super(msg, HttpStatus.GONE); 98 | } 99 | } 100 | 101 | /** 102 | * 415: Unsupported Media Type 103 | * https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415 104 | * 105 | * @export 106 | * @class UnsupportedMediaException 107 | * @extends {HttpException} 108 | */ 109 | export class UnsupportedMediaException extends HttpException { 110 | constructor(msg: string | object) { 111 | super(msg, HttpStatus.UNSUPPORTED_MEDIA_TYPE); 112 | } 113 | } 114 | 115 | /** 116 | * 429: Too Many Requests 117 | * https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429 118 | * 119 | * @export 120 | * @class TooManyRequestsException 121 | * @extends {HttpException} 122 | */ 123 | export class TooManyRequestsException extends HttpException { 124 | constructor(msg: string | object) { 125 | super(msg, HttpStatus.TOO_MANY_REQUESTS); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-typescript-starter", 3 | "version": "0.0.1", 4 | "description": "NestJS API starter", 5 | "author": "Joshua Wiens ", 6 | "license": "Apache-2.0", 7 | "main": "./src/server.ts", 8 | "scripts": { 9 | "start": "npm run serve:dev", 10 | "build": "npm run lint && npm run clean:dist && npm run transpile", 11 | "build:dist": "npm run clean:dist && npm run transpile", 12 | "clean": "npm run clean:dist", 13 | "clean:all": "npm run clean:coverage && npm run clean:dist", 14 | "clean:coverage": "rimraf './coverage'", 15 | "clean:dist": "rimraf './dist'", 16 | "serve:dev": "nodemon --watch src --watch .env", 17 | "serve:dist": "node dist/server.js", 18 | "test": "cross-env NODE_ENV=test jest --runInBand --forceExit", 19 | "test:verbose": "npm run test -- --verbose", 20 | "test:coverage": "npm run clean:coverage && npm run test -- --coverage", 21 | "report-codeclimate": "codeclimate-test-reporter < coverage/lcov.info", 22 | "lint": "tslint --type-check --project tsconfig.json -c ./tslint.json 'src/**/*.ts' --exclude 'src/**/*.d.ts' --exclude 'src/**/*.spec.ts' --exclude 'src/**/*.spec-data.ts'", 23 | "certs": "bash ./.ssl/generateLocalCerts.sh 'localhost'", 24 | "release": "standard-version", 25 | "security": "nsp check", 26 | "transpile": "tsc" 27 | }, 28 | "dependencies": { 29 | "@nestjs/common": "^4.4.2", 30 | "@nestjs/core": "^4.4.2", 31 | "@nestjs/microservices": "^4.4.1", 32 | "@nestjs/testing": "^4.4.1", 33 | "@nestjs/websockets": "^4.4.1", 34 | "body-parser": "^1.18.1", 35 | "chalk": "2.1.0", 36 | "compression": "^1.7.0", 37 | "class-transformer": "^0.1.7", 38 | "class-validator": "^0.7.2", 39 | "cors": "^2.8.4", 40 | "dotenv": "^4.0.0", 41 | "express-jwt": "^5.3.0", 42 | "express-jwt-authz": "^1.0.0", 43 | "helmet": "^3.8.1", 44 | "jwks-rsa": "^1.2.1", 45 | "mongoose": "^4.13.4", 46 | "redis": "^2.8.0", 47 | "reflect-metadata": "^0.1.10", 48 | "rxjs": "^5.5.2", 49 | "tslib": "^1.7.1" 50 | }, 51 | "devDependencies": { 52 | "@types/body-parser": "^1.16.4", 53 | "@types/chalk": "^0.4.31", 54 | "@types/compression": "0.0.33", 55 | "@types/cors": "^2.8.1", 56 | "@types/dotenv": "^4.0.0", 57 | "@types/express-jwt": "0.0.37", 58 | "@types/helmet": "0.0.36", 59 | "@types/jest": "^21.1.2", 60 | "@types/mongoose": "^4.7.24", 61 | "@types/node": "^8.0.19", 62 | "codeclimate-test-reporter": "^0.5.0", 63 | "cross-env": "^5.0.4", 64 | "jest": "^21.2.1", 65 | "@easymetrics/mockgoose": "^7.3.4", 66 | "mongoose-data-seed": "^1.0.2", 67 | "nodemon": "^1.11.0", 68 | "nsp": "^2.7.0", 69 | "rimraf": "^2.6.1", 70 | "selfsigned": "^1.10.1", 71 | "standard-version": "^4.2.0", 72 | "ts-jest": "^21.1.1", 73 | "ts-node": "^3.3.0", 74 | "tslint": "^5.5.0", 75 | "typescript": "2.5.3" 76 | }, 77 | "jest": { 78 | "globals": { 79 | "ts-jest": { 80 | "tsConfigFile": "tsconfig.spec.json" 81 | } 82 | }, 83 | "transform": { 84 | ".(ts)": "/node_modules/ts-jest/preprocessor.js" 85 | }, 86 | "testRegex": "(/src/.*\\.spec)\\.ts", 87 | "moduleFileExtensions": [ 88 | "ts", 89 | "js", 90 | "json" 91 | ], 92 | "testEnvironment": "node", 93 | "setupTestFrameworkScriptFile": "./setupJest.ts" 94 | }, 95 | "repository": { 96 | "type": "git", 97 | "url": "git+https://github.com/d3viant0ne/nest-typescript-starter.git" 98 | }, 99 | "bugs": { 100 | "url": "https://github.com/d3viant0ne/nest-typescript-starter/issues" 101 | }, 102 | "homepage": "https://github.com/d3viant0ne/nest-typescript-starter#readme", 103 | "keywords": [ 104 | "nestjs", 105 | "nest", 106 | "typescript", 107 | "jest" 108 | ] 109 | } 110 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------