@tndhuy/create-app 1.1.4 → 1.2.2

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/dist/cli.js CHANGED
@@ -40,6 +40,7 @@ function toPascalCase(kebab) {
40
40
  function buildReplacements(serviceName) {
41
41
  return [
42
42
  ["nestjs-backend-template", serviceName],
43
+ ["{{SERVICE_NAME_KEBAB}}", serviceName],
43
44
  ["NestjsBackendTemplate", toPascalCase(serviceName)]
44
45
  ];
45
46
  }
@@ -158,18 +159,48 @@ async function collectPaths(dir) {
158
159
  }
159
160
  return results;
160
161
  }
161
- async function replaceFileContents(dir, replacements) {
162
+ async function replaceFileContents(dir, replacements, options) {
162
163
  const entries = await (0, import_promises2.readdir)(dir, { withFileTypes: true });
163
164
  for (const entry of entries) {
164
165
  const fullPath = (0, import_path2.join)(dir, entry.name);
165
166
  if (entry.isDirectory()) {
166
- await replaceFileContents(fullPath, replacements);
167
+ await replaceFileContents(fullPath, replacements, options);
167
168
  } else {
168
169
  const binary = await isBinaryFile(fullPath);
169
170
  if (binary) continue;
170
171
  try {
171
172
  let content = await (0, import_promises2.readFile)(fullPath, "utf-8");
172
173
  let modified = false;
174
+ const isPrisma = options.orm === "prisma";
175
+ const isMongoose = options.orm === "mongoose";
176
+ const hasRedis = options.modules.includes("redis");
177
+ const hasOtel = options.modules.includes("otel");
178
+ const hasKafka = options.modules.includes("kafka");
179
+ const isPostgres = options.db === "postgres";
180
+ const isMongo = options.db === "mongo";
181
+ const checkBlock = (content2, flag, tag) => {
182
+ const startTag = `{{#IF_${tag}}}`;
183
+ const endTag = `{{/IF_${tag}}}`;
184
+ if (content2.includes(startTag)) {
185
+ if (flag) {
186
+ return content2.replaceAll(startTag, "").replaceAll(endTag, "");
187
+ } else {
188
+ const escapedStart = startTag.replace(/\{/g, "\\{").replace(/\}/g, "\\}");
189
+ const escapedEnd = endTag.replace(/\{/g, "\\{").replace(/\}/g, "\\}");
190
+ const regex = new RegExp(`${escapedStart}[\\s\\S]*?${escapedEnd}`, "g");
191
+ return content2.replace(regex, "");
192
+ }
193
+ }
194
+ return content2;
195
+ };
196
+ content = checkBlock(content, isPrisma, "PRISMA");
197
+ content = checkBlock(content, isMongoose, "MONGOOSE");
198
+ content = checkBlock(content, hasRedis, "REDIS");
199
+ content = checkBlock(content, hasOtel, "OTEL");
200
+ content = checkBlock(content, hasKafka, "KAFKA");
201
+ content = checkBlock(content, isPostgres, "POSTGRES");
202
+ content = checkBlock(content, isMongo, "MONGO");
203
+ modified = content !== await (0, import_promises2.readFile)(fullPath, "utf-8");
173
204
  for (const [from, to] of replacements) {
174
205
  if (content.includes(from)) {
175
206
  content = content.replaceAll(from, to);
@@ -216,8 +247,21 @@ async function patchPackageJson(destDir, options) {
216
247
  const raw = await (0, import_promises2.readFile)(pkgPath, "utf-8");
217
248
  const pkg = JSON.parse(raw);
218
249
  pkg.name = options.serviceName;
250
+ delete pkg.workspaces;
219
251
  const PRISMA_LATEST = "^7.5.0";
220
252
  const PRISMA_MONGO_COMPAT = "^6.0.0";
253
+ if (options.orm === "prisma") {
254
+ if (!pkg.scripts) pkg.scripts = {};
255
+ pkg.scripts["db:generate"] = "prisma generate";
256
+ pkg.scripts["db:push"] = "prisma db push";
257
+ pkg.scripts["db:pull"] = "prisma db pull";
258
+ pkg.scripts["db:studio"] = "prisma studio";
259
+ pkg.scripts["db:format"] = "prisma format";
260
+ if (options.db === "postgres") {
261
+ pkg.scripts["db:migrate:dev"] = "prisma migrate dev";
262
+ pkg.scripts["db:migrate:deploy"] = "prisma migrate deploy";
263
+ }
264
+ }
221
265
  if (options.db === "mongo") {
222
266
  if (options.orm === "prisma") {
223
267
  if (pkg.dependencies) {
@@ -393,8 +437,31 @@ async function removeOtel(destDir) {
393
437
  // Remove: import otelSdk from './instrumentation';
394
438
  /^import\s+\w+\s+from\s+['"][^'"]*instrumentation['"];\n?/m,
395
439
  // Remove: otelSdk?.start(); line
396
- /^\s*\w+Sdk\?\.start\(\);\n?/m
440
+ /^\s*\w+Sdk\?\.start\(\);\n?/m,
441
+ // Remove shutdown logic: void otelSdk?.shutdown()...
442
+ /^\s*void\s+otelSdk\?\.shutdown\(\)\.catch\(\(\)\s*=>\s*undefined\);\n?/m
397
443
  ]);
444
+ const pinoConfigPath = (0, import_path2.join)(destDir, "src", "shared", "logger", "pino.config.ts");
445
+ if (await pathExists(pinoConfigPath)) {
446
+ await removeMatchingLines(pinoConfigPath, [
447
+ // Remove OTel imports
448
+ /^import\s*\{[^}]*trace[^}]*\}\s*from\s*['"]@opentelemetry\/api['"];\n?/m,
449
+ // Remove mixin block (more robust regex for multiline)
450
+ /^\s*mixin\(\)\s*\{[\s\S]*?\n\s*\},\n/m
451
+ ]);
452
+ }
453
+ const redisServicePath = (0, import_path2.join)(destDir, "src", "infrastructure", "cache", "redis.service.ts");
454
+ if (await pathExists(redisServicePath)) {
455
+ await removeMatchingLines(redisServicePath, [
456
+ /^import\s*\{[^}]*trace[^}]*\}\s*from\s*['"]@opentelemetry\/api['"];\n?/m,
457
+ // Remove tracing spans from methods
458
+ /^\s*const\s+span\s*=\s*trace\.getTracer[\s\S]*?span\.end\(\);\n/gm,
459
+ // Fallback: remove any remaining span.end() or span related lines
460
+ /^\s*span\.end\(\);\n/gm,
461
+ /^\s*const\s+span\s*=\s*trace\.getTracer.*\n/gm
462
+ ]);
463
+ }
464
+ await safeDeleteFile(destDir, (0, import_path2.join)(destDir, "prometheus.yml"));
398
465
  }
399
466
  async function addKafka(destDir, serviceName) {
400
467
  await generateKafkaModule(destDir, serviceName);
@@ -464,7 +531,7 @@ async function scaffold(options) {
464
531
  }
465
532
  await (0, import_promises2.cp)(templateDir, destDir, { recursive: true });
466
533
  const replacements = buildReplacements(serviceName);
467
- await replaceFileContents(destDir, replacements);
534
+ await replaceFileContents(destDir, replacements, options);
468
535
  await renamePathsWithPlaceholders(destDir, replacements);
469
536
  await patchPackageJson(destDir, options);
470
537
  if (options.db === "mongo" && options.orm === "prisma") {
@@ -535,6 +602,7 @@ async function main() {
535
602
  };
536
603
  if (positionalName && !validateServiceName(positionalName)) {
537
604
  config.serviceName = positionalName;
605
+ (0, import_prompts.note)(`Using service name: ${config.serviceName} (from arguments)`, "Info");
538
606
  }
539
607
  let step = config.serviceName ? 1 : 0;
540
608
  const totalSteps = 4;
@@ -711,6 +779,16 @@ Modules : ${selectedModules}`,
711
779
  try {
712
780
  await (0, import_execa.execa)(pkgManager, ["install"], { cwd: destDir, stdio: "inherit" });
713
781
  is.stop("Dependencies installed successfully.");
782
+ if (config.orm === "prisma") {
783
+ const ps = (0, import_prompts.spinner)();
784
+ ps.start("Generating Prisma Client...");
785
+ try {
786
+ await (0, import_execa.execa)(pkgManager, ["run", "db:generate"], { cwd: destDir });
787
+ ps.stop("Prisma Client generated successfully.");
788
+ } catch (err) {
789
+ ps.stop("Prisma Client generation failed. You may need to run it manually.");
790
+ }
791
+ }
714
792
  } catch (err) {
715
793
  is.stop("Dependency installation failed.");
716
794
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tndhuy/create-app",
3
- "version": "1.1.4",
3
+ "version": "1.2.2",
4
4
  "private": false,
5
5
  "bin": {
6
6
  "create-app": "dist/cli.js"
@@ -0,0 +1,128 @@
1
+ # nestjs-backend-template
2
+
3
+ ## Overview
4
+
5
+ - Production-ready NestJS 11 template with Domain-Driven Design (DDD) architecture — four layers (Domain, Application, Infrastructure, Presenter) with CQRS via `@nestjs/cqrs`
6
+ - Pre-configured infrastructure: Prisma ORM, ioredis with circuit breaker, OpenTelemetry tracing and Prometheus metrics, Scalar API docs at `/docs`
7
+ - Agent-ready: Claude Code (`.claude/`) and Antigravity (`.agent/`) configs pre-wired with codebase context files and GSD skill
8
+
9
+ ## Quick Start
10
+
11
+ 1. **Clone the repository**
12
+
13
+ ```bash
14
+ git clone <repo-url> my-service && cd my-service
15
+ ```
16
+
17
+ 2. **Install dependencies**
18
+
19
+ ```bash
20
+ pnpm install
21
+ ```
22
+
23
+ 3. **Configure environment**
24
+
25
+ ```bash
26
+ cp .env.example .env
27
+ ```
28
+
29
+ Edit `.env` and set at minimum:
30
+ - `DATABASE_URL` — MongoDB connection string
31
+ - `REDIS_URL` — Redis connection string
32
+
33
+ 4. **Start infrastructure**
34
+
35
+ ```bash
36
+ docker compose up -d
37
+ ```
38
+
39
+ Starts MongoDB and Redis locally.
40
+
41
+ 5. **Start the server**
42
+
43
+ ```bash
44
+ pnpm dev
45
+ ```
46
+
47
+ Server starts at http://localhost:3000. API docs available at http://localhost:3000/docs (login: `admin` / `admin`).
48
+
49
+ ## Available Scripts
50
+
51
+ | Script | Description |
52
+ |--------|-------------|
53
+ | `pnpm start:dev` | Start in watch mode (development) |
54
+ | `pnpm build` | Compile TypeScript to `dist/` |
55
+ | `pnpm start:prod` | Run compiled build (`dist/src/main`) |
56
+ | `pnpm test` | Run unit tests with Jest |
57
+ | `pnpm test:cov` | Run tests with coverage report |
58
+ | `pnpm test:e2e` | Run end-to-end tests |
59
+ | `pnpm lint` | Run ESLint with auto-fix |
60
+ | `pnpm format` | Run Prettier on `src/` and `test/` |
61
+ | `pnpm db:generate` | Generate Prisma Client from schema |
62
+ | `pnpm db:sync` | Sync Prisma schema to MongoDB (`prisma db push`) |
63
+ | `pnpm db:sync:force` | Force sync schema with destructive changes (`--accept-data-loss`) |
64
+ | `pnpm db:push` | Push schema changes directly to database (no migration files) |
65
+ | `pnpm db:pull` | Pull database schema into `prisma/schema/` |
66
+ | `pnpm db:validate` | Validate Prisma schema and datasource config |
67
+ | `pnpm db:format` | Format Prisma schema files under `prisma/schema/` |
68
+ | `pnpm db:studio` | Open Prisma Studio for data browsing/editing |
69
+
70
+ ## MongoDB Schema Workflow
71
+
72
+ Prisma migrations are not used for MongoDB in this template.
73
+
74
+ 1. Edit schema files in `prisma/schema/`
75
+ 2. Run `pnpm db:format`
76
+ 3. Run `pnpm db:validate`
77
+ 4. Run `pnpm db:sync` (or `pnpm db:sync:force` when explicitly needed)
78
+ 5. Run `pnpm db:generate`
79
+
80
+ ## Environment Variables
81
+
82
+ | Variable | Required | Description | Default |
83
+ |----------|----------|-------------|---------|
84
+ | `PORT` | No | HTTP server port | `3000` |
85
+ | `NODE_ENV` | No | Environment (`development` / `production`) | `development` |
86
+ | `DATABASE_URL` | Yes | MongoDB connection string | — |
87
+ | `REDIS_URL` | Yes | Redis connection string | — |
88
+ | `API_VERSION` | No | API version for URI prefix | `1` |
89
+ | `APP_NAME` | No | Application name shown in Scalar docs | `NestJS Backend Template` |
90
+ | `APP_DESCRIPTION` | No | API description shown in Scalar docs | `API Documentation` |
91
+ | `REQUEST_TIMEOUT` | No | Request timeout in milliseconds | `30000` |
92
+ | `THROTTLE_TTL` | No | Rate limit window in seconds | `60` |
93
+ | `THROTTLE_LIMIT` | No | Max requests per window | `100` |
94
+ | `DOCS_USER` | No | Basic auth username for `/docs` | `admin` |
95
+ | `DOCS_PASS` | No | Basic auth password for `/docs` | `admin` |
96
+ | `OTEL_ENABLED` | No | Enable OpenTelemetry tracing and metrics | `false` |
97
+ | `OTEL_SERVICE_NAME` | No | Service name reported to OTel collector | `nestjs-backend-template` |
98
+ | `OTEL_PROMETHEUS_PORT` | No | Port for Prometheus metrics scrape endpoint | `9464` |
99
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | No | OTLP HTTP endpoint for trace export | `http://localhost:4318` |
100
+ | `LARK_APP_ID` | No | Lark app ID for MCP integration (agent use) | — |
101
+ | `LARK_APP_SECRET` | No | Lark app secret for MCP integration (agent use) | — |
102
+
103
+ ## Architecture
104
+
105
+ This template follows Domain-Driven Design (DDD) with four layers: Domain, Application, Infrastructure, and Presenter. Each domain module is fully self-contained under `src/<module>/` and communicates through the CQRS bus.
106
+
107
+ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full guide including layer rules, data flow, and the `ExampleModule` walkthrough.
108
+
109
+ ## Adding a New Module
110
+
111
+ Each new domain module follows the same DDD structure as `src/example/`. Follow the step-by-step walkthrough in [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md).
112
+
113
+ ## Tech Stack
114
+
115
+ - **NestJS** 11.0.1 — framework (Express 5 platform)
116
+ - **Prisma** 7.5.0 — MongoDB ORM with schema sync (`db push`)
117
+ - **ioredis** 5.10.1 — Redis client with cockatiel circuit breaker
118
+ - **Pino** / nestjs-pino — structured JSON logging
119
+ - **OpenTelemetry** SDK — distributed tracing (OTLP) and Prometheus metrics
120
+ - **Scalar** (@scalar/nestjs-api-reference) — interactive API docs
121
+ - **class-validator** / **class-transformer** — DTO validation and transformation
122
+ - **@nestjs/cqrs** 11.0.3 — command and query bus
123
+ - **@nestjs/throttler** 6.5.0 — global rate limiting
124
+ - **TypeScript** 5.7.3
125
+
126
+ ## License
127
+
128
+ UNLICENSED
@@ -1,7 +1,28 @@
1
1
  services:
2
+ {{#IF_POSTGRES}}
3
+ postgres:
4
+ image: postgres:16-alpine
5
+ container_name: {{SERVICE_NAME_KEBAB}}-postgres
6
+ restart: unless-stopped
7
+ ports:
8
+ - "5432:5432"
9
+ environment:
10
+ POSTGRES_USER: user
11
+ POSTGRES_PASSWORD: password
12
+ POSTGRES_DB: template_db
13
+ volumes:
14
+ - postgres_data:/var/lib/postgresql/data
15
+ healthcheck:
16
+ test: ["CMD-SHELL", "pg_isready -U user -d template_db"]
17
+ interval: 10s
18
+ timeout: 5s
19
+ retries: 5
20
+ {{/IF_POSTGRES}}
21
+
22
+ {{#IF_MONGO}}
2
23
  mongodb:
3
24
  image: mongo:latest
4
- container_name: nestjs-template-mongodb
25
+ container_name: {{SERVICE_NAME_KEBAB}}-mongodb
5
26
  restart: unless-stopped
6
27
  ports:
7
28
  - "27017:27017"
@@ -13,11 +34,13 @@ services:
13
34
  test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
14
35
  interval: 10s
15
36
  timeout: 5s
16
- retries: 10
37
+ retries: 5
38
+ {{/IF_MONGO}}
17
39
 
40
+ {{#IF_REDIS}}
18
41
  redis:
19
42
  image: redis:latest
20
- container_name: nestjs-template-redis
43
+ container_name: {{SERVICE_NAME_KEBAB}}-redis
21
44
  restart: unless-stopped
22
45
  ports:
23
46
  - "6379:6379"
@@ -28,8 +51,57 @@ services:
28
51
  test: ["CMD", "redis-cli", "ping"]
29
52
  interval: 10s
30
53
  timeout: 5s
31
- retries: 10
54
+ retries: 5
55
+ {{/IF_REDIS}}
56
+
57
+ {{#IF_KAFKA}}
58
+ zookeeper:
59
+ image: bitnami/zookeeper:latest
60
+ container_name: {{SERVICE_NAME_KEBAB}}-zookeeper
61
+ ports:
62
+ - "2181:2181"
63
+ environment:
64
+ - ALLOW_ANONYMOUS_LOGIN=yes
65
+
66
+ kafka:
67
+ image: bitnami/kafka:latest
68
+ container_name: {{SERVICE_NAME_KEBAB}}-kafka
69
+ ports:
70
+ - "9092:9092"
71
+ environment:
72
+ - KAFKA_BROKER_ID=1
73
+ - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
74
+ - ALLOW_PLAINTEXT_LISTENER=yes
75
+ - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092
76
+ depends_on:
77
+ - zookeeper
78
+ {{/IF_KAFKA}}
79
+
80
+ {{#IF_OTEL}}
81
+ jaeger:
82
+ image: jaegertracing/all-in-one:latest
83
+ container_name: {{SERVICE_NAME_KEBAB}}-jaeger
84
+ ports:
85
+ - "16686:16686"
86
+ - "4317:4317"
87
+ - "4318:4318"
88
+
89
+ prometheus:
90
+ image: prom/prometheus:latest
91
+ container_name: {{SERVICE_NAME_KEBAB}}-prometheus
92
+ ports:
93
+ - "9090:9090"
94
+ volumes:
95
+ - ./prometheus.yml:/etc/prometheus/prometheus.yml
96
+ {{/IF_OTEL}}
32
97
 
33
98
  volumes:
99
+ {{#IF_POSTGRES}}
100
+ postgres_data:
101
+ {{/IF_POSTGRES}}
102
+ {{#IF_MONGO}}
34
103
  mongodb_data:
104
+ {{/IF_MONGO}}
105
+ {{#IF_REDIS}}
35
106
  redis_data:
107
+ {{/IF_REDIS}}
@@ -0,0 +1,7 @@
1
+ global:
2
+ scrape_interval: 15s
3
+
4
+ scrape_configs:
5
+ - job_name: 'nestjs-service'
6
+ static_configs:
7
+ - targets: ['host.docker.internal:9464']
@@ -4,11 +4,22 @@ import { Observable } from 'rxjs';
4
4
  import { map } from 'rxjs/operators';
5
5
  import { PUBLIC_API_KEY } from '../decorators/public-api.decorator';
6
6
 
7
+ export const RAW_RESPONSE_KEY = 'raw_response';
8
+
7
9
  @Injectable()
8
10
  export class TransformInterceptor<T> implements NestInterceptor<T, unknown> {
9
11
  constructor(private readonly reflector: Reflector) {}
10
12
 
11
13
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
14
+ const isRaw = this.reflector.getAllAndOverride<boolean>(RAW_RESPONSE_KEY, [
15
+ context.getHandler(),
16
+ context.getClass(),
17
+ ]);
18
+
19
+ if (isRaw) {
20
+ return next.handle();
21
+ }
22
+
12
23
  const isPublic = this.reflector.getAllAndOverride<boolean>(PUBLIC_API_KEY, [
13
24
  context.getHandler(),
14
25
  context.getClass(),
@@ -87,7 +87,11 @@ async function bootstrap() {
87
87
  process.on('SIGTERM', () => {
88
88
  loggerService.log('SIGTERM signal received: closing HTTP server');
89
89
  setTimeout(() => {
90
- void otelSdk?.shutdown().catch(() => undefined);
90
+ if (process.env.OTEL_ENABLED === 'true') {
91
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
92
+ const otelSdk = require('./instrumentation').default;
93
+ void otelSdk?.shutdown().catch(() => undefined);
94
+ }
91
95
  httpServer.close(() => {
92
96
  loggerService.log('HTTP server closed');
93
97
  process.exit(0);
@@ -0,0 +1,76 @@
1
+ # {{SERVICE_NAME_KEBAB}}
2
+
3
+ Production-ready NestJS 11 backend service based on DDD architecture.
4
+
5
+ ## Architecture Overview
6
+
7
+ This project follows **Domain-Driven Design (DDD)** principles with four distinct layers:
8
+
9
+ 1. **Domain:** Core logic, Entities, Value Objects, and Repository interfaces. (No framework dependencies)
10
+ 2. **Application:** Command/Query handlers (CQRS), DTOs, and application services.
11
+ 3. **Infrastructure:** Database implementations (Prisma/Mongoose), Redis cache, and external integrations.
12
+ 4. **Presenter:** Controllers (HTTP/RPC), Interceptors, and Filters.
13
+
14
+ ## Features
15
+
16
+ - **Pino Logging:** High-performance logging with request context tracking.
17
+ - **Request ID:** Every request is assigned a unique `X-Request-Id` header for traceability.
18
+ - **OpenTelemetry:** Distributed tracing and Prometheus metrics (if enabled).
19
+ - **Circuit Breaker:** Resilience for Redis and external calls using Cockatiel.
20
+ - **API Reference:** Interactive Scalar documentation available at `/docs`.
21
+
22
+ ## Quick Start
23
+
24
+ ### 1. Configure Environment
25
+
26
+ ```bash
27
+ cp .env.example .env
28
+ ```
29
+
30
+ ### 2. Infrastructure
31
+
32
+ Start database and cache using Docker:
33
+
34
+ ```bash
35
+ docker compose up -d
36
+ ```
37
+
38
+ ### 3. Database Setup
39
+
40
+ {{#IF_PRISMA}}
41
+ ```bash
42
+ # Generate Prisma Client
43
+ npm run db:generate
44
+
45
+ # Apply migrations
46
+ npm run db:migrate:deploy
47
+ ```
48
+ {{/IF_PRISMA}}
49
+ {{#IF_MONGOOSE}}
50
+ Ensure your `MONGODB_URL` is correct in `.env`.
51
+ {{/IF_MONGOOSE}}
52
+
53
+ ### 4. Running the App
54
+
55
+ ```bash
56
+ # Development
57
+ npm run start:dev
58
+
59
+ # Production build
60
+ npm run build
61
+ npm run start:prod
62
+ ```
63
+
64
+ ## Logging & Observability
65
+
66
+ - **Console:** Pretty-printed logs in development mode.
67
+ - **Files:** Logs are automatically rotated and stored in the `logs/` directory.
68
+ - **Correlation:** Search for `requestId` in logs to trace all logs for a specific user request.
69
+
70
+ ## Available Scripts
71
+
72
+ - `npm run build`: Compile the project.
73
+ - `npm run start:dev`: Start in watch mode.
74
+ - `npm run test`: Run unit tests.
75
+ - `npm run test:e2e`: Run end-to-end tests.
76
+ - `npm run lint`: Fix code style issues.
@@ -0,0 +1,107 @@
1
+ services:
2
+ {{#IF_POSTGRES}}
3
+ postgres:
4
+ image: postgres:16-alpine
5
+ container_name: {{SERVICE_NAME_KEBAB}}-postgres
6
+ restart: unless-stopped
7
+ ports:
8
+ - "5432:5432"
9
+ environment:
10
+ POSTGRES_USER: user
11
+ POSTGRES_PASSWORD: password
12
+ POSTGRES_DB: template_db
13
+ volumes:
14
+ - postgres_data:/var/lib/postgresql/data
15
+ healthcheck:
16
+ test: ["CMD-SHELL", "pg_isready -U user -d template_db"]
17
+ interval: 10s
18
+ timeout: 5s
19
+ retries: 5
20
+ {{/IF_POSTGRES}}
21
+
22
+ {{#IF_MONGO}}
23
+ mongodb:
24
+ image: mongo:latest
25
+ container_name: {{SERVICE_NAME_KEBAB}}-mongodb
26
+ restart: unless-stopped
27
+ ports:
28
+ - "27017:27017"
29
+ environment:
30
+ MONGO_INITDB_DATABASE: template_db
31
+ volumes:
32
+ - mongodb_data:/data/db
33
+ healthcheck:
34
+ test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
35
+ interval: 10s
36
+ timeout: 5s
37
+ retries: 5
38
+ {{/IF_MONGO}}
39
+
40
+ {{#IF_REDIS}}
41
+ redis:
42
+ image: redis:latest
43
+ container_name: {{SERVICE_NAME_KEBAB}}-redis
44
+ restart: unless-stopped
45
+ ports:
46
+ - "6379:6379"
47
+ command: ["redis-server", "--appendonly", "yes"]
48
+ volumes:
49
+ - redis_data:/data
50
+ healthcheck:
51
+ test: ["CMD", "redis-cli", "ping"]
52
+ interval: 10s
53
+ timeout: 5s
54
+ retries: 5
55
+ {{/IF_REDIS}}
56
+
57
+ {{#IF_KAFKA}}
58
+ zookeeper:
59
+ image: bitnami/zookeeper:latest
60
+ container_name: {{SERVICE_NAME_KEBAB}}-zookeeper
61
+ ports:
62
+ - "2181:2181"
63
+ environment:
64
+ - ALLOW_ANONYMOUS_LOGIN=yes
65
+
66
+ kafka:
67
+ image: bitnami/kafka:latest
68
+ container_name: {{SERVICE_NAME_KEBAB}}-kafka
69
+ ports:
70
+ - "9092:9092"
71
+ environment:
72
+ - KAFKA_BROKER_ID=1
73
+ - KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
74
+ - ALLOW_PLAINTEXT_LISTENER=yes
75
+ - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092
76
+ depends_on:
77
+ - zookeeper
78
+ {{/IF_KAFKA}}
79
+
80
+ {{#IF_OTEL}}
81
+ jaeger:
82
+ image: jaegertracing/all-in-one:latest
83
+ container_name: {{SERVICE_NAME_KEBAB}}-jaeger
84
+ ports:
85
+ - "16686:16686"
86
+ - "4317:4317"
87
+ - "4318:4318"
88
+
89
+ prometheus:
90
+ image: prom/prometheus:latest
91
+ container_name: {{SERVICE_NAME_KEBAB}}-prometheus
92
+ ports:
93
+ - "9090:9090"
94
+ volumes:
95
+ - ./prometheus.yml:/etc/prometheus/prometheus.yml
96
+ {{/IF_OTEL}}
97
+
98
+ volumes:
99
+ {{#IF_POSTGRES}}
100
+ postgres_data:
101
+ {{/IF_POSTGRES}}
102
+ {{#IF_MONGO}}
103
+ mongodb_data:
104
+ {{/IF_MONGO}}
105
+ {{#IF_REDIS}}
106
+ redis_data:
107
+ {{/IF_REDIS}}
@@ -4,10 +4,7 @@
4
4
  "description": "",
5
5
  "author": "",
6
6
  "private": true,
7
- "license": "UNLICENSED",
8
- "workspaces": [
9
- "packages/*"
10
- ],
7
+ "license": "MIT",
11
8
  "scripts": {
12
9
  "build": "nest build",
13
10
  "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
@@ -63,7 +60,6 @@
63
60
  "@nestjs/schematics": "^11.0.0",
64
61
  "@nestjs/testing": "^11.0.1",
65
62
  "@types/express": "^5.0.0",
66
- "@types/ioredis": "^5.0.0",
67
63
  "@types/jest": "^30.0.0",
68
64
  "@types/node": "^22.10.7",
69
65
  "@types/supertest": "^6.0.2",
@@ -0,0 +1,7 @@
1
+ global:
2
+ scrape_interval: 15s
3
+
4
+ scrape_configs:
5
+ - job_name: 'nestjs-service'
6
+ static_configs:
7
+ - targets: ['host.docker.internal:9464']
@@ -4,11 +4,22 @@ import { Observable } from 'rxjs';
4
4
  import { map } from 'rxjs/operators';
5
5
  import { PUBLIC_API_KEY } from '../decorators/public-api.decorator';
6
6
 
7
+ export const RAW_RESPONSE_KEY = 'raw_response';
8
+
7
9
  @Injectable()
8
10
  export class TransformInterceptor<T> implements NestInterceptor<T, unknown> {
9
11
  constructor(private readonly reflector: Reflector) {}
10
12
 
11
13
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
14
+ const isRaw = this.reflector.getAllAndOverride<boolean>(RAW_RESPONSE_KEY, [
15
+ context.getHandler(),
16
+ context.getClass(),
17
+ ]);
18
+
19
+ if (isRaw) {
20
+ return next.handle();
21
+ }
22
+
12
23
  const isPublic = this.reflector.getAllAndOverride<boolean>(PUBLIC_API_KEY, [
13
24
  context.getHandler(),
14
25
  context.getClass(),
@@ -87,7 +87,11 @@ async function bootstrap() {
87
87
  process.on('SIGTERM', () => {
88
88
  loggerService.log('SIGTERM signal received: closing HTTP server');
89
89
  setTimeout(() => {
90
- void otelSdk?.shutdown().catch(() => undefined);
90
+ if (process.env.OTEL_ENABLED === 'true') {
91
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
92
+ const otelSdk = require('./instrumentation').default;
93
+ void otelSdk?.shutdown().catch(() => undefined);
94
+ }
91
95
  httpServer.close(() => {
92
96
  loggerService.log('HTTP server closed');
93
97
  process.exit(0);