@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.
- package/README.md +53 -0
- package/dist/cli.js +534 -0
- package/package.json +37 -0
- package/templates/mongo/.env.example +32 -0
- package/templates/mongo/Dockerfile +64 -0
- package/templates/mongo/docker-compose.yml +35 -0
- package/templates/mongo/eslint.config.mjs +35 -0
- package/templates/mongo/nest-cli.json +8 -0
- package/templates/mongo/package.json +105 -0
- package/templates/mongo/src/app.module.ts +59 -0
- package/templates/mongo/src/common/decorators/public-api.decorator.ts +9 -0
- package/templates/mongo/src/common/decorators/raw-response.decorator.ts +4 -0
- package/templates/mongo/src/common/filters/http-exception.filter.spec.ts +95 -0
- package/templates/mongo/src/common/filters/http-exception.filter.ts +43 -0
- package/templates/mongo/src/common/filters/rpc-exception.filter.ts +18 -0
- package/templates/mongo/src/common/index.ts +5 -0
- package/templates/mongo/src/common/interceptors/timeout.interceptor.ts +32 -0
- package/templates/mongo/src/common/interceptors/transform.interceptor.spec.ts +52 -0
- package/templates/mongo/src/common/interceptors/transform.interceptor.ts +25 -0
- package/templates/mongo/src/common/middleware/correlation-id.middleware.spec.ts +69 -0
- package/templates/mongo/src/common/middleware/correlation-id.middleware.ts +26 -0
- package/templates/mongo/src/infrastructure/cache/inject-redis.decorator.ts +4 -0
- package/templates/mongo/src/infrastructure/cache/redis.module.ts +9 -0
- package/templates/mongo/src/infrastructure/cache/redis.service.spec.ts +174 -0
- package/templates/mongo/src/infrastructure/cache/redis.service.ts +121 -0
- package/templates/mongo/src/infrastructure/config/config.module.ts +36 -0
- package/templates/mongo/src/infrastructure/config/environment.validation.spec.ts +100 -0
- package/templates/mongo/src/infrastructure/config/environment.validation.ts +21 -0
- package/templates/mongo/src/infrastructure/database/mongodb.module.ts +17 -0
- package/templates/mongo/src/infrastructure/health/health.controller.ts +46 -0
- package/templates/mongo/src/infrastructure/health/health.module.ts +12 -0
- package/templates/mongo/src/infrastructure/health/redis.health-indicator.ts +20 -0
- package/templates/mongo/src/instrumentation.spec.ts +24 -0
- package/templates/mongo/src/instrumentation.ts +44 -0
- package/templates/mongo/src/main.ts +102 -0
- package/templates/mongo/src/modules/example/application/commands/create-item.command.ts +3 -0
- package/templates/mongo/src/modules/example/application/commands/create-item.handler.spec.ts +49 -0
- package/templates/mongo/src/modules/example/application/commands/create-item.handler.ts +20 -0
- package/templates/mongo/src/modules/example/application/commands/delete-item.command.ts +3 -0
- package/templates/mongo/src/modules/example/application/commands/delete-item.handler.ts +15 -0
- package/templates/mongo/src/modules/example/application/dtos/create-item.dto.ts +9 -0
- package/templates/mongo/src/modules/example/application/dtos/item.response.dto.ts +9 -0
- package/templates/mongo/src/modules/example/application/queries/get-item.handler.spec.ts +49 -0
- package/templates/mongo/src/modules/example/application/queries/get-item.handler.ts +16 -0
- package/templates/mongo/src/modules/example/application/queries/get-item.query.ts +3 -0
- package/templates/mongo/src/modules/example/application/queries/list-items.handler.ts +16 -0
- package/templates/mongo/src/modules/example/application/queries/list-items.query.ts +3 -0
- package/templates/mongo/src/modules/example/domain/item-name.value-object.spec.ts +49 -0
- package/templates/mongo/src/modules/example/domain/item-name.value-object.ts +18 -0
- package/templates/mongo/src/modules/example/domain/item.entity.spec.ts +48 -0
- package/templates/mongo/src/modules/example/domain/item.entity.ts +19 -0
- package/templates/mongo/src/modules/example/domain/item.repository.interface.ts +10 -0
- package/templates/mongo/src/modules/example/example.module.ts +31 -0
- package/templates/mongo/src/modules/example/infrastructure/.gitkeep +0 -0
- package/templates/mongo/src/modules/example/infrastructure/persistence/mongoose-item.repository.ts +42 -0
- package/templates/mongo/src/modules/example/infrastructure/persistence/schemas/item.schema.ts +15 -0
- package/templates/mongo/src/modules/example/presenter/item.controller.ts +52 -0
- package/templates/mongo/src/shared/base/aggregate-root.spec.ts +44 -0
- package/templates/mongo/src/shared/base/aggregate-root.ts +20 -0
- package/templates/mongo/src/shared/base/domain-event.ts +6 -0
- package/templates/mongo/src/shared/base/entity.spec.ts +36 -0
- package/templates/mongo/src/shared/base/entity.ts +13 -0
- package/templates/mongo/src/shared/base/index.ts +5 -0
- package/templates/mongo/src/shared/base/repository.interface.ts +6 -0
- package/templates/mongo/src/shared/base/value-object.spec.ts +39 -0
- package/templates/mongo/src/shared/base/value-object.ts +13 -0
- package/templates/mongo/src/shared/dto/pagination.dto.spec.ts +49 -0
- package/templates/mongo/src/shared/dto/pagination.dto.ts +37 -0
- package/templates/mongo/src/shared/dto/response.dto.ts +13 -0
- package/templates/mongo/src/shared/exceptions/app.exception.spec.ts +59 -0
- package/templates/mongo/src/shared/exceptions/app.exception.ts +19 -0
- package/templates/mongo/src/shared/exceptions/error-codes.ts +9 -0
- package/templates/mongo/src/shared/index.ts +7 -0
- package/templates/mongo/src/shared/logger/logger.module.ts +12 -0
- package/templates/mongo/src/shared/logger/logger.service.ts +48 -0
- package/templates/mongo/src/shared/logger/pino.config.ts +86 -0
- package/templates/mongo/src/shared/validation-options.ts +38 -0
- package/templates/mongo/src/shared/valueobjects/date.valueobject.spec.ts +40 -0
- package/templates/mongo/src/shared/valueobjects/date.valueobject.ts +14 -0
- package/templates/mongo/src/shared/valueobjects/id.valueobject.spec.ts +28 -0
- package/templates/mongo/src/shared/valueobjects/id.valueobject.ts +14 -0
- package/templates/mongo/src/shared/valueobjects/index.ts +4 -0
- package/templates/mongo/src/shared/valueobjects/number.valueobject.spec.ts +48 -0
- package/templates/mongo/src/shared/valueobjects/number.valueobject.ts +14 -0
- package/templates/mongo/src/shared/valueobjects/string.valueobject.spec.ts +37 -0
- package/templates/mongo/src/shared/valueobjects/string.valueobject.ts +14 -0
- package/templates/mongo/tsconfig.build.json +4 -0
- package/templates/mongo/tsconfig.json +23 -0
- package/templates/postgres/.env.example +32 -0
- package/templates/postgres/Dockerfile +64 -0
- package/templates/postgres/eslint.config.mjs +35 -0
- package/templates/postgres/nest-cli.json +8 -0
- package/templates/postgres/package.json +103 -0
- package/templates/postgres/prisma/schema.prisma +14 -0
- package/templates/postgres/prisma.config.ts +11 -0
- package/templates/postgres/src/app.module.ts +34 -0
- package/templates/postgres/src/common/decorators/public-api.decorator.ts +9 -0
- package/templates/postgres/src/common/decorators/raw-response.decorator.ts +4 -0
- package/templates/postgres/src/common/filters/http-exception.filter.spec.ts +95 -0
- package/templates/postgres/src/common/filters/http-exception.filter.ts +43 -0
- package/templates/postgres/src/common/filters/rpc-exception.filter.ts +18 -0
- package/templates/postgres/src/common/index.ts +5 -0
- package/templates/postgres/src/common/interceptors/timeout.interceptor.ts +32 -0
- package/templates/postgres/src/common/interceptors/transform.interceptor.spec.ts +52 -0
- package/templates/postgres/src/common/interceptors/transform.interceptor.ts +25 -0
- package/templates/postgres/src/common/middleware/correlation-id.middleware.spec.ts +69 -0
- package/templates/postgres/src/common/middleware/correlation-id.middleware.ts +26 -0
- package/templates/postgres/src/infrastructure/cache/inject-redis.decorator.ts +4 -0
- package/templates/postgres/src/infrastructure/cache/redis.module.ts +9 -0
- package/templates/postgres/src/infrastructure/cache/redis.service.spec.ts +174 -0
- package/templates/postgres/src/infrastructure/cache/redis.service.ts +121 -0
- package/templates/postgres/src/infrastructure/config/config.module.ts +36 -0
- package/templates/postgres/src/infrastructure/config/environment.validation.spec.ts +100 -0
- package/templates/postgres/src/infrastructure/config/environment.validation.ts +21 -0
- package/templates/postgres/src/infrastructure/database/inject-prisma.decorator.ts +4 -0
- package/templates/postgres/src/infrastructure/database/prisma.module.ts +9 -0
- package/templates/postgres/src/infrastructure/database/prisma.service.ts +21 -0
- package/templates/postgres/src/infrastructure/health/health.controller.ts +46 -0
- package/templates/postgres/src/infrastructure/health/health.module.ts +12 -0
- package/templates/postgres/src/infrastructure/health/prisma.health-indicator.ts +19 -0
- package/templates/postgres/src/infrastructure/health/redis.health-indicator.ts +20 -0
- package/templates/postgres/src/instrumentation.spec.ts +24 -0
- package/templates/postgres/src/instrumentation.ts +44 -0
- package/templates/postgres/src/main.ts +102 -0
- package/templates/postgres/src/modules/example/application/commands/create-item.command.ts +3 -0
- package/templates/postgres/src/modules/example/application/commands/create-item.handler.spec.ts +49 -0
- package/templates/postgres/src/modules/example/application/commands/create-item.handler.ts +20 -0
- package/templates/postgres/src/modules/example/application/commands/delete-item.command.ts +3 -0
- package/templates/postgres/src/modules/example/application/commands/delete-item.handler.ts +15 -0
- package/templates/postgres/src/modules/example/application/dtos/create-item.dto.ts +9 -0
- package/templates/postgres/src/modules/example/application/dtos/item.response.dto.ts +9 -0
- package/templates/postgres/src/modules/example/application/queries/get-item.handler.spec.ts +49 -0
- package/templates/postgres/src/modules/example/application/queries/get-item.handler.ts +16 -0
- package/templates/postgres/src/modules/example/application/queries/get-item.query.ts +3 -0
- package/templates/postgres/src/modules/example/application/queries/list-items.handler.ts +16 -0
- package/templates/postgres/src/modules/example/application/queries/list-items.query.ts +3 -0
- package/templates/postgres/src/modules/example/domain/item-name.value-object.spec.ts +49 -0
- package/templates/postgres/src/modules/example/domain/item-name.value-object.ts +18 -0
- package/templates/postgres/src/modules/example/domain/item.entity.spec.ts +48 -0
- package/templates/postgres/src/modules/example/domain/item.entity.ts +19 -0
- package/templates/postgres/src/modules/example/domain/item.repository.interface.ts +10 -0
- package/templates/postgres/src/modules/example/example.module.ts +26 -0
- package/templates/postgres/src/modules/example/infrastructure/.gitkeep +0 -0
- package/templates/postgres/src/modules/example/infrastructure/persistence/prisma-item.repository.ts +34 -0
- package/templates/postgres/src/modules/example/presenter/item.controller.ts +52 -0
- package/templates/postgres/src/shared/base/aggregate-root.spec.ts +44 -0
- package/templates/postgres/src/shared/base/aggregate-root.ts +20 -0
- package/templates/postgres/src/shared/base/domain-event.ts +6 -0
- package/templates/postgres/src/shared/base/entity.spec.ts +36 -0
- package/templates/postgres/src/shared/base/entity.ts +13 -0
- package/templates/postgres/src/shared/base/index.ts +5 -0
- package/templates/postgres/src/shared/base/repository.interface.ts +6 -0
- package/templates/postgres/src/shared/base/value-object.spec.ts +39 -0
- package/templates/postgres/src/shared/base/value-object.ts +13 -0
- package/templates/postgres/src/shared/dto/pagination.dto.spec.ts +49 -0
- package/templates/postgres/src/shared/dto/pagination.dto.ts +37 -0
- package/templates/postgres/src/shared/dto/response.dto.ts +13 -0
- package/templates/postgres/src/shared/exceptions/app.exception.spec.ts +59 -0
- package/templates/postgres/src/shared/exceptions/app.exception.ts +19 -0
- package/templates/postgres/src/shared/exceptions/error-codes.ts +9 -0
- package/templates/postgres/src/shared/index.ts +7 -0
- package/templates/postgres/src/shared/logger/logger.module.ts +12 -0
- package/templates/postgres/src/shared/logger/logger.service.ts +48 -0
- package/templates/postgres/src/shared/logger/pino.config.ts +86 -0
- package/templates/postgres/src/shared/validation-options.ts +38 -0
- package/templates/postgres/src/shared/valueobjects/date.valueobject.spec.ts +40 -0
- package/templates/postgres/src/shared/valueobjects/date.valueobject.ts +14 -0
- package/templates/postgres/src/shared/valueobjects/id.valueobject.spec.ts +28 -0
- package/templates/postgres/src/shared/valueobjects/id.valueobject.ts +14 -0
- package/templates/postgres/src/shared/valueobjects/index.ts +4 -0
- package/templates/postgres/src/shared/valueobjects/number.valueobject.spec.ts +48 -0
- package/templates/postgres/src/shared/valueobjects/number.valueobject.ts +14 -0
- package/templates/postgres/src/shared/valueobjects/string.valueobject.spec.ts +37 -0
- package/templates/postgres/src/shared/valueobjects/string.valueobject.ts +14 -0
- package/templates/postgres/tsconfig.build.json +4 -0
- 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
|