@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,48 @@
1
+ import { NumberValueObject } from './number.valueobject';
2
+
3
+ describe('NumberValueObject', () => {
4
+ it('should store and return the numeric value', () => {
5
+ const vo = new NumberValueObject(42);
6
+ expect(vo.value).toBe(42);
7
+ });
8
+
9
+ it('should store zero as a valid value', () => {
10
+ const vo = new NumberValueObject(0);
11
+ expect(vo.value).toBe(0);
12
+ });
13
+
14
+ it('should store negative numbers', () => {
15
+ const vo = new NumberValueObject(-7);
16
+ expect(vo.value).toBe(-7);
17
+ });
18
+
19
+ it('should throw when constructed with NaN', () => {
20
+ expect(() => new NumberValueObject(NaN)).toThrow(
21
+ 'Number value must be a finite number',
22
+ );
23
+ });
24
+
25
+ it('should throw when constructed with Infinity', () => {
26
+ expect(() => new NumberValueObject(Infinity)).toThrow(
27
+ 'Number value must be a finite number',
28
+ );
29
+ });
30
+
31
+ it('should throw when constructed with negative Infinity', () => {
32
+ expect(() => new NumberValueObject(-Infinity)).toThrow(
33
+ 'Number value must be a finite number',
34
+ );
35
+ });
36
+
37
+ it('should be equal to another NumberValueObject with the same value', () => {
38
+ const vo1 = new NumberValueObject(99);
39
+ const vo2 = new NumberValueObject(99);
40
+ expect(vo1.equals(vo2)).toBe(true);
41
+ });
42
+
43
+ it('should not be equal to a NumberValueObject with a different value', () => {
44
+ const vo1 = new NumberValueObject(1);
45
+ const vo2 = new NumberValueObject(2);
46
+ expect(vo1.equals(vo2)).toBe(false);
47
+ });
48
+ });
@@ -0,0 +1,14 @@
1
+ import { ValueObject } from '../base/value-object';
2
+
3
+ export class NumberValueObject extends ValueObject<{ value: number }> {
4
+ constructor(value: number) {
5
+ if (!Number.isFinite(value)) {
6
+ throw new Error('Number value must be a finite number');
7
+ }
8
+ super({ value });
9
+ }
10
+
11
+ get value(): number {
12
+ return this.props.value;
13
+ }
14
+ }
@@ -0,0 +1,37 @@
1
+ import { StringValueObject } from './string.valueobject';
2
+
3
+ describe('StringValueObject', () => {
4
+ it('should store and return the string value', () => {
5
+ const vo = new StringValueObject('hello');
6
+ expect(vo.value).toBe('hello');
7
+ });
8
+
9
+ it('should throw when constructed with null', () => {
10
+ expect(() => new StringValueObject(null as any)).toThrow(
11
+ 'String value must not be null or undefined',
12
+ );
13
+ });
14
+
15
+ it('should throw when constructed with undefined', () => {
16
+ expect(() => new StringValueObject(undefined as any)).toThrow(
17
+ 'String value must not be null or undefined',
18
+ );
19
+ });
20
+
21
+ it('should be equal to another StringValueObject with the same value', () => {
22
+ const vo1 = new StringValueObject('hello');
23
+ const vo2 = new StringValueObject('hello');
24
+ expect(vo1.equals(vo2)).toBe(true);
25
+ });
26
+
27
+ it('should not be equal to a StringValueObject with a different value', () => {
28
+ const vo1 = new StringValueObject('hello');
29
+ const vo2 = new StringValueObject('world');
30
+ expect(vo1.equals(vo2)).toBe(false);
31
+ });
32
+
33
+ it('should allow empty strings (only null/undefined are rejected)', () => {
34
+ const vo = new StringValueObject('');
35
+ expect(vo.value).toBe('');
36
+ });
37
+ });
@@ -0,0 +1,14 @@
1
+ import { ValueObject } from '../base/value-object';
2
+
3
+ export class StringValueObject extends ValueObject<{ value: string }> {
4
+ constructor(value: string) {
5
+ if (value === null || value === undefined) {
6
+ throw new Error('String value must not be null or undefined');
7
+ }
8
+ super({ value });
9
+ }
10
+
11
+ get value(): string {
12
+ return this.props.value;
13
+ }
14
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
4
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "declaration": true,
5
+ "removeComments": true,
6
+ "emitDecoratorMetadata": true,
7
+ "experimentalDecorators": true,
8
+ "allowSyntheticDefaultImports": true,
9
+ "esModuleInterop": true,
10
+ "target": "ES2023",
11
+ "sourceMap": true,
12
+ "outDir": "./dist",
13
+ "baseUrl": "./",
14
+ "paths": {
15
+ "@shared/*": ["src/shared/*"]
16
+ },
17
+ "incremental": true,
18
+ "skipLibCheck": true,
19
+ "strictNullChecks": true,
20
+ "noImplicitAny": false
21
+ },
22
+ "exclude": ["node_modules", "dist", "test", "packages"]
23
+ }
@@ -0,0 +1,32 @@
1
+ # Application
2
+ PORT=3000
3
+ NODE_ENV=development
4
+
5
+ # Database
6
+ DATABASE_URL=postgresql://user:password@localhost:5432/template_db?schema=public
7
+
8
+ # Redis
9
+ REDIS_URL=redis://localhost:6379
10
+
11
+ # API
12
+ API_VERSION=1
13
+ APP_NAME=NestJS Backend Template
14
+ APP_DESCRIPTION=API Documentation
15
+ REQUEST_TIMEOUT=30000
16
+
17
+ # Rate Limiting
18
+ THROTTLE_TTL=60
19
+ THROTTLE_LIMIT=100
20
+
21
+ # Docs
22
+ DOCS_USER=admin
23
+ DOCS_PASS=admin
24
+
25
+ # Observability - Logging
26
+ # LOG_LEVEL is controlled by NODE_ENV (debug in dev, info in prod)
27
+
28
+ # Observability - OpenTelemetry (opt-in)
29
+ OTEL_ENABLED=false
30
+ OTEL_SERVICE_NAME=nestjs-backend-template
31
+ OTEL_PROMETHEUS_PORT=9464
32
+ OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
@@ -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
+ // @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,103 @@
1
+ {
2
+ "name": "nestjs-backend-template",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "author": "",
6
+ "private": true,
7
+ "license": "UNLICENSED",
8
+ "workspaces": [
9
+ "packages/*"
10
+ ],
11
+ "scripts": {
12
+ "build": "nest build",
13
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
14
+ "start": "nest start",
15
+ "start:dev": "nest start --watch",
16
+ "start:debug": "nest start --debug --watch",
17
+ "start:prod": "node dist/src/main",
18
+ "db:migrate:deploy": "prisma migrate deploy",
19
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
20
+ "test": "jest",
21
+ "test:watch": "jest --watch",
22
+ "test:cov": "jest --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
+ },
26
+ "dependencies": {
27
+ "@nestjs/common": "^11.0.1",
28
+ "@nestjs/config": "^4.0.3",
29
+ "@nestjs/core": "^11.0.1",
30
+ "@nestjs/cqrs": "^11.0.3",
31
+ "@nestjs/platform-express": "^11.0.1",
32
+ "@nestjs/swagger": "^11.2.6",
33
+ "@nestjs/terminus": "^11.1.1",
34
+ "@nestjs/throttler": "^6.5.0",
35
+ "@opentelemetry/api": "^1.9.1",
36
+ "@opentelemetry/auto-instrumentations-node": "^0.72.0",
37
+ "@opentelemetry/exporter-prometheus": "^0.214.0",
38
+ "@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
39
+ "@opentelemetry/resources": "^2.6.1",
40
+ "@opentelemetry/sdk-node": "^0.214.0",
41
+ "@opentelemetry/sdk-trace-base": "^2.6.1",
42
+ "@prisma/adapter-pg": "^7.5.0",
43
+ "@prisma/client": "^7.5.0",
44
+ "@scalar/nestjs-api-reference": "^1.1.5",
45
+ "@types/pg": "^8.20.0",
46
+ "class-transformer": "^0.5.1",
47
+ "class-validator": "^0.15.1",
48
+ "cockatiel": "^3.2.1",
49
+ "express-basic-auth": "^1.2.1",
50
+ "ioredis": "^5.10.1",
51
+ "nestjs-pino": "^4.6.1",
52
+ "pg": "^8.20.0",
53
+ "pino-http": "^11.0.0",
54
+ "pino-pretty": "^13.1.3",
55
+ "pino-roll": "^4.0.0",
56
+ "reflect-metadata": "^0.2.2",
57
+ "rxjs": "^7.8.1"
58
+ },
59
+ "devDependencies": {
60
+ "@eslint/eslintrc": "^3.2.0",
61
+ "@eslint/js": "^9.18.0",
62
+ "@nestjs/cli": "^11.0.0",
63
+ "@nestjs/schematics": "^11.0.0",
64
+ "@nestjs/testing": "^11.0.1",
65
+ "@types/express": "^5.0.0",
66
+ "@types/ioredis": "^5.0.0",
67
+ "@types/jest": "^30.0.0",
68
+ "@types/node": "^22.10.7",
69
+ "@types/supertest": "^6.0.2",
70
+ "eslint": "^9.18.0",
71
+ "eslint-config-prettier": "^10.0.1",
72
+ "eslint-plugin-prettier": "^5.2.2",
73
+ "globals": "^16.0.0",
74
+ "jest": "^30.0.0",
75
+ "prettier": "^3.4.2",
76
+ "prisma": "^7.5.0",
77
+ "source-map-support": "^0.5.21",
78
+ "supertest": "^7.0.0",
79
+ "ts-jest": "^29.2.5",
80
+ "ts-loader": "^9.5.2",
81
+ "ts-node": "^10.9.2",
82
+ "tsconfig-paths": "^4.2.0",
83
+ "typescript": "^5.7.3",
84
+ "typescript-eslint": "^8.20.0"
85
+ },
86
+ "jest": {
87
+ "moduleFileExtensions": [
88
+ "js",
89
+ "json",
90
+ "ts"
91
+ ],
92
+ "rootDir": "src",
93
+ "testRegex": ".*\\.spec\\.ts$",
94
+ "transform": {
95
+ "^.+\\.(t|j)s$": "ts-jest"
96
+ },
97
+ "collectCoverageFrom": [
98
+ "**/*.(t|j)s"
99
+ ],
100
+ "coverageDirectory": "../coverage",
101
+ "testEnvironment": "node"
102
+ }
103
+ }
@@ -0,0 +1,14 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "postgresql"
7
+ }
8
+
9
+ model Item {
10
+ id String @id
11
+ name String
12
+ createdAt DateTime @default(now())
13
+ updatedAt DateTime @updatedAt
14
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig, env } from 'prisma/config';
2
+
3
+ export default defineConfig({
4
+ schema: 'prisma/schema.prisma',
5
+ migrations: {
6
+ path: 'prisma/migrations',
7
+ },
8
+ datasource: {
9
+ url: process.env.DATABASE_URL ?? 'postgresql://localhost:5432/app',
10
+ },
11
+ });
@@ -0,0 +1,34 @@
1
+ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
2
+ import { APP_GUARD } from '@nestjs/core';
3
+ import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
4
+ import { AppConfigModule } from './infrastructure/config/config.module';
5
+ import { DatabaseModule } from './infrastructure/database/prisma.module';
6
+ import { CacheModule } from './infrastructure/cache/redis.module';
7
+ import { HealthModule } from './infrastructure/health/health.module';
8
+ import { ExampleModule } from './modules/example/example.module';
9
+ import { CorrelationIdMiddleware } from './common/middleware/correlation-id.middleware';
10
+ import { LoggerModule } from './shared/logger/logger.module';
11
+
12
+ @Module({
13
+ imports: [
14
+ AppConfigModule,
15
+ DatabaseModule,
16
+ CacheModule,
17
+ HealthModule,
18
+ ExampleModule,
19
+ LoggerModule,
20
+ ThrottlerModule.forRoot([
21
+ {
22
+ ttl: parseInt(process.env.THROTTLE_TTL ?? '60', 10) * 1000,
23
+ limit: parseInt(process.env.THROTTLE_LIMIT ?? '100', 10),
24
+ },
25
+ ]),
26
+ ],
27
+ controllers: [],
28
+ providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
29
+ })
30
+ export class AppModule implements NestModule {
31
+ configure(consumer: MiddlewareConsumer) {
32
+ consumer.apply(CorrelationIdMiddleware).forRoutes('*');
33
+ }
34
+ }
@@ -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
+ }