@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
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @tndhuy/create-app
2
+
3
+ Scaffold a new NestJS DDD backend service from the template.
4
+
5
+ ## Usage
6
+
7
+ ```
8
+ npx @tndhuy/create-app@latest my-service
9
+ ```
10
+
11
+ ## What it does
12
+
13
+ 1. Prompts for service name (kebab-case)
14
+ 2. Asks for database: PostgreSQL (default) or MongoDB
15
+ 3. Asks for optional modules: Redis, OpenTelemetry, Kafka
16
+ 4. Copies the template, replaces placeholder names, wires selected modules
17
+ 5. Outputs a ready-to-run project directory
18
+
19
+ ## After scaffolding
20
+
21
+ ```
22
+ cd my-service
23
+ npm install
24
+ cp .env.example .env
25
+ npm run start:dev
26
+ ```
27
+
28
+ ## Setup (team members)
29
+
30
+ To install from GitHub Packages, add this to your `~/.npmrc`:
31
+
32
+ ```
33
+ @tndhuy:registry=https://npm.pkg.github.com
34
+ //npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
35
+ ```
36
+
37
+ Generate a token at https://github.com/settings/tokens with `read:packages` scope.
38
+
39
+ ## Publishing (maintainers)
40
+
41
+ ```
42
+ git tag create-app@0.0.2
43
+ git push origin create-app@0.0.2
44
+ ```
45
+
46
+ The GitHub Actions workflow handles build, test, and publish automatically.
47
+
48
+ ## Development
49
+
50
+ ```
51
+ npm test --workspace=packages/create-app # Run tests
52
+ npm run build --workspace=packages/create-app # Build CLI
53
+ ```
package/dist/cli.js ADDED
@@ -0,0 +1,534 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_prompts = require("@clack/prompts");
28
+ var import_path3 = require("path");
29
+
30
+ // src/scaffold.ts
31
+ var import_promises2 = require("fs/promises");
32
+ var import_path2 = require("path");
33
+
34
+ // src/replacements.ts
35
+ function toPascalCase(kebab) {
36
+ return kebab.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
37
+ }
38
+ function buildReplacements(serviceName) {
39
+ return [
40
+ ["nestjs-backend-template", serviceName],
41
+ ["NestjsBackendTemplate", toPascalCase(serviceName)]
42
+ ];
43
+ }
44
+ function validateServiceName(name) {
45
+ if (!name || name.length < 2) {
46
+ return "Service name must be at least 2 characters";
47
+ }
48
+ if (/[A-Z]/.test(name)) {
49
+ return "Service name must be lowercase (no uppercase letters)";
50
+ }
51
+ if (name.includes(" ")) {
52
+ return "Service name must not contain spaces";
53
+ }
54
+ if (name.includes("..") || name.includes("/") || name.includes("\\")) {
55
+ return "Service name must not contain path traversal characters (.. / or \\)";
56
+ }
57
+ if (name.startsWith("-") || name.endsWith("-")) {
58
+ return "Service name must not start or end with a hyphen";
59
+ }
60
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(name)) {
61
+ return "Service name must contain only lowercase letters, numbers, and hyphens";
62
+ }
63
+ return void 0;
64
+ }
65
+
66
+ // src/kafka-module.ts
67
+ var import_promises = require("fs/promises");
68
+ var import_path = require("path");
69
+ async function generateKafkaModule(destDir, serviceName) {
70
+ const kafkaDir = (0, import_path.join)(destDir, "src", "kafka");
71
+ await (0, import_promises.mkdir)(kafkaDir, { recursive: true });
72
+ const moduleContent = `import { Module } from '@nestjs/common';
73
+ import { ClientsModule, Transport } from '@nestjs/microservices';
74
+ import { ConfigService } from '@nestjs/config';
75
+ import { KafkaController } from './kafka.controller';
76
+
77
+ @Module({
78
+ imports: [
79
+ ClientsModule.registerAsync([{
80
+ name: 'KAFKA_SERVICE',
81
+ useFactory: (configService: ConfigService) => ({
82
+ transport: Transport.KAFKA,
83
+ options: {
84
+ client: {
85
+ brokers: [configService.get<string>('KAFKA_BROKER', 'localhost:9092')],
86
+ },
87
+ consumer: {
88
+ groupId: \`${serviceName}-consumer-group\`,
89
+ },
90
+ },
91
+ }),
92
+ inject: [ConfigService],
93
+ }]),
94
+ ],
95
+ controllers: [KafkaController],
96
+ exports: [ClientsModule],
97
+ })
98
+ export class KafkaModule {}
99
+ `;
100
+ await (0, import_promises.writeFile)((0, import_path.join)(kafkaDir, "kafka.module.ts"), moduleContent, "utf-8");
101
+ const controllerContent = `import { Controller } from '@nestjs/common';
102
+ import { MessagePattern, Payload } from '@nestjs/microservices';
103
+
104
+ @Controller()
105
+ export class KafkaController {
106
+ @MessagePattern('example-topic')
107
+ handleMessage(@Payload() message: unknown) {
108
+ // TODO: Implement your Kafka message handler
109
+ return message;
110
+ }
111
+ }
112
+ `;
113
+ await (0, import_promises.writeFile)(
114
+ (0, import_path.join)(kafkaDir, "kafka.controller.ts"),
115
+ controllerContent,
116
+ "utf-8"
117
+ );
118
+ const barrelContent = `export { KafkaModule } from './kafka.module';
119
+ export { KafkaController } from './kafka.controller';
120
+ `;
121
+ await (0, import_promises.writeFile)((0, import_path.join)(kafkaDir, "index.ts"), barrelContent, "utf-8");
122
+ }
123
+
124
+ // src/scaffold.ts
125
+ async function isBinaryFile(filePath) {
126
+ try {
127
+ const buffer = Buffer.alloc(512);
128
+ const handle = await import("fs/promises").then((m) => m.open(filePath, "r"));
129
+ try {
130
+ const { bytesRead } = await handle.read(buffer, 0, 512, 0);
131
+ await handle.close();
132
+ for (let i = 0; i < bytesRead; i++) {
133
+ if (buffer[i] === 0) {
134
+ return true;
135
+ }
136
+ }
137
+ return false;
138
+ } catch {
139
+ await handle.close();
140
+ return false;
141
+ }
142
+ } catch {
143
+ return false;
144
+ }
145
+ }
146
+ async function collectPaths(dir) {
147
+ const results = [];
148
+ const entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
149
+ for (const entry of entries) {
150
+ const fullPath = (0, import_path2.join)(dir, entry.name);
151
+ if (entry.isDirectory()) {
152
+ const nested = await collectPaths(fullPath);
153
+ results.push(...nested);
154
+ }
155
+ results.push(fullPath);
156
+ }
157
+ return results;
158
+ }
159
+ async function replaceFileContents(dir, replacements) {
160
+ const entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
161
+ for (const entry of entries) {
162
+ const fullPath = (0, import_path2.join)(dir, entry.name);
163
+ if (entry.isDirectory()) {
164
+ await replaceFileContents(fullPath, replacements);
165
+ } else {
166
+ const binary = await isBinaryFile(fullPath);
167
+ if (binary) continue;
168
+ try {
169
+ let content = await (0, import_promises2.readFile)(fullPath, "utf-8");
170
+ let modified = false;
171
+ for (const [from, to] of replacements) {
172
+ if (content.includes(from)) {
173
+ content = content.replaceAll(from, to);
174
+ modified = true;
175
+ }
176
+ }
177
+ if (modified) {
178
+ await (0, import_promises2.writeFile)(fullPath, content, "utf-8");
179
+ }
180
+ } catch {
181
+ }
182
+ }
183
+ }
184
+ }
185
+ async function renamePathsWithPlaceholders(dir, replacements) {
186
+ const allPaths = await collectPaths(dir);
187
+ allPaths.sort((a, b) => {
188
+ const depthA = a.split("/").length;
189
+ const depthB = b.split("/").length;
190
+ return depthB - depthA;
191
+ });
192
+ for (const oldPath of allPaths) {
193
+ const parent = (0, import_path2.dirname)(oldPath);
194
+ let newName = (0, import_path2.basename)(oldPath);
195
+ let changed = false;
196
+ for (const [from, to] of replacements) {
197
+ if (newName.includes(from)) {
198
+ newName = newName.replaceAll(from, to);
199
+ changed = true;
200
+ }
201
+ }
202
+ if (changed) {
203
+ const newPath = (0, import_path2.join)(parent, newName);
204
+ try {
205
+ await (0, import_promises2.rename)(oldPath, newPath);
206
+ } catch {
207
+ }
208
+ }
209
+ }
210
+ }
211
+ async function patchPackageJson(destDir, options) {
212
+ const pkgPath = (0, import_path2.join)(destDir, "package.json");
213
+ try {
214
+ const raw = await (0, import_promises2.readFile)(pkgPath, "utf-8");
215
+ const pkg = JSON.parse(raw);
216
+ pkg.name = options.serviceName;
217
+ if (options.db === "mongo") {
218
+ if (pkg.dependencies) {
219
+ delete pkg.dependencies["@prisma/client"];
220
+ delete pkg.dependencies["@prisma/adapter-pg"];
221
+ delete pkg.dependencies["pg"];
222
+ delete pkg.dependencies["@types/pg"];
223
+ pkg.dependencies["mongoose"] = "^8.0.0";
224
+ pkg.dependencies["@nestjs/mongoose"] = "^11.0.0";
225
+ }
226
+ if (pkg.devDependencies) {
227
+ delete pkg.devDependencies["prisma"];
228
+ }
229
+ }
230
+ if (options.modules.includes("kafka")) {
231
+ if (!pkg.dependencies) pkg.dependencies = {};
232
+ pkg.dependencies["@nestjs/microservices"] = "^11.1.18";
233
+ pkg.dependencies["kafkajs"] = "^2.2.4";
234
+ }
235
+ if (!options.modules.includes("redis")) {
236
+ if (pkg.dependencies) {
237
+ delete pkg.dependencies["ioredis"];
238
+ delete pkg.dependencies["@nestjs-modules/ioredis"];
239
+ }
240
+ }
241
+ if (!options.modules.includes("otel")) {
242
+ if (pkg.dependencies) {
243
+ for (const dep of Object.keys(pkg.dependencies)) {
244
+ if (dep.startsWith("@opentelemetry/")) {
245
+ delete pkg.dependencies[dep];
246
+ }
247
+ }
248
+ }
249
+ }
250
+ await (0, import_promises2.writeFile)(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
251
+ } catch (err) {
252
+ console.warn("Warning: Could not patch package.json:", err);
253
+ }
254
+ }
255
+ async function pathExists(p) {
256
+ try {
257
+ await (0, import_promises2.stat)(p);
258
+ return true;
259
+ } catch {
260
+ return false;
261
+ }
262
+ }
263
+ async function safeDeleteFile(destDir, filePath) {
264
+ const resolved = (0, import_path2.resolve)(filePath);
265
+ const resolvedDestDir = (0, import_path2.resolve)(destDir);
266
+ if (!resolved.startsWith(resolvedDestDir + "/") && resolved !== resolvedDestDir) {
267
+ throw new Error(`Security: path '${filePath}' is outside destDir '${destDir}'`);
268
+ }
269
+ try {
270
+ await (0, import_promises2.rm)(resolved, { force: true });
271
+ } catch {
272
+ }
273
+ }
274
+ async function safeDeleteDir(destDir, dirPath) {
275
+ const resolved = (0, import_path2.resolve)(dirPath);
276
+ const resolvedDestDir = (0, import_path2.resolve)(destDir);
277
+ if (!resolved.startsWith(resolvedDestDir + "/") && resolved !== resolvedDestDir) {
278
+ throw new Error(`Security: path '${dirPath}' is outside destDir '${destDir}'`);
279
+ }
280
+ try {
281
+ await (0, import_promises2.rm)(resolved, { recursive: true, force: true });
282
+ } catch {
283
+ }
284
+ }
285
+ async function removeMatchingLines(filePath, patterns) {
286
+ if (!await pathExists(filePath)) return;
287
+ try {
288
+ const content = await (0, import_promises2.readFile)(filePath, "utf-8");
289
+ let updated = content;
290
+ for (const pattern of patterns) {
291
+ updated = updated.replace(pattern, "");
292
+ }
293
+ updated = updated.replace(/\n{3,}/g, "\n\n");
294
+ if (updated !== content) {
295
+ await (0, import_promises2.writeFile)(filePath, updated, "utf-8");
296
+ }
297
+ } catch {
298
+ }
299
+ }
300
+ async function removeRedis(destDir) {
301
+ await safeDeleteDir(destDir, (0, import_path2.join)(destDir, "src", "infrastructure", "cache"));
302
+ const pkgPath = (0, import_path2.join)(destDir, "package.json");
303
+ if (await pathExists(pkgPath)) {
304
+ try {
305
+ const raw = await (0, import_promises2.readFile)(pkgPath, "utf-8");
306
+ const pkg = JSON.parse(raw);
307
+ if (pkg.dependencies) {
308
+ delete pkg.dependencies["ioredis"];
309
+ delete pkg.dependencies["@nestjs-modules/ioredis"];
310
+ }
311
+ await (0, import_promises2.writeFile)(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
312
+ } catch {
313
+ }
314
+ }
315
+ const appModulePath = (0, import_path2.join)(destDir, "src", "app.module.ts");
316
+ await removeMatchingLines(appModulePath, [
317
+ // Remove the import statement for CacheModule (from cache/redis.module)
318
+ /^import\s*\{[^}]*CacheModule[^}]*\}\s*from\s*['"][^'"]*cache[^'"]*['"];\n?/m,
319
+ // Remove CacheModule entry from the imports array (with optional trailing comma)
320
+ /^\s*CacheModule,?\n/m
321
+ ]);
322
+ const envExamplePath = (0, import_path2.join)(destDir, ".env.example");
323
+ await removeMatchingLines(envExamplePath, [
324
+ // Remove REDIS_URL line
325
+ /^REDIS_URL=.*\n?/m,
326
+ // Remove REDIS_HOST line
327
+ /^REDIS_HOST=.*\n?/m,
328
+ // Remove REDIS_PORT line
329
+ /^REDIS_PORT=.*\n?/m,
330
+ // Remove # Redis section header if it becomes orphaned
331
+ /^# Redis\n(?=\n)/m
332
+ ]);
333
+ }
334
+ async function removeOtel(destDir) {
335
+ await safeDeleteFile(destDir, (0, import_path2.join)(destDir, "src", "instrumentation.ts"));
336
+ await safeDeleteFile(
337
+ destDir,
338
+ (0, import_path2.join)(destDir, "src", "instrumentation.spec.ts")
339
+ );
340
+ const pkgPath = (0, import_path2.join)(destDir, "package.json");
341
+ if (await pathExists(pkgPath)) {
342
+ try {
343
+ const raw = await (0, import_promises2.readFile)(pkgPath, "utf-8");
344
+ const pkg = JSON.parse(raw);
345
+ if (pkg.dependencies) {
346
+ for (const dep of Object.keys(pkg.dependencies)) {
347
+ if (dep.startsWith("@opentelemetry/")) {
348
+ delete pkg.dependencies[dep];
349
+ }
350
+ }
351
+ }
352
+ await (0, import_promises2.writeFile)(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
353
+ } catch {
354
+ }
355
+ }
356
+ const envExamplePath = (0, import_path2.join)(destDir, ".env.example");
357
+ await removeMatchingLines(envExamplePath, [
358
+ /^OTEL_ENABLED=.*\n?/m,
359
+ /^OTEL_SERVICE_NAME=.*\n?/m,
360
+ /^OTEL_PROMETHEUS_PORT=.*\n?/m,
361
+ /^OTEL_EXPORTER_OTLP_ENDPOINT=.*\n?/m,
362
+ // Remove the # Observability - OpenTelemetry comment block if it becomes orphaned
363
+ /^# Observability - OpenTelemetry[^\n]*\n(?=\n|$)/m
364
+ ]);
365
+ const mainTsPath = (0, import_path2.join)(destDir, "src", "main.ts");
366
+ await removeMatchingLines(mainTsPath, [
367
+ // Remove: import otelSdk from './instrumentation';
368
+ /^import\s+\w+\s+from\s+['"][^'"]*instrumentation['"];\n?/m,
369
+ // Remove: otelSdk?.start(); line
370
+ /^\s*\w+Sdk\?\.start\(\);\n?/m
371
+ ]);
372
+ }
373
+ async function addKafka(destDir, serviceName) {
374
+ await generateKafkaModule(destDir, serviceName);
375
+ const pkgPath = (0, import_path2.join)(destDir, "package.json");
376
+ if (await pathExists(pkgPath)) {
377
+ try {
378
+ const raw = await (0, import_promises2.readFile)(pkgPath, "utf-8");
379
+ const pkg = JSON.parse(raw);
380
+ if (!pkg.dependencies) pkg.dependencies = {};
381
+ pkg.dependencies["@nestjs/microservices"] = "^11.1.18";
382
+ pkg.dependencies["kafkajs"] = "^2.2.4";
383
+ await (0, import_promises2.writeFile)(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
384
+ } catch {
385
+ }
386
+ }
387
+ const appModulePath = (0, import_path2.join)(destDir, "src", "app.module.ts");
388
+ if (await pathExists(appModulePath)) {
389
+ try {
390
+ let content = await (0, import_promises2.readFile)(appModulePath, "utf-8");
391
+ const kafkaImportLine = `import { KafkaModule } from './kafka/kafka.module';
392
+ `;
393
+ if (!content.includes(kafkaImportLine)) {
394
+ content = content.replace(
395
+ /^(@Module\()/m,
396
+ `${kafkaImportLine}
397
+ $1`
398
+ );
399
+ }
400
+ if (!content.includes("KafkaModule,") && !content.includes("KafkaModule\n")) {
401
+ content = content.replace(
402
+ /(imports:\s*\[[^\]]*?)(\s*\])/s,
403
+ (match, arrayContent, closing) => {
404
+ const trimmed = arrayContent.trimEnd();
405
+ const sep = trimmed.endsWith(",") ? "" : ",";
406
+ return `${trimmed}${sep}
407
+ KafkaModule,${closing}`;
408
+ }
409
+ );
410
+ }
411
+ await (0, import_promises2.writeFile)(appModulePath, content, "utf-8");
412
+ } catch {
413
+ }
414
+ }
415
+ const envExamplePath = (0, import_path2.join)(destDir, ".env.example");
416
+ if (await pathExists(envExamplePath)) {
417
+ try {
418
+ let content = await (0, import_promises2.readFile)(envExamplePath, "utf-8");
419
+ if (!content.includes("KAFKA_BROKER")) {
420
+ content = content.trimEnd() + "\n\n# Kafka\nKAFKA_BROKER=localhost:9092\n";
421
+ await (0, import_promises2.writeFile)(envExamplePath, content, "utf-8");
422
+ }
423
+ } catch {
424
+ }
425
+ }
426
+ }
427
+ async function scaffold(options) {
428
+ const templateDir = (0, import_path2.join)(__dirname, "..", "templates", options.db);
429
+ const { destDir, serviceName, modules } = options;
430
+ await (0, import_promises2.cp)(templateDir, destDir, { recursive: true });
431
+ const replacements = buildReplacements(serviceName);
432
+ await replaceFileContents(destDir, replacements);
433
+ await renamePathsWithPlaceholders(destDir, replacements);
434
+ await patchPackageJson(destDir, options);
435
+ if (!modules.includes("redis")) {
436
+ await removeRedis(destDir);
437
+ }
438
+ if (!modules.includes("otel")) {
439
+ await removeOtel(destDir);
440
+ }
441
+ if (modules.includes("kafka")) {
442
+ await addKafka(destDir, serviceName);
443
+ }
444
+ }
445
+
446
+ // src/cli.ts
447
+ function guardCancel(value) {
448
+ if ((0, import_prompts.isCancel)(value)) {
449
+ (0, import_prompts.cancel)("Operation cancelled.");
450
+ process.exit(0);
451
+ }
452
+ return value;
453
+ }
454
+ async function main() {
455
+ (0, import_prompts.intro)("create-app -- NestJS DDD scaffolder");
456
+ const argName = process.argv[2];
457
+ let serviceName;
458
+ if (argName && !validateServiceName(argName)) {
459
+ serviceName = argName;
460
+ console.log(` Service name : ${serviceName} (from arguments)`);
461
+ } else {
462
+ serviceName = guardCancel(
463
+ await (0, import_prompts.text)({
464
+ message: "Service name (kebab-case)",
465
+ placeholder: "my-service",
466
+ validate: (v) => validateServiceName(v)
467
+ })
468
+ );
469
+ }
470
+ const db = guardCancel(
471
+ await (0, import_prompts.select)({
472
+ message: "Select database",
473
+ options: [
474
+ { value: "postgres", label: "PostgreSQL", hint: "default" },
475
+ { value: "mongo", label: "MongoDB" }
476
+ ]
477
+ })
478
+ );
479
+ const modules = guardCancel(
480
+ await (0, import_prompts.multiselect)({
481
+ message: "Optional modules (space to toggle, enter to confirm)",
482
+ options: [
483
+ { value: "redis", label: "Redis", hint: "caching + circuit breaker" },
484
+ { value: "otel", label: "OpenTelemetry", hint: "traces + metrics" },
485
+ { value: "kafka", label: "Kafka", hint: "message broker boilerplate" }
486
+ ],
487
+ required: false
488
+ })
489
+ );
490
+ const selectedModules = modules.length > 0 ? modules.join(", ") : "none";
491
+ console.log("");
492
+ console.log(` Service name : ${serviceName}`);
493
+ console.log(` Database : ${db}`);
494
+ console.log(` Modules : ${selectedModules}`);
495
+ console.log("");
496
+ const proceed = guardCancel(
497
+ await (0, import_prompts.confirm)({
498
+ message: "Scaffold project with these settings?",
499
+ initialValue: true
500
+ })
501
+ );
502
+ if (!proceed) {
503
+ (0, import_prompts.cancel)("Scaffolding cancelled.");
504
+ process.exit(0);
505
+ }
506
+ const destDir = (0, import_path3.join)(process.cwd(), serviceName);
507
+ const s = (0, import_prompts.spinner)();
508
+ s.start("Scaffolding project...");
509
+ try {
510
+ await scaffold({
511
+ serviceName,
512
+ db,
513
+ modules,
514
+ destDir
515
+ });
516
+ s.stop("Project scaffolded!");
517
+ } catch (err) {
518
+ s.stop("Scaffolding failed.");
519
+ throw err;
520
+ }
521
+ (0, import_prompts.outro)(
522
+ `Next steps:
523
+
524
+ cd ${serviceName}
525
+ npm install
526
+ cp .env.example .env
527
+ npm run start:dev
528
+ `
529
+ );
530
+ }
531
+ main().catch((err) => {
532
+ (0, import_prompts.cancel)(err instanceof Error ? err.message : String(err));
533
+ process.exit(1);
534
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@tndhuy/create-app",
3
+ "version": "1.0.0",
4
+ "private": false,
5
+ "bin": {
6
+ "create-app": "dist/cli.js"
7
+ },
8
+ "files": [
9
+ "dist",
10
+ "templates"
11
+ ],
12
+ "publishConfig": {
13
+ "access": "public",
14
+ "registry": "https://registry.npmjs.org/"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/tndhuy/nestjs-backend-template.git"
19
+ },
20
+ "scripts": {
21
+ "build": "tsup",
22
+ "test": "jest",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "dependencies": {
26
+ "@clack/prompts": "^1.2.0",
27
+ "execa": "^9.6.1"
28
+ },
29
+ "devDependencies": {
30
+ "tsup": "^8.5.1",
31
+ "typescript": "^5.4.0",
32
+ "@types/node": "^20.0.0",
33
+ "jest": "^29.5.0",
34
+ "ts-jest": "^29.1.0",
35
+ "@types/jest": "^29.5.0"
36
+ }
37
+ }
@@ -0,0 +1,32 @@
1
+ # Application
2
+ PORT=3000
3
+ NODE_ENV=development
4
+
5
+ # Database
6
+ DATABASE_URL=mongodb://localhost:27017/template_db
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