├── .dockerignore ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── Dockerfile ├── README.md ├── docker-compose.yml ├── ecosystem.config.js ├── env └── local.env ├── jest.setup.redis-mock.js ├── nestjs-cli.json ├── nodemon-debug.json ├── nodemon.json ├── package.json ├── prettier.config.js ├── run.sh ├── src ├── main.ts └── modules │ ├── app.module.ts │ ├── echo │ ├── echo.controller.ts │ ├── echo.module.ts │ └── echo.v2.controller.ts │ ├── exchange │ ├── exchange.controller.ts │ ├── exchange.module.ts │ ├── exchange.service.ts │ └── rate.model.ts │ ├── job │ ├── job.controller.ts │ ├── job.model.ts │ └── job.module.ts │ ├── root.controller.ts │ ├── routes.ts │ ├── shared │ ├── functions │ │ └── register-winston.ts │ └── shared.module.ts │ └── uppercase.pipe.ts ├── test └── echo.test.ts ├── tsconfig.build.json ├── tsconfig.json ├── vitest.config.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile* 4 | docker-compose* 5 | .dockerignore 6 | .git 7 | .gitignore 8 | README.md 9 | LICENSE 10 | .vscode 11 | yarn-error.log 12 | yarn.lock 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: [ 8 | '@typescript-eslint/eslint-plugin', 9 | 'radar' 10 | ], 11 | extends: [ 12 | 'plugin:@typescript-eslint/eslint-recommended', 13 | 'plugin:@typescript-eslint/recommended', 14 | 'prettier', 15 | 'plugin:radar/recommended' 16 | ], 17 | root: true, 18 | env: { 19 | node: true, 20 | jest: true, 21 | }, 22 | rules: { 23 | '@typescript-eslint/camelcase': 'off', 24 | '@typescript-eslint/class-name-casing': 'off', 25 | '@typescript-eslint/explicit-function-return-type': 'off', 26 | '@typescript-eslint/interface-name-prefix': 'off', 27 | '@typescript-eslint/no-explicit-any': 'off', 28 | 'no-console': 'error', 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | dist 4 | coverage 5 | documentation 6 | log 7 | yarn-error.log 8 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=14 2 | 3 | FROM keymetrics/pm2:${NODE_VERSION}-alpine 4 | # set api port from env var 5 | ENV API_PORT $API_PORT 6 | 7 | # defaults to local, compose overrides this to either dev, qa or prod 8 | ARG NODE_ENV=local 9 | ENV NODE_ENV $NODE_ENV 10 | 11 | # Create own api folder 12 | RUN mkdir -p /api 13 | WORKDIR /api 14 | 15 | # Install the dependencies 16 | ENV NPM_CONFIG_LOGLEVEL warn 17 | COPY package.json . 18 | RUN yarn 19 | 20 | # Build the API 21 | COPY tsconfig.json . 22 | COPY tsconfig.build.json . 23 | COPY env env/ 24 | COPY src src/ 25 | RUN yarn build 26 | 27 | # Expose the API port 28 | EXPOSE $API_PORT 29 | 30 | # Run the API with pm2 31 | RUN echo ${NODE_ENV} > node_env 32 | COPY ecosystem.config.js . 33 | ENTRYPOINT [ "pm2-runtime", "start", "ecosystem.config.js", "--env", "${NODE_ENV}"] 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | docker build . -t ts-nest 3 | docker run -i -t -p 3000:3000 ts-nest 4 | ``` 5 | 6 | *Or if you can run bash then just,* 7 | 8 | ``` 9 | sh run.sh 10 | ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | api: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | environment: 9 | - API_PORT 10 | ports: 11 | - $API_PORT:$API_PORT 12 | networks: 13 | - docker-ts-nest-net 14 | volumes: 15 | - .:/api 16 | - /api/node_modules 17 | depends_on: 18 | - redis 19 | redis: 20 | environment: 21 | - REDIS_PORT 22 | image: redis:alpine 23 | expose: 24 | - $REDIS_PORT 25 | networks: 26 | - docker-ts-nest-net 27 | 28 | networks: 29 | docker-ts-nest-net: 30 | driver: bridge 31 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps : [{ 3 | name: 'docker-ts-nest', 4 | script: './dist/main.js', 5 | 6 | // Options reference: https://pm2.io/doc/en/runtime/reference/ecosystem-file/ 7 | instances: 1, 8 | autorestart: true, 9 | watch: false, 10 | max_memory_restart: '1G', 11 | env: { 12 | NODE_ENV: 'local' 13 | }, 14 | env_dev: { 15 | NODE_ENV: 'dev' 16 | }, 17 | env_local: { 18 | NODE_ENV: 'local' 19 | }, 20 | env_prod: { 21 | NODE_ENV: 'prod' 22 | }, 23 | env_qa: { 24 | NODE_ENV: 'qa' 25 | } 26 | }] 27 | }; 28 | -------------------------------------------------------------------------------- /env/local.env: -------------------------------------------------------------------------------- 1 | DB_URI=zzz 2 | -------------------------------------------------------------------------------- /jest.setup.redis-mock.js: -------------------------------------------------------------------------------- 1 | jest.mock('redis', () => jest.requireActual('redis-mock')); 2 | -------------------------------------------------------------------------------- /nestjs-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node --inspect-brk -r ts-node/register -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker-ts-nest", 3 | "version": "0.6.0", 4 | "license": "MIT", 5 | "engines": { 6 | "node": "^16.16.0 || >=18.0.0", 7 | "yarn": ">=1.22.19" 8 | }, 9 | "scripts": { 10 | "prebuild": "rimraf dist", 11 | "build": "nest build -b swc", 12 | "dev": "nest start -b swc", 13 | "format": "prettier --cache --write \"src/**/*.ts\"", 14 | "start": "ts-node -r tsconfig-paths/register src/main.ts", 15 | "start:dev": "nodemon", 16 | "start:debug": "nodemon --config nodemon-debug.json", 17 | "prestart:prod": "rimraf dist && yarn build", 18 | "start:prod": "cross-env-shell NODE_ENV=prod pm2 start ecosystem.config.js", 19 | "lint": "eslint '{src,test}/**/*.ts' --fix", 20 | "test": "vitest run", 21 | "test:watch": "vitest run --watch", 22 | "test:cov": "vitest run --coverage", 23 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 24 | "test:e2e": "jest --config ./test/jest-e2e.json", 25 | "check:all": "yarn lint && yarn format && yarn test:cov", 26 | "doc": "npx compodoc --theme material -p ./tsconfig.json", 27 | "doc:serve": "npx compodoc -s", 28 | "commitmsg": "commitlint -e", 29 | "release": "standard-version --no-verify" 30 | }, 31 | "dependencies": { 32 | "@nest-middlewares/cors": "^7.0.1", 33 | "@nest-middlewares/helmet": "^7.0.1", 34 | "@nest-middlewares/response-time": "^7.0.1", 35 | "@nestjs/axios": "~3.0.0", 36 | "@nestjs/bull": "~10.0.0", 37 | "@nestjs/common": "~10.0.2", 38 | "@nestjs/core": "~10.0.2", 39 | "@nestjs/microservices": "~10.0.2", 40 | "@nestjs/platform-express": "~10.0.2", 41 | "@nestjs/swagger": "~7.0.2", 42 | "@nestjs/testing": "~10.0.2", 43 | "@nestjs/websockets": "~10.0.2", 44 | "axios": "1.4.0", 45 | "bull": "^4.10.4", 46 | "dotenv": "^16.3.1", 47 | "express": "^4.18.2", 48 | "helmet": "7.0.0", 49 | "nest-winston": "^1.9.2", 50 | "reflect-metadata": "^0.1.13", 51 | "remeda": "^1.19.0", 52 | "response-time": "^2.3.2", 53 | "rxjs": "~7.8.1", 54 | "supertest": "^6.3.3", 55 | "swagger-ui-express": "^4.6.3", 56 | "winston": "^3.9.0", 57 | "winston-daily-rotate-file": "^4.7.1", 58 | "zod": "^3.21.4" 59 | }, 60 | "devDependencies": { 61 | "@commitlint/cli": "^17.6.5", 62 | "@commitlint/config-angular": "^17.6.5", 63 | "@compodoc/compodoc": "^1.1.21", 64 | "@nestjs/cli": "~10.0.3", 65 | "@swc/cli": "^0.1.62", 66 | "@swc/core": "^1.3.66", 67 | "@types/bull": "^4.10.0", 68 | "@types/node": "^18.16.18", 69 | "@typescript-eslint/eslint-plugin": "~5.60.0", 70 | "@typescript-eslint/parser": "~5.60.0", 71 | "@vitest/coverage-v8": "^0.32.2", 72 | "c8": "^8.0.0", 73 | "cross-env": "^7.0.3", 74 | "eslint": "~8.43.0", 75 | "eslint-config-prettier": "^8.8.0", 76 | "eslint-plugin-import": "^2.27.5", 77 | "eslint-plugin-radar": "^0.2.1", 78 | "husky": "^8.0.3", 79 | "import-sort-style-module": "^6.0.0", 80 | "lint-staged": "^13.2.2", 81 | "nodemon": "^2.0.22", 82 | "prettier": "~2.8.8", 83 | "prettier-plugin-import-sort": "^0.0.7", 84 | "redis-mock": "^0.56.3", 85 | "rimraf": "^5.0.1", 86 | "standard-version": "^9.5.0", 87 | "ts-loader": "^9.4.3", 88 | "ts-node": "^10.9.1", 89 | "tsconfig-paths": "^4.2.0", 90 | "typescript": "~5.1.3", 91 | "vite-plugin-checker": "^0.6.1", 92 | "vite-tsconfig-paths": "^4.2.0", 93 | "vitest": "^0.32.2" 94 | }, 95 | "importSort": { 96 | ".ts": { 97 | "style": "module", 98 | "parser": "typescript" 99 | } 100 | }, 101 | "jest": { 102 | "moduleFileExtensions": [ 103 | "js", 104 | "json", 105 | "ts" 106 | ], 107 | "rootDir": "src", 108 | "testRegex": ".spec.ts$", 109 | "transform": { 110 | "^.+\\.(t|j)s$": "ts-jest" 111 | }, 112 | "coverageDirectory": "../coverage", 113 | "setupFilesAfterEnv": "./jest.setup.redis-mock.js", 114 | "testEnvironment": "node" 115 | }, 116 | "husky": { 117 | "hooks": { 118 | "pre-commit": "lint-staged" 119 | } 120 | }, 121 | "lint-staged": { 122 | "*.ts": [ 123 | "prettier --write", 124 | "git add" 125 | ] 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | tabWidth: 2, 4 | useTabs: true, 5 | semi: true, 6 | singleQuote: true, 7 | trailingComma: 'none', // other options `es5` or `all` 8 | bracketSpacing: true, 9 | arrowParens: 'avoid', // other option 'always' 10 | parser: 'typescript' 11 | }; 12 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$1" = "rebuild" ]; then 4 | docker build --tag=docker-ts-nest -f ./Dockerfile . 5 | fi 6 | 7 | if [ "$1" = "api" ]; then 8 | docker build --tag=docker-ts-nest -f ./Dockerfile . 9 | fi 10 | 11 | API_PORT=3000 REDIS_PORT=6379 docker-compose up -d api 12 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { VersioningType } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { ExpressAdapter } from '@nestjs/platform-express'; 4 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 5 | 6 | import { ApplicationModule } from './modules/app.module'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(ApplicationModule, new ExpressAdapter()); 10 | 11 | app.enableVersioning({ 12 | type: VersioningType.HEADER, 13 | header: 'version' 14 | }); 15 | 16 | const options = new DocumentBuilder() 17 | .setTitle('Hello example') 18 | .setDescription('The hello API description') 19 | .setVersion('1.0') 20 | .addTag('hello') 21 | .addBearerAuth() 22 | .build(); 23 | 24 | const document = SwaggerModule.createDocument(app, options); 25 | SwaggerModule.setup('/swagger', app, document); 26 | 27 | const API_PORT = +process.env.API_PORT || 3000; 28 | await app.listen(API_PORT); 29 | } 30 | 31 | bootstrap(); 32 | -------------------------------------------------------------------------------- /src/modules/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RouterModule } from '@nestjs/core'; 3 | 4 | import { EchoModule } from './echo/echo.module'; 5 | import { JobModule } from './job/job.module'; 6 | import { RootController } from './root.controller'; 7 | import { routes } from './routes'; 8 | import { registerWinston } from './shared/functions/register-winston'; 9 | 10 | @Module({ 11 | controllers: [RootController], 12 | // * retire ExchangeModule because API KEY is now in need. 13 | imports: [RouterModule.register(routes), EchoModule, JobModule, registerWinston()] 14 | }) 15 | export class ApplicationModule {} 16 | -------------------------------------------------------------------------------- /src/modules/echo/echo.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Inject, Param } from '@nestjs/common'; 2 | import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; 3 | import { Logger } from 'winston'; 4 | 5 | @ApiTags('hello') 6 | @Controller() 7 | export class EchoController { 8 | constructor(@Inject('winston') private readonly logger: Logger) {} 9 | 10 | @ApiOperation({ summary: 'Echo input on request to response' }) 11 | @ApiResponse({ status: 200, description: 'Successful response' }) 12 | @Get('/:input') 13 | public echo(@Param('input') input: string): Message { 14 | const message = { echo: input }; 15 | this.logger.info(message); 16 | 17 | return message; 18 | } 19 | } 20 | 21 | interface Message { 22 | echo?: string; 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/echo/echo.module.ts: -------------------------------------------------------------------------------- 1 | import { CorsMiddleware } from '@nest-middlewares/cors'; 2 | import { HelmetMiddleware } from '@nest-middlewares/helmet'; 3 | import { ResponseTimeMiddleware } from '@nest-middlewares/response-time'; 4 | import { MiddlewareConsumer, Module } from '@nestjs/common'; 5 | 6 | import { SharedModule } from '../shared/shared.module'; 7 | import { EchoController } from './echo.controller'; 8 | import { EchoV2Controller } from './echo.v2.controller'; 9 | 10 | @Module({ 11 | // * place v2 first 12 | controllers: [EchoV2Controller, EchoController], 13 | imports: [SharedModule] 14 | }) 15 | export class EchoModule { 16 | public configure(consumer: MiddlewareConsumer): void { 17 | consumer.apply(CorsMiddleware, HelmetMiddleware, ResponseTimeMiddleware).forRoutes(EchoController); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/echo/echo.v2.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Inject, Param, Version } from '@nestjs/common'; 2 | import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; 3 | import { Logger } from 'winston'; 4 | 5 | @ApiTags('hello') 6 | @Controller() 7 | export class EchoV2Controller { 8 | constructor(@Inject('winston') private readonly logger: Logger) {} 9 | 10 | @ApiOperation({ summary: 'Echo input on request to response - v2' }) 11 | @ApiResponse({ status: 200, description: 'Successful response' }) 12 | @Version('2') 13 | @Get('/:input') 14 | public echoV2(@Param('input') input: string): Message { 15 | const message = { echo: input, version: 'v2' }; 16 | this.logger.info(message); 17 | 18 | return message; 19 | } 20 | } 21 | 22 | interface Message { 23 | echo?: string; 24 | version?: string; 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/exchange/exchange.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, Res } from '@nestjs/common'; 2 | import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; 3 | import { AxiosResponse } from 'axios'; 4 | 5 | import { UpperCasePipe } from '../uppercase.pipe'; 6 | import { ExchangeService } from './exchange.service'; 7 | import { Rate } from './rate.model'; 8 | 9 | @ApiTags('hello') 10 | @Controller() 11 | export class ExchangeController { 12 | constructor(private exchangeService: ExchangeService) {} 13 | 14 | @ApiOperation({ summary: 'Return Exchange Rate per request' }) 15 | @ApiResponse({ status: 200, description: 'Successful response' }) 16 | @ApiParam({ name: 'from', required: true, type: String }) 17 | @ApiParam({ name: 'to', required: true, type: String }) 18 | @Get('/:from/:to') 19 | public rate( 20 | @Param('from', new UpperCasePipe()) from: string, 21 | @Param('to', new UpperCasePipe()) to: string, 22 | /* eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types */ 23 | @Res() response 24 | ): void { 25 | const rate: Rate = { 26 | from, 27 | to 28 | }; 29 | 30 | this.exchangeService.getRate(from).subscribe((res: AxiosResponse) => { 31 | rate.rate = res.data.rates[to] as number; 32 | 33 | response.send(JSON.stringify(rate)); 34 | }); 35 | } 36 | } 37 | 38 | interface Fixer { 39 | rates: number[]; 40 | } 41 | -------------------------------------------------------------------------------- /src/modules/exchange/exchange.module.ts: -------------------------------------------------------------------------------- 1 | import { CorsMiddleware } from '@nest-middlewares/cors'; 2 | import { HelmetMiddleware } from '@nest-middlewares/helmet'; 3 | import { ResponseTimeMiddleware } from '@nest-middlewares/response-time'; 4 | import { HttpModule } from '@nestjs/axios'; 5 | import { MiddlewareConsumer, Module } from '@nestjs/common'; 6 | 7 | import { SharedModule } from '../shared/shared.module'; 8 | import { ExchangeController } from './exchange.controller'; 9 | import { ExchangeService } from './exchange.service'; 10 | 11 | @Module({ 12 | controllers: [ExchangeController], 13 | imports: [HttpModule, SharedModule], 14 | providers: [ExchangeService] 15 | }) 16 | export class ExchangeModule { 17 | public configure(consumer: MiddlewareConsumer): void { 18 | consumer.apply(CorsMiddleware, HelmetMiddleware, ResponseTimeMiddleware).forRoutes('rate'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/exchange/exchange.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from '@nestjs/axios'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { AxiosResponse } from 'axios'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Injectable() 7 | export class ExchangeService { 8 | constructor(private readonly httpService: HttpService) {} 9 | 10 | getRate(from: string): Observable> { 11 | return this.httpService.get(`https://api.frankfurter.app/latest?base=${from}`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/exchange/rate.model.ts: -------------------------------------------------------------------------------- 1 | export interface Rate { 2 | from?: string; 3 | to?: string; 4 | rate?: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/job/job.controller.ts: -------------------------------------------------------------------------------- 1 | import { InjectQueue } from '@nestjs/bull'; 2 | import { Body, Controller, Get, Param, Post } from '@nestjs/common'; 3 | import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; 4 | import { Job, Queue } from 'bull'; 5 | 6 | import { JobId } from './job.model'; 7 | 8 | @ApiTags('hello') 9 | @Controller() 10 | export class JobController { 11 | constructor(@InjectQueue('store') readonly queue: Queue) {} 12 | 13 | @ApiOperation({ summary: 'Add job to queue' }) 14 | @ApiResponse({ status: 200, description: 'Successful response' }) 15 | @Post('/add') 16 | /* eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types */ 17 | public async addJob(@Body() value: T): Promise { 18 | const job: Job = await this.queue.add(value); 19 | return { id: job.id }; 20 | } 21 | 22 | @ApiOperation({ summary: 'Return job per id requested' }) 23 | @ApiResponse({ status: 200, description: 'Successful response' }) 24 | @Get('/get/:id') 25 | /* eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types */ 26 | public async getJob(@Param('id') id: string): Promise> { 27 | return this.queue.getJob(id); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/job/job.model.ts: -------------------------------------------------------------------------------- 1 | export interface JobId { 2 | id: number | string; 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/job/job.module.ts: -------------------------------------------------------------------------------- 1 | import { CorsMiddleware } from '@nest-middlewares/cors'; 2 | import { HelmetMiddleware } from '@nest-middlewares/helmet'; 3 | import { ResponseTimeMiddleware } from '@nest-middlewares/response-time'; 4 | import { BullModule, BullModuleOptions } from '@nestjs/bull'; 5 | import { MiddlewareConsumer, Module } from '@nestjs/common'; 6 | import { DoneCallback, Job } from 'bull'; 7 | 8 | import { SharedModule } from '../shared/shared.module'; 9 | import { JobController } from './job.controller'; 10 | 11 | const REDIS_PORT = +process.env.REDIS_PORT || 6379; 12 | const BullQueueModule = BullModule.registerQueueAsync({ 13 | name: 'store', 14 | inject: [], 15 | useFactory: async (): Promise => ({ 16 | redis: { 17 | host: 'redis', 18 | port: REDIS_PORT 19 | }, 20 | processors: [ 21 | (job: Job, done: DoneCallback) => { 22 | done(undefined, job.data); 23 | } 24 | ] 25 | }) 26 | }); 27 | 28 | @Module({ 29 | controllers: [JobController], 30 | imports: [BullQueueModule, SharedModule] 31 | }) 32 | export class JobModule { 33 | public configure(consumer: MiddlewareConsumer): void { 34 | consumer.apply(CorsMiddleware, HelmetMiddleware, ResponseTimeMiddleware).forRoutes('job'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/root.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, HttpStatus, Response } from '@nestjs/common'; 2 | import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; 3 | 4 | @ApiTags('hello') 5 | @Controller() 6 | export class RootController { 7 | @ApiOperation({ summary: 'Respond Hello World' }) 8 | @ApiResponse({ status: 200, description: 'Successfully response' }) 9 | @Get('/') 10 | /* eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types */ 11 | public greet(@Response() res): void { 12 | res.status(HttpStatus.OK).send('Hello, World'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@nestjs/core'; 2 | 3 | import { EchoModule } from './echo/echo.module'; 4 | import { ExchangeModule } from './exchange/exchange.module'; 5 | import { JobModule } from './job/job.module'; 6 | 7 | export const routes: Routes = [ 8 | { 9 | module: EchoModule, 10 | path: '/echo' 11 | }, 12 | { 13 | module: ExchangeModule, 14 | path: '/rate' 15 | }, 16 | { 17 | module: JobModule, 18 | path: '/job' 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /src/modules/shared/functions/register-winston.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule } from '@nestjs/common'; 2 | import { WinstonModule } from 'nest-winston'; 3 | 4 | /* eslint-disable @typescript-eslint/no-var-requires */ 5 | const winston = require('winston'); 6 | winston.transports.DailyRotateFile = require('winston-daily-rotate-file'); 7 | const fs = require('fs'); 8 | const logDir = 'log'; 9 | /* eslint-enable @typescript-eslint/no-var-requires */ 10 | 11 | if (!fs.existsSync(logDir)) { 12 | fs.mkdirSync(logDir); 13 | } 14 | 15 | const dailyRotateFileTransport = new winston.transports.DailyRotateFile({ 16 | datePattern: 'YYYY-MM-DD', 17 | filename: `${logDir}/docker-ts-nest-%DATE%.log`, 18 | maxFiles: '7d' 19 | }); 20 | 21 | export function registerWinston(): DynamicModule { 22 | return WinstonModule.forRootAsync({ 23 | inject: [], 24 | useFactory: () => ({ 25 | format: winston.format.combine( 26 | winston.format.timestamp({ 27 | format: 'MM/DD/YYYY HH:mm:ss.SSS' 28 | }), 29 | winston.format.json(), 30 | winston.format.splat() 31 | ), 32 | level: 'info', 33 | transports: [new winston.transports.Console({ level: 'debug' }), dailyRotateFileTransport], 34 | exitOnError: false 35 | }) 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/modules/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { HelmetMiddleware } from '@nest-middlewares/helmet'; 2 | import { ResponseTimeMiddleware } from '@nest-middlewares/response-time'; 3 | import { Module } from '@nestjs/common'; 4 | import { ResponseTimeOptions } from 'response-time'; 5 | 6 | @Module({ 7 | /* eslint-disable-next-line @typescript-eslint/no-use-before-define */ 8 | exports: [SharedModule] 9 | }) 10 | export class SharedModule { 11 | public configure(): void { 12 | HelmetMiddleware.configure(this.getHelmetConfiguration()); 13 | ResponseTimeMiddleware.configure(this.getResponseTimeOptions()); 14 | } 15 | 16 | /* eslint-disable radar/no-identical-functions */ 17 | private getHelmetConfiguration() { 18 | return { 19 | // default helmet configuration 20 | }; 21 | } 22 | 23 | private getResponseTimeOptions(): ResponseTimeOptions { 24 | return { 25 | // default response-time options 26 | }; 27 | } 28 | /* eslint-enable radar/no-identical-functions */ 29 | } 30 | -------------------------------------------------------------------------------- /src/modules/uppercase.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class UpperCasePipe implements PipeTransform { 5 | /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ 6 | public async transform(value: string, metadata: ArgumentMetadata): Promise { 7 | if (!value) { 8 | return value; 9 | } 10 | if (typeof value !== 'string') { 11 | throw new BadRequestException('UpperCase transform failed'); 12 | } 13 | return value.toUpperCase(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/echo.test.ts: -------------------------------------------------------------------------------- 1 | import { RouterModule } from '@nestjs/core'; 2 | import { Test } from '@nestjs/testing'; 3 | import request, { SuperTest, Test as AppTest } from 'supertest'; 4 | import { afterAll, beforeAll, describe, expect, it } from 'vitest'; 5 | 6 | import { EchoModule } from '../src/modules/echo/echo.module'; 7 | import { registerWinston } from '../src/modules/shared/functions/register-winston'; 8 | import { routes } from '../src/modules/routes'; 9 | 10 | 11 | 12 | describe('Echo Module', () => { 13 | let app; 14 | let server: SuperTest; 15 | 16 | beforeAll(async () => { 17 | const module = await Test.createTestingModule({ 18 | imports: [ RouterModule.register(routes), EchoModule, registerWinston()], 19 | }) 20 | .compile(); 21 | 22 | app = module.createNestApplication(); 23 | await app.init(); 24 | 25 | server = request(app.getHttpServer()) 26 | }); 27 | 28 | afterAll(async () => { 29 | await app.close(); 30 | }); 31 | 32 | it('should return 200 OK', () => { 33 | return server.get('/echo/hello').expect(200); 34 | }); 35 | 36 | it('should return the response expected', () => { 37 | const expected = { 38 | echo: 'hello' 39 | }; 40 | 41 | return server.get('/echo/hello').expect(200),expect(expected); 42 | }); 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import check from 'vite-plugin-checker'; 3 | import tsconfigPaths from 'vite-tsconfig-paths'; 4 | 5 | export default defineConfig({ 6 | test: { 7 | environment: 'node', 8 | silent: true, 9 | coverage: { 10 | reporter: ['text', 'text-summary'] 11 | }, 12 | reporters: ['verbose'] 13 | }, 14 | plugins: [check({ typescript: true }), tsconfigPaths()] 15 | }); 16 | --------------------------------------------------------------------------------