@tndhuy/create-app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +53 -0
  2. package/dist/cli.js +534 -0
  3. package/package.json +37 -0
  4. package/templates/mongo/.env.example +32 -0
  5. package/templates/mongo/Dockerfile +64 -0
  6. package/templates/mongo/docker-compose.yml +35 -0
  7. package/templates/mongo/eslint.config.mjs +35 -0
  8. package/templates/mongo/nest-cli.json +8 -0
  9. package/templates/mongo/package.json +105 -0
  10. package/templates/mongo/src/app.module.ts +59 -0
  11. package/templates/mongo/src/common/decorators/public-api.decorator.ts +9 -0
  12. package/templates/mongo/src/common/decorators/raw-response.decorator.ts +4 -0
  13. package/templates/mongo/src/common/filters/http-exception.filter.spec.ts +95 -0
  14. package/templates/mongo/src/common/filters/http-exception.filter.ts +43 -0
  15. package/templates/mongo/src/common/filters/rpc-exception.filter.ts +18 -0
  16. package/templates/mongo/src/common/index.ts +5 -0
  17. package/templates/mongo/src/common/interceptors/timeout.interceptor.ts +32 -0
  18. package/templates/mongo/src/common/interceptors/transform.interceptor.spec.ts +52 -0
  19. package/templates/mongo/src/common/interceptors/transform.interceptor.ts +25 -0
  20. package/templates/mongo/src/common/middleware/correlation-id.middleware.spec.ts +69 -0
  21. package/templates/mongo/src/common/middleware/correlation-id.middleware.ts +26 -0
  22. package/templates/mongo/src/infrastructure/cache/inject-redis.decorator.ts +4 -0
  23. package/templates/mongo/src/infrastructure/cache/redis.module.ts +9 -0
  24. package/templates/mongo/src/infrastructure/cache/redis.service.spec.ts +174 -0
  25. package/templates/mongo/src/infrastructure/cache/redis.service.ts +121 -0
  26. package/templates/mongo/src/infrastructure/config/config.module.ts +36 -0
  27. package/templates/mongo/src/infrastructure/config/environment.validation.spec.ts +100 -0
  28. package/templates/mongo/src/infrastructure/config/environment.validation.ts +21 -0
  29. package/templates/mongo/src/infrastructure/database/mongodb.module.ts +17 -0
  30. package/templates/mongo/src/infrastructure/health/health.controller.ts +46 -0
  31. package/templates/mongo/src/infrastructure/health/health.module.ts +12 -0
  32. package/templates/mongo/src/infrastructure/health/redis.health-indicator.ts +20 -0
  33. package/templates/mongo/src/instrumentation.spec.ts +24 -0
  34. package/templates/mongo/src/instrumentation.ts +44 -0
  35. package/templates/mongo/src/main.ts +102 -0
  36. package/templates/mongo/src/modules/example/application/commands/create-item.command.ts +3 -0
  37. package/templates/mongo/src/modules/example/application/commands/create-item.handler.spec.ts +49 -0
  38. package/templates/mongo/src/modules/example/application/commands/create-item.handler.ts +20 -0
  39. package/templates/mongo/src/modules/example/application/commands/delete-item.command.ts +3 -0
  40. package/templates/mongo/src/modules/example/application/commands/delete-item.handler.ts +15 -0
  41. package/templates/mongo/src/modules/example/application/dtos/create-item.dto.ts +9 -0
  42. package/templates/mongo/src/modules/example/application/dtos/item.response.dto.ts +9 -0
  43. package/templates/mongo/src/modules/example/application/queries/get-item.handler.spec.ts +49 -0
  44. package/templates/mongo/src/modules/example/application/queries/get-item.handler.ts +16 -0
  45. package/templates/mongo/src/modules/example/application/queries/get-item.query.ts +3 -0
  46. package/templates/mongo/src/modules/example/application/queries/list-items.handler.ts +16 -0
  47. package/templates/mongo/src/modules/example/application/queries/list-items.query.ts +3 -0
  48. package/templates/mongo/src/modules/example/domain/item-name.value-object.spec.ts +49 -0
  49. package/templates/mongo/src/modules/example/domain/item-name.value-object.ts +18 -0
  50. package/templates/mongo/src/modules/example/domain/item.entity.spec.ts +48 -0
  51. package/templates/mongo/src/modules/example/domain/item.entity.ts +19 -0
  52. package/templates/mongo/src/modules/example/domain/item.repository.interface.ts +10 -0
  53. package/templates/mongo/src/modules/example/example.module.ts +31 -0
  54. package/templates/mongo/src/modules/example/infrastructure/.gitkeep +0 -0
  55. package/templates/mongo/src/modules/example/infrastructure/persistence/mongoose-item.repository.ts +42 -0
  56. package/templates/mongo/src/modules/example/infrastructure/persistence/schemas/item.schema.ts +15 -0
  57. package/templates/mongo/src/modules/example/presenter/item.controller.ts +52 -0
  58. package/templates/mongo/src/shared/base/aggregate-root.spec.ts +44 -0
  59. package/templates/mongo/src/shared/base/aggregate-root.ts +20 -0
  60. package/templates/mongo/src/shared/base/domain-event.ts +6 -0
  61. package/templates/mongo/src/shared/base/entity.spec.ts +36 -0
  62. package/templates/mongo/src/shared/base/entity.ts +13 -0
  63. package/templates/mongo/src/shared/base/index.ts +5 -0
  64. package/templates/mongo/src/shared/base/repository.interface.ts +6 -0
  65. package/templates/mongo/src/shared/base/value-object.spec.ts +39 -0
  66. package/templates/mongo/src/shared/base/value-object.ts +13 -0
  67. package/templates/mongo/src/shared/dto/pagination.dto.spec.ts +49 -0
  68. package/templates/mongo/src/shared/dto/pagination.dto.ts +37 -0
  69. package/templates/mongo/src/shared/dto/response.dto.ts +13 -0
  70. package/templates/mongo/src/shared/exceptions/app.exception.spec.ts +59 -0
  71. package/templates/mongo/src/shared/exceptions/app.exception.ts +19 -0
  72. package/templates/mongo/src/shared/exceptions/error-codes.ts +9 -0
  73. package/templates/mongo/src/shared/index.ts +7 -0
  74. package/templates/mongo/src/shared/logger/logger.module.ts +12 -0
  75. package/templates/mongo/src/shared/logger/logger.service.ts +48 -0
  76. package/templates/mongo/src/shared/logger/pino.config.ts +86 -0
  77. package/templates/mongo/src/shared/validation-options.ts +38 -0
  78. package/templates/mongo/src/shared/valueobjects/date.valueobject.spec.ts +40 -0
  79. package/templates/mongo/src/shared/valueobjects/date.valueobject.ts +14 -0
  80. package/templates/mongo/src/shared/valueobjects/id.valueobject.spec.ts +28 -0
  81. package/templates/mongo/src/shared/valueobjects/id.valueobject.ts +14 -0
  82. package/templates/mongo/src/shared/valueobjects/index.ts +4 -0
  83. package/templates/mongo/src/shared/valueobjects/number.valueobject.spec.ts +48 -0
  84. package/templates/mongo/src/shared/valueobjects/number.valueobject.ts +14 -0
  85. package/templates/mongo/src/shared/valueobjects/string.valueobject.spec.ts +37 -0
  86. package/templates/mongo/src/shared/valueobjects/string.valueobject.ts +14 -0
  87. package/templates/mongo/tsconfig.build.json +4 -0
  88. package/templates/mongo/tsconfig.json +23 -0
  89. package/templates/postgres/.env.example +32 -0
  90. package/templates/postgres/Dockerfile +64 -0
  91. package/templates/postgres/eslint.config.mjs +35 -0
  92. package/templates/postgres/nest-cli.json +8 -0
  93. package/templates/postgres/package.json +103 -0
  94. package/templates/postgres/prisma/schema.prisma +14 -0
  95. package/templates/postgres/prisma.config.ts +11 -0
  96. package/templates/postgres/src/app.module.ts +34 -0
  97. package/templates/postgres/src/common/decorators/public-api.decorator.ts +9 -0
  98. package/templates/postgres/src/common/decorators/raw-response.decorator.ts +4 -0
  99. package/templates/postgres/src/common/filters/http-exception.filter.spec.ts +95 -0
  100. package/templates/postgres/src/common/filters/http-exception.filter.ts +43 -0
  101. package/templates/postgres/src/common/filters/rpc-exception.filter.ts +18 -0
  102. package/templates/postgres/src/common/index.ts +5 -0
  103. package/templates/postgres/src/common/interceptors/timeout.interceptor.ts +32 -0
  104. package/templates/postgres/src/common/interceptors/transform.interceptor.spec.ts +52 -0
  105. package/templates/postgres/src/common/interceptors/transform.interceptor.ts +25 -0
  106. package/templates/postgres/src/common/middleware/correlation-id.middleware.spec.ts +69 -0
  107. package/templates/postgres/src/common/middleware/correlation-id.middleware.ts +26 -0
  108. package/templates/postgres/src/infrastructure/cache/inject-redis.decorator.ts +4 -0
  109. package/templates/postgres/src/infrastructure/cache/redis.module.ts +9 -0
  110. package/templates/postgres/src/infrastructure/cache/redis.service.spec.ts +174 -0
  111. package/templates/postgres/src/infrastructure/cache/redis.service.ts +121 -0
  112. package/templates/postgres/src/infrastructure/config/config.module.ts +36 -0
  113. package/templates/postgres/src/infrastructure/config/environment.validation.spec.ts +100 -0
  114. package/templates/postgres/src/infrastructure/config/environment.validation.ts +21 -0
  115. package/templates/postgres/src/infrastructure/database/inject-prisma.decorator.ts +4 -0
  116. package/templates/postgres/src/infrastructure/database/prisma.module.ts +9 -0
  117. package/templates/postgres/src/infrastructure/database/prisma.service.ts +21 -0
  118. package/templates/postgres/src/infrastructure/health/health.controller.ts +46 -0
  119. package/templates/postgres/src/infrastructure/health/health.module.ts +12 -0
  120. package/templates/postgres/src/infrastructure/health/prisma.health-indicator.ts +19 -0
  121. package/templates/postgres/src/infrastructure/health/redis.health-indicator.ts +20 -0
  122. package/templates/postgres/src/instrumentation.spec.ts +24 -0
  123. package/templates/postgres/src/instrumentation.ts +44 -0
  124. package/templates/postgres/src/main.ts +102 -0
  125. package/templates/postgres/src/modules/example/application/commands/create-item.command.ts +3 -0
  126. package/templates/postgres/src/modules/example/application/commands/create-item.handler.spec.ts +49 -0
  127. package/templates/postgres/src/modules/example/application/commands/create-item.handler.ts +20 -0
  128. package/templates/postgres/src/modules/example/application/commands/delete-item.command.ts +3 -0
  129. package/templates/postgres/src/modules/example/application/commands/delete-item.handler.ts +15 -0
  130. package/templates/postgres/src/modules/example/application/dtos/create-item.dto.ts +9 -0
  131. package/templates/postgres/src/modules/example/application/dtos/item.response.dto.ts +9 -0
  132. package/templates/postgres/src/modules/example/application/queries/get-item.handler.spec.ts +49 -0
  133. package/templates/postgres/src/modules/example/application/queries/get-item.handler.ts +16 -0
  134. package/templates/postgres/src/modules/example/application/queries/get-item.query.ts +3 -0
  135. package/templates/postgres/src/modules/example/application/queries/list-items.handler.ts +16 -0
  136. package/templates/postgres/src/modules/example/application/queries/list-items.query.ts +3 -0
  137. package/templates/postgres/src/modules/example/domain/item-name.value-object.spec.ts +49 -0
  138. package/templates/postgres/src/modules/example/domain/item-name.value-object.ts +18 -0
  139. package/templates/postgres/src/modules/example/domain/item.entity.spec.ts +48 -0
  140. package/templates/postgres/src/modules/example/domain/item.entity.ts +19 -0
  141. package/templates/postgres/src/modules/example/domain/item.repository.interface.ts +10 -0
  142. package/templates/postgres/src/modules/example/example.module.ts +26 -0
  143. package/templates/postgres/src/modules/example/infrastructure/.gitkeep +0 -0
  144. package/templates/postgres/src/modules/example/infrastructure/persistence/prisma-item.repository.ts +34 -0
  145. package/templates/postgres/src/modules/example/presenter/item.controller.ts +52 -0
  146. package/templates/postgres/src/shared/base/aggregate-root.spec.ts +44 -0
  147. package/templates/postgres/src/shared/base/aggregate-root.ts +20 -0
  148. package/templates/postgres/src/shared/base/domain-event.ts +6 -0
  149. package/templates/postgres/src/shared/base/entity.spec.ts +36 -0
  150. package/templates/postgres/src/shared/base/entity.ts +13 -0
  151. package/templates/postgres/src/shared/base/index.ts +5 -0
  152. package/templates/postgres/src/shared/base/repository.interface.ts +6 -0
  153. package/templates/postgres/src/shared/base/value-object.spec.ts +39 -0
  154. package/templates/postgres/src/shared/base/value-object.ts +13 -0
  155. package/templates/postgres/src/shared/dto/pagination.dto.spec.ts +49 -0
  156. package/templates/postgres/src/shared/dto/pagination.dto.ts +37 -0
  157. package/templates/postgres/src/shared/dto/response.dto.ts +13 -0
  158. package/templates/postgres/src/shared/exceptions/app.exception.spec.ts +59 -0
  159. package/templates/postgres/src/shared/exceptions/app.exception.ts +19 -0
  160. package/templates/postgres/src/shared/exceptions/error-codes.ts +9 -0
  161. package/templates/postgres/src/shared/index.ts +7 -0
  162. package/templates/postgres/src/shared/logger/logger.module.ts +12 -0
  163. package/templates/postgres/src/shared/logger/logger.service.ts +48 -0
  164. package/templates/postgres/src/shared/logger/pino.config.ts +86 -0
  165. package/templates/postgres/src/shared/validation-options.ts +38 -0
  166. package/templates/postgres/src/shared/valueobjects/date.valueobject.spec.ts +40 -0
  167. package/templates/postgres/src/shared/valueobjects/date.valueobject.ts +14 -0
  168. package/templates/postgres/src/shared/valueobjects/id.valueobject.spec.ts +28 -0
  169. package/templates/postgres/src/shared/valueobjects/id.valueobject.ts +14 -0
  170. package/templates/postgres/src/shared/valueobjects/index.ts +4 -0
  171. package/templates/postgres/src/shared/valueobjects/number.valueobject.spec.ts +48 -0
  172. package/templates/postgres/src/shared/valueobjects/number.valueobject.ts +14 -0
  173. package/templates/postgres/src/shared/valueobjects/string.valueobject.spec.ts +37 -0
  174. package/templates/postgres/src/shared/valueobjects/string.valueobject.ts +14 -0
  175. package/templates/postgres/tsconfig.build.json +4 -0
  176. package/templates/postgres/tsconfig.json +23 -0
@@ -0,0 +1,64 @@
1
+ # =========================
2
+ # 1) Dependencies
3
+ # =========================
4
+ FROM node:22-alpine AS deps
5
+
6
+ RUN apk upgrade --no-cache \
7
+ && addgroup -g 1001 -S appgroup \
8
+ && adduser -S appuser -u 1001 -G appgroup
9
+
10
+ WORKDIR /app
11
+ RUN chown appuser:appgroup /app
12
+
13
+ COPY --chown=appuser:appgroup package*.json ./
14
+ COPY --chown=appuser:appgroup prisma ./prisma/
15
+ COPY --chown=appuser:appgroup prisma.config.ts ./
16
+
17
+ USER appuser
18
+
19
+ RUN npm ci --fetch-timeout=600000 --fetch-retries=5
20
+
21
+
22
+ # =========================
23
+ # 2) Builder
24
+ # =========================
25
+ FROM deps AS builder
26
+
27
+ COPY --chown=appuser:appgroup . .
28
+
29
+ RUN npx prisma generate && npm run build
30
+
31
+
32
+ # =========================
33
+ # 3) Runner (Production)
34
+ # =========================
35
+ FROM node:22-alpine AS runner
36
+
37
+ RUN apk upgrade --no-cache \
38
+ && apk add --no-cache curl \
39
+ && addgroup -g 1001 -S appgroup \
40
+ && adduser -S appuser -u 1001 -G appgroup
41
+
42
+ WORKDIR /app
43
+
44
+ COPY --from=builder --chown=appuser:appgroup /app/package*.json ./
45
+ COPY --from=builder --chown=appuser:appgroup /app/prisma ./prisma
46
+ COPY --from=builder --chown=appuser:appgroup /app/prisma.config.ts ./prisma.config.ts
47
+
48
+ RUN npm ci --omit=dev --fetch-timeout=600000 --fetch-retries=5 && npm cache clean --force
49
+
50
+ COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
51
+
52
+ RUN mkdir -p logs && chown appuser:appgroup logs
53
+
54
+ ENV NODE_ENV=production \
55
+ PORT=3000
56
+
57
+ USER appuser
58
+
59
+ EXPOSE 3000
60
+
61
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
62
+ CMD curl -f http://localhost:3000/health || exit 1
63
+
64
+ CMD ["node", "dist/src/main"]
@@ -0,0 +1,35 @@
1
+ services:
2
+ mongodb:
3
+ image: mongo:latest
4
+ container_name: nestjs-template-mongodb
5
+ restart: unless-stopped
6
+ ports:
7
+ - "27017:27017"
8
+ environment:
9
+ MONGO_INITDB_DATABASE: template_db
10
+ volumes:
11
+ - mongodb_data:/data/db
12
+ healthcheck:
13
+ test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
14
+ interval: 10s
15
+ timeout: 5s
16
+ retries: 10
17
+
18
+ redis:
19
+ image: redis:latest
20
+ container_name: nestjs-template-redis
21
+ restart: unless-stopped
22
+ ports:
23
+ - "6379:6379"
24
+ command: ["redis-server", "--appendonly", "yes"]
25
+ volumes:
26
+ - redis_data:/data
27
+ healthcheck:
28
+ test: ["CMD", "redis-cli", "ping"]
29
+ interval: 10s
30
+ timeout: 5s
31
+ retries: 10
32
+
33
+ volumes:
34
+ mongodb_data:
35
+ redis_data:
@@ -0,0 +1,35 @@
1
+ // @ts-check
2
+ import eslint from '@eslint/js';
3
+ import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
4
+ import globals from 'globals';
5
+ import tseslint from 'typescript-eslint';
6
+
7
+ export default tseslint.config(
8
+ {
9
+ ignores: ['eslint.config.mjs'],
10
+ },
11
+ eslint.configs.recommended,
12
+ ...tseslint.configs.recommendedTypeChecked,
13
+ eslintPluginPrettierRecommended,
14
+ {
15
+ languageOptions: {
16
+ globals: {
17
+ ...globals.node,
18
+ ...globals.jest,
19
+ },
20
+ sourceType: 'commonjs',
21
+ parserOptions: {
22
+ projectService: true,
23
+ tsconfigRootDir: import.meta.dirname,
24
+ },
25
+ },
26
+ },
27
+ {
28
+ rules: {
29
+ '@typescript-eslint/no-explicit-any': 'off',
30
+ '@typescript-eslint/no-floating-promises': 'warn',
31
+ '@typescript-eslint/no-unsafe-argument': 'warn',
32
+ "prettier/prettier": ["error", { endOfLine: "auto" }],
33
+ },
34
+ },
35
+ );
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "src",
5
+ "compilerOptions": {
6
+ "deleteOutDir": true
7
+ }
8
+ }
@@ -0,0 +1,105 @@
1
+ {
2
+ "name": "nestjs-backend-template",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "author": "",
6
+ "private": true,
7
+ "license": "UNLICENSED",
8
+ "scripts": {
9
+ "build": "nest build",
10
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11
+ "start": "nest start",
12
+ "start:dev": "nest start --watch",
13
+ "start:debug": "nest start --debug --watch",
14
+ "start:prod": "node dist/src/main",
15
+ "db:generate": "prisma generate",
16
+ "db:sync": "prisma db push",
17
+ "db:sync:force": "prisma db push --accept-data-loss",
18
+ "db:push": "prisma db push",
19
+ "db:pull": "prisma db pull",
20
+ "db:validate": "prisma validate",
21
+ "db:format": "prisma format",
22
+ "db:studio": "prisma studio",
23
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
24
+ "test": "jest",
25
+ "test:watch": "jest --watch",
26
+ "test:cov": "jest --coverage",
27
+ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
28
+ "test:e2e": "jest --config ./test/jest-e2e.json"
29
+ },
30
+ "dependencies": {
31
+ "@nestjs/common": "^11.0.1",
32
+ "@nestjs/config": "^4.0.3",
33
+ "@nestjs/core": "^11.0.1",
34
+ "@nestjs/cqrs": "^11.0.3",
35
+ "@nestjs/mongoose": "^11.0.4",
36
+ "@nestjs/platform-express": "^11.0.1",
37
+ "@nestjs/swagger": "^11.2.6",
38
+ "@nestjs/terminus": "^11.1.1",
39
+ "@nestjs/throttler": "^6.5.0",
40
+ "@opentelemetry/api": "^1.9.1",
41
+ "@opentelemetry/auto-instrumentations-node": "^0.72.0",
42
+ "@opentelemetry/exporter-prometheus": "^0.214.0",
43
+ "@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
44
+ "@opentelemetry/resources": "^2.6.1",
45
+ "@opentelemetry/sdk-node": "^0.214.0",
46
+ "@opentelemetry/sdk-trace-base": "^2.6.1",
47
+ "@prisma/client": "^7.5.0",
48
+ "@scalar/nestjs-api-reference": "^1.1.5",
49
+ "class-transformer": "^0.5.1",
50
+ "class-validator": "^0.15.1",
51
+ "cockatiel": "^3.2.1",
52
+ "express-basic-auth": "^1.2.1",
53
+ "ioredis": "^5.10.1",
54
+ "mongoose": "^9.4.1",
55
+ "nestjs-pino": "^4.6.1",
56
+ "pino-http": "^11.0.0",
57
+ "pino-pretty": "^13.1.3",
58
+ "reflect-metadata": "^0.2.2",
59
+ "rxjs": "^7.8.1"
60
+ },
61
+ "devDependencies": {
62
+ "@eslint/eslintrc": "^3.2.0",
63
+ "@eslint/js": "^9.18.0",
64
+ "@nestjs/cli": "^11.0.0",
65
+ "@nestjs/schematics": "^11.0.0",
66
+ "@nestjs/testing": "^11.0.1",
67
+ "@types/express": "^5.0.0",
68
+ "@types/ioredis": "^5.0.0",
69
+ "@types/jest": "^30.0.0",
70
+ "@types/node": "^22.10.7",
71
+ "@types/supertest": "^6.0.2",
72
+ "eslint": "^9.18.0",
73
+ "eslint-config-prettier": "^10.0.1",
74
+ "eslint-plugin-prettier": "^5.2.2",
75
+ "globals": "^16.0.0",
76
+ "jest": "^30.0.0",
77
+ "prettier": "^3.4.2",
78
+ "prisma": "^7.5.0",
79
+ "source-map-support": "^0.5.21",
80
+ "supertest": "^7.0.0",
81
+ "ts-jest": "^29.2.5",
82
+ "ts-loader": "^9.5.2",
83
+ "ts-node": "^10.9.2",
84
+ "tsconfig-paths": "^4.2.0",
85
+ "typescript": "^5.7.3",
86
+ "typescript-eslint": "^8.20.0"
87
+ },
88
+ "jest": {
89
+ "moduleFileExtensions": [
90
+ "js",
91
+ "json",
92
+ "ts"
93
+ ],
94
+ "rootDir": "src",
95
+ "testRegex": ".*\\.spec\\.ts$",
96
+ "transform": {
97
+ "^.+\\.(t|j)s$": "ts-jest"
98
+ },
99
+ "collectCoverageFrom": [
100
+ "**/*.(t|j)s"
101
+ ],
102
+ "coverageDirectory": "../coverage",
103
+ "testEnvironment": "node"
104
+ }
105
+ }
@@ -0,0 +1,59 @@
1
+ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
2
+ import { APP_GUARD } from '@nestjs/core';
3
+ import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
4
+ import { LoggerModule } from 'nestjs-pino';
5
+ import { AppConfigModule } from './infrastructure/config/config.module';
6
+ import { MongoDBModule } from './infrastructure/database/mongodb.module';
7
+ import { CacheModule } from './infrastructure/cache/redis.module';
8
+ import { HealthModule } from './infrastructure/health/health.module';
9
+ import { ExampleModule } from './modules/example/example.module';
10
+ import { CorrelationIdMiddleware } from './common/middleware/correlation-id.middleware';
11
+
12
+ @Module({
13
+ imports: [
14
+ AppConfigModule,
15
+ MongoDBModule,
16
+ CacheModule,
17
+ HealthModule,
18
+ ExampleModule,
19
+ LoggerModule.forRoot({
20
+ pinoHttp: {
21
+ level: process.env.NODE_ENV !== 'production' ? 'debug' : 'info',
22
+ genReqId: (req) => req.headers['x-correlation-id'] as string,
23
+ transport:
24
+ process.env.NODE_ENV !== 'production'
25
+ ? {
26
+ target: 'pino-pretty',
27
+ options: {
28
+ colorize: true,
29
+ translateTime: 'SYS:yyyy-mm-dd HH:MM:ss',
30
+ ignore: 'pid,hostname',
31
+ },
32
+ }
33
+ : undefined,
34
+ serializers: {
35
+ req: (req: { method: string; url: string }) => ({
36
+ method: req.method,
37
+ url: req.url,
38
+ }),
39
+ res: (res: { statusCode: number }) => ({
40
+ statusCode: res.statusCode,
41
+ }),
42
+ },
43
+ },
44
+ }),
45
+ ThrottlerModule.forRoot([
46
+ {
47
+ ttl: parseInt(process.env.THROTTLE_TTL ?? '60', 10) * 1000,
48
+ limit: parseInt(process.env.THROTTLE_LIMIT ?? '100', 10),
49
+ },
50
+ ]),
51
+ ],
52
+ controllers: [],
53
+ providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
54
+ })
55
+ export class AppModule implements NestModule {
56
+ configure(consumer: MiddlewareConsumer) {
57
+ consumer.apply(CorrelationIdMiddleware).forRoutes('*');
58
+ }
59
+ }
@@ -0,0 +1,9 @@
1
+ import { SetMetadata } from '@nestjs/common';
2
+
3
+ export const PUBLIC_API_KEY = 'public_api';
4
+ /**
5
+ * Decorator to mark an endpoint as Public API.
6
+ * This will trigger the TransformInterceptor to wrap the response in { success: true, data: T }.
7
+ * Without this decorator, the response remains raw.
8
+ */
9
+ export const PublicApi = () => SetMetadata(PUBLIC_API_KEY, true);
@@ -0,0 +1,4 @@
1
+ import { SetMetadata } from '@nestjs/common';
2
+ import { RAW_RESPONSE_KEY } from '../interceptors/transform.interceptor';
3
+
4
+ export const RawResponse = () => SetMetadata(RAW_RESPONSE_KEY, true);
@@ -0,0 +1,95 @@
1
+ import { HttpException } from '@nestjs/common';
2
+ import { AppException } from '../../shared/exceptions/app.exception';
3
+ import { ErrorCodes } from '../../shared/exceptions/error-codes';
4
+ import { HttpExceptionFilter } from './http-exception.filter';
5
+
6
+ function createMockHost(jsonMock: jest.Mock) {
7
+ const statusMock = jest.fn().mockReturnValue({ json: jsonMock });
8
+ const response = { status: statusMock };
9
+ const request = { method: 'GET', url: '/test' };
10
+ return {
11
+ switchToHttp: () => ({
12
+ getResponse: () => response,
13
+ getRequest: () => request,
14
+ }),
15
+ } as any;
16
+ }
17
+
18
+ describe('HttpExceptionFilter', () => {
19
+ let filter: HttpExceptionFilter;
20
+ let jsonMock: jest.Mock;
21
+
22
+ beforeEach(() => {
23
+ filter = new HttpExceptionFilter();
24
+ jsonMock = jest.fn();
25
+ });
26
+
27
+ it('should format AppException(NOT_FOUND) to correct error shape', () => {
28
+ const exception = new AppException({
29
+ code: ErrorCodes.NOT_FOUND,
30
+ message: 'Item not found',
31
+ statusCode: 404,
32
+ });
33
+ const host = createMockHost(jsonMock);
34
+
35
+ filter.catch(exception, host);
36
+
37
+ expect(jsonMock).toHaveBeenCalledWith({
38
+ success: false,
39
+ error: {
40
+ code: 'NOT_FOUND',
41
+ message: 'Item not found',
42
+ statusCode: 404,
43
+ details: null,
44
+ },
45
+ });
46
+ });
47
+
48
+ it('should include details array from AppException', () => {
49
+ const details = [{ field: 'email', constraints: { isEmail: 'must be email' } }];
50
+ const exception = new AppException({
51
+ code: ErrorCodes.VALIDATION_FAILED,
52
+ message: 'Validation failed',
53
+ statusCode: 400,
54
+ details,
55
+ });
56
+ const host = createMockHost(jsonMock);
57
+
58
+ filter.catch(exception, host);
59
+
60
+ expect(jsonMock).toHaveBeenCalledWith({
61
+ success: false,
62
+ error: {
63
+ code: 'VALIDATION_FAILED',
64
+ message: 'Validation failed',
65
+ statusCode: 400,
66
+ details,
67
+ },
68
+ });
69
+ });
70
+
71
+ it('should handle generic HttpException(403) with FORBIDDEN code', () => {
72
+ const exception = new HttpException('Forbidden', 403);
73
+ const host = createMockHost(jsonMock);
74
+
75
+ filter.catch(exception, host);
76
+
77
+ const call = jsonMock.mock.calls[0][0];
78
+ expect(call.success).toBe(false);
79
+ expect(call.error.statusCode).toBe(403);
80
+ expect(call.error.code).toBe('FORBIDDEN');
81
+ expect(call.error.details).toBeNull();
82
+ });
83
+
84
+ it('should handle generic HttpException(500) with INTERNAL_SERVER_ERROR code', () => {
85
+ const exception = new HttpException('Internal Server Error', 500);
86
+ const host = createMockHost(jsonMock);
87
+
88
+ filter.catch(exception, host);
89
+
90
+ const call = jsonMock.mock.calls[0][0];
91
+ expect(call.success).toBe(false);
92
+ expect(call.error.statusCode).toBe(500);
93
+ expect(call.error.code).toBe('INTERNAL_SERVER_ERROR');
94
+ });
95
+ });
@@ -0,0 +1,43 @@
1
+ import {
2
+ ArgumentsHost,
3
+ Catch,
4
+ ExceptionFilter,
5
+ HttpException,
6
+ HttpStatus,
7
+ } from '@nestjs/common';
8
+ import { Request, Response } from 'express';
9
+ import { LoggerService } from '../../shared/logger/logger.service';
10
+
11
+ @Catch(HttpException)
12
+ export class HttpExceptionFilter implements ExceptionFilter {
13
+ constructor(private readonly logger: LoggerService) {}
14
+
15
+ catch(exception: HttpException, host: ArgumentsHost): void {
16
+ const ctx = host.switchToHttp();
17
+ const response = ctx.getResponse<Response>();
18
+ const request = ctx.getRequest<Request>();
19
+
20
+ const statusCode = exception.getStatus();
21
+ const res = exception.getResponse() as any;
22
+
23
+ const code = res.error || HttpStatus[statusCode] || 'INTERNAL_ERROR';
24
+ const message = res.message || exception.message;
25
+
26
+ this.logger.error(
27
+ `[${request.method}] ${request.url} - ${statusCode} ${code}: ${message}`,
28
+ exception.stack,
29
+ );
30
+
31
+ // Standardized Error Response
32
+ response.status(statusCode).json({
33
+ success: false,
34
+ error: {
35
+ code,
36
+ message,
37
+ statusCode,
38
+ timestamp: new Date().toISOString(),
39
+ path: request.url,
40
+ },
41
+ });
42
+ }
43
+ }
@@ -0,0 +1,18 @@
1
+ // RPC Exception Filter — placeholder for microservice completeness.
2
+ // Requires @nestjs/microservices to be installed.
3
+ // When @nestjs/microservices is added, uncomment the implementation below.
4
+
5
+ /*
6
+ import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
7
+ import { RpcException } from '@nestjs/microservices';
8
+ import { throwError } from 'rxjs';
9
+
10
+ @Catch(RpcException)
11
+ export class RpcExceptionFilter implements ExceptionFilter {
12
+ catch(exception: RpcException, _host: ArgumentsHost) {
13
+ return throwError(() => exception.getError());
14
+ }
15
+ }
16
+ */
17
+
18
+ export {};
@@ -0,0 +1,5 @@
1
+ export * from './filters/http-exception.filter';
2
+ export * from './interceptors/transform.interceptor';
3
+ export * from './interceptors/timeout.interceptor';
4
+ export * from './decorators/raw-response.decorator';
5
+ export * from './middleware/correlation-id.middleware';
@@ -0,0 +1,32 @@
1
+ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
2
+ import { Observable, throwError, TimeoutError } from 'rxjs';
3
+ import { catchError, timeout } from 'rxjs/operators';
4
+ import { AppException } from '../../shared/exceptions/app.exception';
5
+
6
+ const DEFAULT_TIMEOUT_MS = 30000;
7
+
8
+ @Injectable()
9
+ export class TimeoutInterceptor implements NestInterceptor {
10
+ intercept(_context: ExecutionContext, next: CallHandler): Observable<unknown> {
11
+ const timeoutMs = process.env.REQUEST_TIMEOUT
12
+ ? parseInt(process.env.REQUEST_TIMEOUT, 10)
13
+ : DEFAULT_TIMEOUT_MS;
14
+
15
+ return next.handle().pipe(
16
+ timeout(timeoutMs),
17
+ catchError((err) => {
18
+ if (err instanceof TimeoutError) {
19
+ return throwError(
20
+ () =>
21
+ new AppException({
22
+ code: 'REQUEST_TIMEOUT',
23
+ message: 'Request timeout',
24
+ statusCode: 408,
25
+ }),
26
+ );
27
+ }
28
+ return throwError(() => err);
29
+ }),
30
+ );
31
+ }
32
+ }
@@ -0,0 +1,52 @@
1
+ import { Reflector } from '@nestjs/core';
2
+ import { of } from 'rxjs';
3
+ import { RAW_RESPONSE_KEY, TransformInterceptor } from './transform.interceptor';
4
+
5
+ function createMockContext(isRaw: boolean) {
6
+ const reflector = new Reflector();
7
+ jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(isRaw);
8
+ const context = {
9
+ getHandler: () => jest.fn(),
10
+ getClass: () => jest.fn(),
11
+ } as any;
12
+ return { reflector, context };
13
+ }
14
+
15
+ describe('TransformInterceptor', () => {
16
+ it('should wrap controller return value in { success: true, data }', (done) => {
17
+ const { reflector, context } = createMockContext(false);
18
+ const interceptor = new TransformInterceptor(reflector);
19
+ const next = { handle: () => of({ id: '1', name: 'test' }) };
20
+
21
+ interceptor.intercept(context, next).subscribe((result) => {
22
+ expect(result).toEqual({ success: true, data: { id: '1', name: 'test' } });
23
+ done();
24
+ });
25
+ });
26
+
27
+ it('should NOT wrap when @RawResponse() is used', (done) => {
28
+ const { reflector, context } = createMockContext(true);
29
+ const interceptor = new TransformInterceptor(reflector);
30
+ const next = { handle: () => of({ status: 'ok' }) };
31
+
32
+ interceptor.intercept(context, next).subscribe((result) => {
33
+ expect(result).toEqual({ status: 'ok' });
34
+ done();
35
+ });
36
+ });
37
+
38
+ it('should wrap null return value in { success: true, data: null }', (done) => {
39
+ const { reflector, context } = createMockContext(false);
40
+ const interceptor = new TransformInterceptor(reflector);
41
+ const next = { handle: () => of(null) };
42
+
43
+ interceptor.intercept(context, next).subscribe((result) => {
44
+ expect(result).toEqual({ success: true, data: null });
45
+ done();
46
+ });
47
+ });
48
+
49
+ it('should use RAW_RESPONSE_KEY constant', () => {
50
+ expect(RAW_RESPONSE_KEY).toBe('raw_response');
51
+ });
52
+ });
@@ -0,0 +1,25 @@
1
+ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
2
+ import { Reflector } from '@nestjs/core';
3
+ import { Observable } from 'rxjs';
4
+ import { map } from 'rxjs/operators';
5
+ import { PUBLIC_API_KEY } from '../decorators/public-api.decorator';
6
+
7
+ @Injectable()
8
+ export class TransformInterceptor<T> implements NestInterceptor<T, unknown> {
9
+ constructor(private readonly reflector: Reflector) {}
10
+
11
+ intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
12
+ const isPublic = this.reflector.getAllAndOverride<boolean>(PUBLIC_API_KEY, [
13
+ context.getHandler(),
14
+ context.getClass(),
15
+ ]);
16
+
17
+ // Internal first: If not explicitly marked as Public API, return RAW data
18
+ if (!isPublic) {
19
+ return next.handle();
20
+ }
21
+
22
+ // Public API: Wrap response in a standardized success object
23
+ return next.handle().pipe(map((data) => ({ success: true, data })));
24
+ }
25
+ }
@@ -0,0 +1,69 @@
1
+ import { CORRELATION_ID_HEADER, CorrelationIdMiddleware } from './correlation-id.middleware';
2
+
3
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4
+
5
+ function createMockReq(headers: Record<string, string> = {}): any {
6
+ return { headers: { ...headers } };
7
+ }
8
+
9
+ function createMockRes(): any {
10
+ const headers: Record<string, string> = {};
11
+ return {
12
+ setHeader: (name: string, value: string) => {
13
+ headers[name] = value;
14
+ },
15
+ _headers: headers,
16
+ };
17
+ }
18
+
19
+ describe('CorrelationIdMiddleware', () => {
20
+ let middleware: CorrelationIdMiddleware;
21
+
22
+ beforeEach(() => {
23
+ middleware = new CorrelationIdMiddleware();
24
+ });
25
+
26
+ it('should generate UUID v4 when X-Correlation-ID header is absent', () => {
27
+ const req = createMockReq();
28
+ const res = createMockRes();
29
+ const next = jest.fn();
30
+
31
+ middleware.use(req, res, next);
32
+
33
+ const correlationId = req.headers[CORRELATION_ID_HEADER.toLowerCase()];
34
+ expect(correlationId).toMatch(UUID_PATTERN);
35
+ expect(next).toHaveBeenCalled();
36
+ });
37
+
38
+ it('should preserve existing X-Correlation-ID header', () => {
39
+ const req = createMockReq({ 'x-correlation-id': 'abc-123' });
40
+ const res = createMockRes();
41
+ const next = jest.fn();
42
+
43
+ middleware.use(req, res, next);
44
+
45
+ expect(req.headers['x-correlation-id']).toBe('abc-123');
46
+ expect(res._headers[CORRELATION_ID_HEADER]).toBe('abc-123');
47
+ });
48
+
49
+ it('should always set X-Correlation-ID on the response header', () => {
50
+ const req = createMockReq();
51
+ const res = createMockRes();
52
+ const next = jest.fn();
53
+
54
+ middleware.use(req, res, next);
55
+
56
+ expect(res._headers[CORRELATION_ID_HEADER]).toBeDefined();
57
+ expect(res._headers[CORRELATION_ID_HEADER]).toMatch(UUID_PATTERN);
58
+ });
59
+
60
+ it('should call next()', () => {
61
+ const req = createMockReq();
62
+ const res = createMockRes();
63
+ const next = jest.fn();
64
+
65
+ middleware.use(req, res, next);
66
+
67
+ expect(next).toHaveBeenCalledTimes(1);
68
+ });
69
+ });