├── .editorconfig ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── .prettierrc ├── .swcrc ├── README.md ├── Taskfile ├── jest.config.js ├── jest.setup.ts ├── nest-cli.json ├── package.json ├── src ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── create-user.dto.ts ├── main.ts └── str.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.config.js ├── tsconfig.build.json ├── tsconfig.json ├── vite.config.ts └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.{json,yml}] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | browser: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 11 | 'plugin:unicorn/recommended', 12 | 'plugin:prettier/recommended', 13 | 'prettier', 14 | ], 15 | parser: '@typescript-eslint/parser', 16 | parserOptions: { 17 | ecmaVersion: 2020, 18 | project: 'tsconfig.json', 19 | sourceType: 'module', 20 | ecmaFeatures: { 21 | jsx: false, 22 | }, 23 | warnOnUnsupportedTypeScriptVersion: false, 24 | }, 25 | plugins: ['unicorn', '@typescript-eslint', 'prettier', 'only-warn'], 26 | ignorePatterns: ['@generated/**', '*.config.js', '.*rc.js'], 27 | rules: { 28 | // core 29 | 'consistent-return': [1, { treatUndefinedAsUnspecified: true }], 30 | quotes: [1, 'single', { allowTemplateLiterals: true, avoidEscape: true }], 31 | semi: [1, 'always'], 32 | 'max-lines': [1, { max: 300 }], 33 | 'max-params': [1, { max: 5 }], 34 | 'no-unneeded-ternary': [1], 35 | // unicorn 36 | 'unicorn/prefer-spread': 0, 37 | 'unicorn/catch-error-name': 0, 38 | 'unicorn/prefer-node-protocol': 0, 39 | 'unicorn/prevent-abbreviations': [ 40 | 1, 41 | { 42 | replacements: { 43 | args: false, 44 | err: false, 45 | prod: false, 46 | ref: false, 47 | params: false, 48 | }, 49 | checkFilenames: false 50 | }, 51 | ], 52 | // typescript-eslint 53 | '@typescript-eslint/no-floating-promises': 1, 54 | '@typescript-eslint/no-unnecessary-condition': 1, 55 | }, 56 | overrides: [ 57 | { 58 | files: ['*.spec.ts', '**/testing/**/*.ts'], 59 | rules: { 60 | 'consistent-return': 0, 61 | 'max-lines': 0, 62 | '@typescript-eslint/no-explicit-any': 0, 63 | '@typescript-eslint/no-floating-promises': 0, 64 | '@typescript-eslint/no-non-null-assertion': 0, 65 | '@typescript-eslint/camelcase': 0, 66 | 'import/max-dependencies': 0, 67 | }, 68 | }, 69 | ], 70 | }; 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/5d896f6791c4257b74696714c66b2530b8d95a51/Node.gitignore 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | # nyc test coverage 18 | .nyc_output 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | # Bower dependency directory (https://bower.io/) 22 | bower_components 23 | # node-waf configuration 24 | .lock-wscript 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | # Dependency directories 28 | node_modules/ 29 | jspm_packages/ 30 | # Typescript v1 declaration files 31 | typings/ 32 | # Optional npm cache directory 33 | .npm 34 | # Optional eslint cache 35 | .eslintcache 36 | # Optional REPL history 37 | .node_repl_history 38 | # Output of 'npm pack' 39 | *.tgz 40 | # Yarn Integrity file 41 | .yarn-integrity 42 | # dotenv environment variables file 43 | .env 44 | # Custom 45 | dist/ 46 | ~* 47 | .idea 48 | .awcache 49 | .vscode 50 | .rts2_cache_* 51 | .stryker-tmp 52 | reports 53 | .swc 54 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | loglevel = error 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | printWidth: 88, 3 | trailingComma: 'all', 4 | tabWidth: 4, 5 | semi: true, 6 | singleQuote: true, 7 | arrowParens: 'avoid', 8 | overrides: [ 9 | { 10 | files: '*.{json,yml}', 11 | options: { 12 | tabWidth: 2, 13 | }, 14 | }, 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "module": { 4 | "type": "es6", 5 | "strict": true, 6 | "strictMode": true, 7 | "noInterop": false 8 | }, 9 | "jsc": { 10 | "externalHelpers": true, 11 | "target": "es2019", 12 | "parser": { 13 | "syntax": "typescript", 14 | "tsx": true, 15 | "decorators": true, 16 | "dynamicImport": true 17 | }, 18 | "transform": { 19 | "legacyDecorator": true, 20 | "decoratorMetadata": true, 21 | "react": { 22 | "throwIfNamespace": false, 23 | "development": false, 24 | "useBuiltins": false, 25 | "pragma": "React.createElement", 26 | "pragmaFrag": "React.Fragment", 27 | "importSource": "react" 28 | } 29 | }, 30 | "keepClassNames": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nest-vite-node-starter 2 | 3 | Nest.JS via vite example 4 | 5 | ## Installation 6 | 7 | ```bash 8 | $ npm install 9 | ``` 10 | 11 | ## Running the app 12 | 13 | ```bash 14 | # development 15 | $ npm run dev 16 | ``` 17 | 18 | ## Test 19 | 20 | ```bash 21 | # unit tests 22 | $ npm run test 23 | 24 | # test coverage 25 | $ npm run test:cov 26 | 27 | # e2e tests 28 | $ npm run test:e2e 29 | ``` 30 | 31 | ``` 32 | POST http://localhost:3000/user 33 | Content-type: application/json 34 | 35 | {"email":"user@mail.com","password":"123456"} 36 | ``` 37 | 38 | ## Known Issues 39 | 40 | - https://github.com/swc-project/swc/issues/2117 (fixed) 41 | - under the hood (swc is used) 42 | - https://github.com/vitest-dev/vitest/issues/708 (will not be fixed) (issue when vite-plugin-node is not used) 43 | - Build files with esbuild as a bundler (which might require a lot of custom options), and run tests against it 44 | - Use swc plugin https://github.com/egoist/unplugin-swc 45 | 46 | ## Resources 47 | 48 | - https://github.com/axe-me/vite-plugin-node 49 | - https://github.com/vitejs/awesome-vite 50 | -------------------------------------------------------------------------------- /Taskfile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PATH="$PWD/node_modules/.bin":$PATH 3 | set -e 4 | 5 | build() { 6 | set -x 7 | rm -rf dist 8 | nest build 9 | set +x 10 | } 11 | 12 | build_wp() { 13 | set -x 14 | rm -rf dist 15 | nest build --webpack 16 | set +x 17 | } 18 | 19 | build_swc() { 20 | set -x 21 | rm -rf dist 22 | export NODE_ENV=production 23 | swc src --source-maps=true -d dist 24 | set +x 25 | } 26 | 27 | build_vite() { 28 | set -x 29 | export NODE_ENV=production 30 | vite build --sourcemap 31 | set +x 32 | } 33 | 34 | "$@" 35 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const getJestMappersFromTSConfig = require('tsconfig-paths-jest-mapper'); 2 | const loadJsonFile = require('load-json-file'); 3 | const { convert } = require('tsconfig-to-swcconfig'); 4 | 5 | const swcrc = loadJsonFile('./.swcrc'); 6 | const swcFromTsConfig = convert(undefined, undefined, { 7 | sourceMaps: false, 8 | }); 9 | 10 | const swcConfig = swcFromTsConfig; 11 | 12 | module.exports = { 13 | testEnvironment: 'node', 14 | setupFiles: ['/jest.setup.ts'], 15 | transform: { 16 | '^.+\\.(t|j)sx?$': ['@swc/jest', swcConfig], 17 | }, 18 | collectCoverage: false, 19 | coverageDirectory: 'coverage', 20 | coverageReporters: [ 21 | // "lcov", 22 | 'text', 23 | ], 24 | collectCoverageFrom: ['src/**/*.ts', '!src/**/*.spec.ts'], 25 | testMatch: ['/src/**/*.spec.ts'], 26 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], 27 | modulePathIgnorePatterns: ['/dist'], 28 | moduleNameMapper: { 29 | ...getJestMappersFromTSConfig(), 30 | '\\.(css|less|sass|scss)$': 'identity-obj-proxy', 31 | '\\.(gif|ttf|eot|svg|png|jpg|jpeg)$': '/test/__mocks__/fileMock.js', 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-vite-node-starter", 3 | "version": "0.0.0-dev", 4 | "description": "", 5 | "private": true, 6 | "scripts": { 7 | "dev": "node node_modules/vite/bin/vite.js", 8 | "start": "nest start", 9 | "start:dev": "nest start --watch", 10 | "start:debug": "nest start --debug --watch", 11 | "start:prod": "node dist/main", 12 | "test": "npm run eslint && npm run tscheck && npm run test:cov && npm run test:e2e", 13 | "test:r": "jest --runInBand", 14 | "test:cov": "npm run test:r -- --coverage", 15 | "test:w": "npm run test:r -- --watch", 16 | "eslint": "node node_modules/eslint/bin/eslint \"src/**/*.{ts,tsx}\"", 17 | "eslint:w": "watchexec -w src \"npm run eslint\"", 18 | "eslint:fix": "npm run eslint -- --fix", 19 | "tscheck": "tsc --noEmit", 20 | "tscheck:w": "npm run tscheck -- --watch", 21 | "commit": "cz", 22 | "build": "sh Taskfile build_wp", 23 | "build:vite": "sh Taskfile build_vite", 24 | "test:e2e": "jest --runInBand --config ./test/jest-e2e.config.js", 25 | "test:d": "ndb -r @swc/register node_modules/jest/bin/jest.js --runInBand --watch" 26 | }, 27 | "dependencies": { 28 | "@nestjs/common": "^8.4.4", 29 | "@nestjs/core": "^8.4.4" 30 | }, 31 | "devDependencies": { 32 | "@nestjs/cli": "^8.2.5", 33 | "@nestjs/platform-express": "^8.4.4", 34 | "@nestjs/schematics": "^8.0.10", 35 | "@nestjs/testing": "^8.4.4", 36 | "@swc/cli": "^0.1.57", 37 | "@swc/core": "^1.2.165", 38 | "@swc/helpers": "^0.3.8", 39 | "@swc/jest": "^0.2.20", 40 | "@swc/register": "^0.1.10", 41 | "@types/express": "^4.17.13", 42 | "@types/jest": "^27.4.1", 43 | "@types/node": "^17.0.24", 44 | "@types/supertest": "^2.0.12", 45 | "@typescript-eslint/eslint-plugin": "^5.19.0", 46 | "@typescript-eslint/parser": "^5.19.0", 47 | "class-transformer": "^0.5.1", 48 | "class-validator": "^0.13.2", 49 | "env-bool": "^2.0.1", 50 | "eslint": "^8.13.0", 51 | "eslint-config-prettier": "^8.5.0", 52 | "eslint-plugin-only-warn": "^1.0.3", 53 | "eslint-plugin-prettier": "^4.0.0", 54 | "eslint-plugin-unicorn": "^42.0.0", 55 | "expect": "^27.5.1", 56 | "jest": "^27.5.1", 57 | "load-json-file": "6", 58 | "prettier": "^2.6.2", 59 | "reflect-metadata": "^0.1.13", 60 | "rxjs": "^7.5.5", 61 | "supertest": "^6.2.2", 62 | "ts-loader": "^9.2.8", 63 | "ts-node": "^10.7.0", 64 | "tsconfig-paths": "^3.14.1", 65 | "tsconfig-paths-jest-mapper": "^1.4.0", 66 | "tsconfig-to-swcconfig": "^1.5.0", 67 | "tslib": "^2.3.1", 68 | "typescript": "^4.6.3", 69 | "vite": "^2.9.5", 70 | "vite-plugin-node": "^0.0.19", 71 | "vite-tsconfig-paths": "^3.4.1", 72 | "watchexec-bin": "^1.0.0", 73 | "webpack": "^5.72.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | 6 | describe('AppController', () => { 7 | let appController: AppController; 8 | 9 | beforeEach(async () => { 10 | const app: TestingModule = await Test.createTestingModule({ 11 | controllers: [AppController], 12 | providers: [AppService], 13 | }).compile(); 14 | 15 | appController = app.get(AppController); 16 | }); 17 | 18 | describe('root', () => { 19 | it('should return "Hello World!"', () => { 20 | expect(appController.getHello()).toBe('Hello World!'); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Get, Post } from '@nestjs/common'; 2 | 3 | import { AppService } from './app.service'; 4 | import { CreateUserDto } from './create-user.dto'; 5 | 6 | @Controller() 7 | export class AppController { 8 | constructor(private readonly appService: AppService) {} 9 | 10 | @Get() 11 | getHello(): string { 12 | return this.appService.getHello(); 13 | } 14 | 15 | @Get('/random') 16 | getRandom(): string { 17 | return Math.random().toString(); 18 | } 19 | 20 | @Post('/user') 21 | echo(@Body() createUserDto: CreateUserDto) { 22 | return createUserDto; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | 6 | @Module({ 7 | imports: [], 8 | controllers: [AppController], 9 | providers: [AppService], 10 | }) 11 | export class AppModule {} 12 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { str as string_ } from './str'; 4 | 5 | @Injectable() 6 | export class AppService { 7 | getHello(): string { 8 | return `Hello ${string_}!`; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString, Length } from 'class-validator'; 2 | 3 | export class CreateUserDto { 4 | @IsEmail() 5 | readonly email!: string; 6 | 7 | @IsString() 8 | @Length(6, 32) 9 | readonly password!: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INestApplication, 3 | NestApplicationOptions, 4 | ValidationPipe, 5 | } from '@nestjs/common'; 6 | import { NestFactory } from '@nestjs/core'; 7 | 8 | import { AppModule } from './app.module'; 9 | 10 | export async function createApp( 11 | options?: NestApplicationOptions, 12 | ): Promise { 13 | const app = await NestFactory.create(AppModule, options); 14 | app.enableCors(); 15 | app.useGlobalPipes( 16 | new ValidationPipe({ 17 | transform: true, 18 | validationError: { 19 | target: false, 20 | }, 21 | }), 22 | ); 23 | // useContainer(app.select(AppModule), { fallbackOnErrors: true }); 24 | return app; 25 | } 26 | 27 | async function main() { 28 | const app = await createApp(); 29 | await app.listen(3000); 30 | } 31 | 32 | export let viteNodeApp; 33 | 34 | if (process.env.NODE_ENV === 'production') { 35 | void main(); 36 | } else { 37 | viteNodeApp = createApp(); 38 | } 39 | -------------------------------------------------------------------------------- /src/str.ts: -------------------------------------------------------------------------------- 1 | export const str = `World`; 2 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import request from 'supertest'; 3 | import { createApp } from '../src/main'; 4 | 5 | describe('AppController (e2e)', () => { 6 | let app: INestApplication; 7 | 8 | beforeEach(async () => { 9 | app = await createApp({ logger: false }); 10 | await app.init(); 11 | }); 12 | 13 | it('/ (GET)', () => { 14 | return request(app.getHttpServer()).get('/').expect(200).expect('Hello World!'); 15 | }); 16 | 17 | it('/user (POST) (test class-transformer)', async () => { 18 | const result = await request(app.getHttpServer()) 19 | .post('/user') 20 | .send({ email: 'christopher@crankman.com', password: '123456' }); 21 | // .expect(201); 22 | expect(result.body).toEqual({ 23 | email: 'christopher@crankman.com', 24 | password: '123456', 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/jest-e2e.config.js: -------------------------------------------------------------------------------- 1 | const loadJsonFile = require('load-json-file'); 2 | const { convert } = require('tsconfig-to-swcconfig'); 3 | 4 | const swcrc = loadJsonFile('./.swcrc'); 5 | const swcFromTsConfig = convert(undefined, undefined, { 6 | sourceMaps: false, 7 | }); 8 | 9 | const swcConfig = swcFromTsConfig; 10 | 11 | module.exports = { 12 | moduleFileExtensions: ['js', 'json', 'ts'], 13 | rootDir: '.', 14 | testEnvironment: 'node', 15 | testRegex: '.e2e-spec.ts$', 16 | transform: { 17 | '^.+\\.(t|j)sx?$': ['@swc/jest', swcConfig], 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ts-node": { 3 | "swc": true, 4 | "transpileOnly": true 5 | }, 6 | "compilerOptions": { 7 | "target": "es2019", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "importHelpers": true, 11 | "strict": true, 12 | "noUncheckedIndexedAccess": true, 13 | "noImplicitAny": false, 14 | "experimentalDecorators": true, 15 | "emitDecoratorMetadata": true, 16 | "outDir": "dist", 17 | "pretty": true, 18 | "esModuleInterop": true, 19 | "removeComments": false, 20 | "sourceMap": true, 21 | "declaration": false, 22 | "declarationMap": false, 23 | "skipLibCheck": true, 24 | "lib": ["esnext"], 25 | "baseUrl": ".", 26 | "paths": {} 27 | }, 28 | "include": ["src", "test"] 29 | } 30 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigEnv, defineConfig, loadEnv } from 'vite'; 2 | import { VitePluginNode } from 'vite-plugin-node'; 3 | import tsconfigPaths from 'vite-tsconfig-paths'; 4 | 5 | export default defineConfig(({ command, mode }: ConfigEnv) => { 6 | return { 7 | build: { 8 | target: 'es2020', 9 | }, 10 | define: { 11 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 12 | }, 13 | optimizeDeps: { 14 | // Vite does not work well with optionnal dependencies, mark them as ignored for now 15 | exclude: [ 16 | '@nestjs/platform-socket.io', 17 | '@nestjs/websockets', 18 | '@nestjs/microservices', 19 | 'amqp-connection-manager', 20 | 'amqplib', 21 | 'nats', 22 | '@grpc/proto-loader', 23 | '@grpc/grpc-js', 24 | 'redis', 25 | 'kafkajs', 26 | 'mqtt', 27 | 'cache-manager', 28 | ], 29 | }, 30 | plugins: [ 31 | tsconfigPaths(), 32 | ...VitePluginNode({ 33 | adapter: 'nest', 34 | appPath: './src/main.ts', 35 | tsCompiler: 'swc', 36 | }), 37 | ], 38 | }; 39 | }); 40 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const { envBool } = require('env-bool'); 2 | 3 | const hasWatchFlag = 4 | process.argv[1]?.endsWith('nest.js') && process.argv.includes('--watch'); 5 | 6 | /** 7 | * @param { import('webpack').Configuration } config 8 | * @param { import('webpack') } webpack 9 | */ 10 | module.exports = (config, webpack) => { 11 | const isWatch = config.watch || hasWatchFlag; 12 | const disableTsCheck = envBool(process.env.WORKSPACE_NO_TS_CHECK); 13 | 14 | Object.assign(config, { 15 | devtool: 'source-map', 16 | mode: 'production', 17 | }); 18 | 19 | Object.assign(config.optimization, { 20 | nodeEnv: 'production', 21 | minimize: false, 22 | moduleIds: 'named', 23 | chunkIds: 'named', 24 | }); 25 | 26 | Object.assign(config.output, { 27 | iife: false, 28 | }); 29 | 30 | config.plugins = config.plugins.filter(p => { 31 | const pluginName = p?.constructor?.name; 32 | if (isWatch && pluginName === 'ForkTsCheckerWebpackPlugin' && disableTsCheck) { 33 | return false; 34 | } 35 | return true; 36 | }); 37 | 38 | return config; 39 | }; 40 | --------------------------------------------------------------------------------