@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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { NumberValueObject } from './number.valueobject';
|
|
2
|
+
|
|
3
|
+
describe('NumberValueObject', () => {
|
|
4
|
+
it('should store and return the numeric value', () => {
|
|
5
|
+
const vo = new NumberValueObject(42);
|
|
6
|
+
expect(vo.value).toBe(42);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should store zero as a valid value', () => {
|
|
10
|
+
const vo = new NumberValueObject(0);
|
|
11
|
+
expect(vo.value).toBe(0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should store negative numbers', () => {
|
|
15
|
+
const vo = new NumberValueObject(-7);
|
|
16
|
+
expect(vo.value).toBe(-7);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should throw when constructed with NaN', () => {
|
|
20
|
+
expect(() => new NumberValueObject(NaN)).toThrow(
|
|
21
|
+
'Number value must be a finite number',
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should throw when constructed with Infinity', () => {
|
|
26
|
+
expect(() => new NumberValueObject(Infinity)).toThrow(
|
|
27
|
+
'Number value must be a finite number',
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should throw when constructed with negative Infinity', () => {
|
|
32
|
+
expect(() => new NumberValueObject(-Infinity)).toThrow(
|
|
33
|
+
'Number value must be a finite number',
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should be equal to another NumberValueObject with the same value', () => {
|
|
38
|
+
const vo1 = new NumberValueObject(99);
|
|
39
|
+
const vo2 = new NumberValueObject(99);
|
|
40
|
+
expect(vo1.equals(vo2)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should not be equal to a NumberValueObject with a different value', () => {
|
|
44
|
+
const vo1 = new NumberValueObject(1);
|
|
45
|
+
const vo2 = new NumberValueObject(2);
|
|
46
|
+
expect(vo1.equals(vo2)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ValueObject } from '../base/value-object';
|
|
2
|
+
|
|
3
|
+
export class NumberValueObject extends ValueObject<{ value: number }> {
|
|
4
|
+
constructor(value: number) {
|
|
5
|
+
if (!Number.isFinite(value)) {
|
|
6
|
+
throw new Error('Number value must be a finite number');
|
|
7
|
+
}
|
|
8
|
+
super({ value });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get value(): number {
|
|
12
|
+
return this.props.value;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { StringValueObject } from './string.valueobject';
|
|
2
|
+
|
|
3
|
+
describe('StringValueObject', () => {
|
|
4
|
+
it('should store and return the string value', () => {
|
|
5
|
+
const vo = new StringValueObject('hello');
|
|
6
|
+
expect(vo.value).toBe('hello');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('should throw when constructed with null', () => {
|
|
10
|
+
expect(() => new StringValueObject(null as any)).toThrow(
|
|
11
|
+
'String value must not be null or undefined',
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should throw when constructed with undefined', () => {
|
|
16
|
+
expect(() => new StringValueObject(undefined as any)).toThrow(
|
|
17
|
+
'String value must not be null or undefined',
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should be equal to another StringValueObject with the same value', () => {
|
|
22
|
+
const vo1 = new StringValueObject('hello');
|
|
23
|
+
const vo2 = new StringValueObject('hello');
|
|
24
|
+
expect(vo1.equals(vo2)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should not be equal to a StringValueObject with a different value', () => {
|
|
28
|
+
const vo1 = new StringValueObject('hello');
|
|
29
|
+
const vo2 = new StringValueObject('world');
|
|
30
|
+
expect(vo1.equals(vo2)).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should allow empty strings (only null/undefined are rejected)', () => {
|
|
34
|
+
const vo = new StringValueObject('');
|
|
35
|
+
expect(vo.value).toBe('');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ValueObject } from '../base/value-object';
|
|
2
|
+
|
|
3
|
+
export class StringValueObject extends ValueObject<{ value: string }> {
|
|
4
|
+
constructor(value: string) {
|
|
5
|
+
if (value === null || value === undefined) {
|
|
6
|
+
throw new Error('String value must not be null or undefined');
|
|
7
|
+
}
|
|
8
|
+
super({ value });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get value(): string {
|
|
12
|
+
return this.props.value;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "commonjs",
|
|
4
|
+
"declaration": true,
|
|
5
|
+
"removeComments": true,
|
|
6
|
+
"emitDecoratorMetadata": true,
|
|
7
|
+
"experimentalDecorators": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"target": "ES2023",
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"outDir": "./dist",
|
|
13
|
+
"baseUrl": "./",
|
|
14
|
+
"paths": {
|
|
15
|
+
"@shared/*": ["src/shared/*"]
|
|
16
|
+
},
|
|
17
|
+
"incremental": true,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"strictNullChecks": true,
|
|
20
|
+
"noImplicitAny": false
|
|
21
|
+
},
|
|
22
|
+
"exclude": ["node_modules", "dist", "test", "packages"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Application
|
|
2
|
+
PORT=3000
|
|
3
|
+
NODE_ENV=development
|
|
4
|
+
|
|
5
|
+
# Database
|
|
6
|
+
DATABASE_URL=postgresql://user:password@localhost:5432/template_db?schema=public
|
|
7
|
+
|
|
8
|
+
# Redis
|
|
9
|
+
REDIS_URL=redis://localhost:6379
|
|
10
|
+
|
|
11
|
+
# API
|
|
12
|
+
API_VERSION=1
|
|
13
|
+
APP_NAME=NestJS Backend Template
|
|
14
|
+
APP_DESCRIPTION=API Documentation
|
|
15
|
+
REQUEST_TIMEOUT=30000
|
|
16
|
+
|
|
17
|
+
# Rate Limiting
|
|
18
|
+
THROTTLE_TTL=60
|
|
19
|
+
THROTTLE_LIMIT=100
|
|
20
|
+
|
|
21
|
+
# Docs
|
|
22
|
+
DOCS_USER=admin
|
|
23
|
+
DOCS_PASS=admin
|
|
24
|
+
|
|
25
|
+
# Observability - Logging
|
|
26
|
+
# LOG_LEVEL is controlled by NODE_ENV (debug in dev, info in prod)
|
|
27
|
+
|
|
28
|
+
# Observability - OpenTelemetry (opt-in)
|
|
29
|
+
OTEL_ENABLED=false
|
|
30
|
+
OTEL_SERVICE_NAME=nestjs-backend-template
|
|
31
|
+
OTEL_PROMETHEUS_PORT=9464
|
|
32
|
+
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# =========================
|
|
2
|
+
# 1) Dependencies
|
|
3
|
+
# =========================
|
|
4
|
+
FROM node:22-alpine AS deps
|
|
5
|
+
|
|
6
|
+
RUN apk upgrade --no-cache \
|
|
7
|
+
&& addgroup -g 1001 -S appgroup \
|
|
8
|
+
&& adduser -S appuser -u 1001 -G appgroup
|
|
9
|
+
|
|
10
|
+
WORKDIR /app
|
|
11
|
+
RUN chown appuser:appgroup /app
|
|
12
|
+
|
|
13
|
+
COPY --chown=appuser:appgroup package*.json ./
|
|
14
|
+
COPY --chown=appuser:appgroup prisma ./prisma/
|
|
15
|
+
COPY --chown=appuser:appgroup prisma.config.ts ./
|
|
16
|
+
|
|
17
|
+
USER appuser
|
|
18
|
+
|
|
19
|
+
RUN npm ci --fetch-timeout=600000 --fetch-retries=5
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# =========================
|
|
23
|
+
# 2) Builder
|
|
24
|
+
# =========================
|
|
25
|
+
FROM deps AS builder
|
|
26
|
+
|
|
27
|
+
COPY --chown=appuser:appgroup . .
|
|
28
|
+
|
|
29
|
+
RUN npx prisma generate && npm run build
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# =========================
|
|
33
|
+
# 3) Runner (Production)
|
|
34
|
+
# =========================
|
|
35
|
+
FROM node:22-alpine AS runner
|
|
36
|
+
|
|
37
|
+
RUN apk upgrade --no-cache \
|
|
38
|
+
&& apk add --no-cache curl \
|
|
39
|
+
&& addgroup -g 1001 -S appgroup \
|
|
40
|
+
&& adduser -S appuser -u 1001 -G appgroup
|
|
41
|
+
|
|
42
|
+
WORKDIR /app
|
|
43
|
+
|
|
44
|
+
COPY --from=builder --chown=appuser:appgroup /app/package*.json ./
|
|
45
|
+
COPY --from=builder --chown=appuser:appgroup /app/prisma ./prisma
|
|
46
|
+
COPY --from=builder --chown=appuser:appgroup /app/prisma.config.ts ./prisma.config.ts
|
|
47
|
+
|
|
48
|
+
RUN npm ci --omit=dev --fetch-timeout=600000 --fetch-retries=5 && npm cache clean --force
|
|
49
|
+
|
|
50
|
+
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
|
|
51
|
+
|
|
52
|
+
RUN mkdir -p logs && chown appuser:appgroup logs
|
|
53
|
+
|
|
54
|
+
ENV NODE_ENV=production \
|
|
55
|
+
PORT=3000
|
|
56
|
+
|
|
57
|
+
USER appuser
|
|
58
|
+
|
|
59
|
+
EXPOSE 3000
|
|
60
|
+
|
|
61
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
62
|
+
CMD curl -f http://localhost:3000/health || exit 1
|
|
63
|
+
|
|
64
|
+
CMD ["node", "dist/src/main"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import eslint from '@eslint/js';
|
|
3
|
+
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
|
4
|
+
import globals from 'globals';
|
|
5
|
+
import tseslint from 'typescript-eslint';
|
|
6
|
+
|
|
7
|
+
export default tseslint.config(
|
|
8
|
+
{
|
|
9
|
+
ignores: ['eslint.config.mjs'],
|
|
10
|
+
},
|
|
11
|
+
eslint.configs.recommended,
|
|
12
|
+
...tseslint.configs.recommendedTypeChecked,
|
|
13
|
+
eslintPluginPrettierRecommended,
|
|
14
|
+
{
|
|
15
|
+
languageOptions: {
|
|
16
|
+
globals: {
|
|
17
|
+
...globals.node,
|
|
18
|
+
...globals.jest,
|
|
19
|
+
},
|
|
20
|
+
sourceType: 'commonjs',
|
|
21
|
+
parserOptions: {
|
|
22
|
+
projectService: true,
|
|
23
|
+
tsconfigRootDir: import.meta.dirname,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
rules: {
|
|
29
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
30
|
+
'@typescript-eslint/no-floating-promises': 'warn',
|
|
31
|
+
'@typescript-eslint/no-unsafe-argument': 'warn',
|
|
32
|
+
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nestjs-backend-template",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"author": "",
|
|
6
|
+
"private": true,
|
|
7
|
+
"license": "UNLICENSED",
|
|
8
|
+
"workspaces": [
|
|
9
|
+
"packages/*"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "nest build",
|
|
13
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
14
|
+
"start": "nest start",
|
|
15
|
+
"start:dev": "nest start --watch",
|
|
16
|
+
"start:debug": "nest start --debug --watch",
|
|
17
|
+
"start:prod": "node dist/src/main",
|
|
18
|
+
"db:migrate:deploy": "prisma migrate deploy",
|
|
19
|
+
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
|
20
|
+
"test": "jest",
|
|
21
|
+
"test:watch": "jest --watch",
|
|
22
|
+
"test:cov": "jest --coverage",
|
|
23
|
+
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
|
24
|
+
"test:e2e": "jest --config ./test/jest-e2e.json"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@nestjs/common": "^11.0.1",
|
|
28
|
+
"@nestjs/config": "^4.0.3",
|
|
29
|
+
"@nestjs/core": "^11.0.1",
|
|
30
|
+
"@nestjs/cqrs": "^11.0.3",
|
|
31
|
+
"@nestjs/platform-express": "^11.0.1",
|
|
32
|
+
"@nestjs/swagger": "^11.2.6",
|
|
33
|
+
"@nestjs/terminus": "^11.1.1",
|
|
34
|
+
"@nestjs/throttler": "^6.5.0",
|
|
35
|
+
"@opentelemetry/api": "^1.9.1",
|
|
36
|
+
"@opentelemetry/auto-instrumentations-node": "^0.72.0",
|
|
37
|
+
"@opentelemetry/exporter-prometheus": "^0.214.0",
|
|
38
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
|
|
39
|
+
"@opentelemetry/resources": "^2.6.1",
|
|
40
|
+
"@opentelemetry/sdk-node": "^0.214.0",
|
|
41
|
+
"@opentelemetry/sdk-trace-base": "^2.6.1",
|
|
42
|
+
"@prisma/adapter-pg": "^7.5.0",
|
|
43
|
+
"@prisma/client": "^7.5.0",
|
|
44
|
+
"@scalar/nestjs-api-reference": "^1.1.5",
|
|
45
|
+
"@types/pg": "^8.20.0",
|
|
46
|
+
"class-transformer": "^0.5.1",
|
|
47
|
+
"class-validator": "^0.15.1",
|
|
48
|
+
"cockatiel": "^3.2.1",
|
|
49
|
+
"express-basic-auth": "^1.2.1",
|
|
50
|
+
"ioredis": "^5.10.1",
|
|
51
|
+
"nestjs-pino": "^4.6.1",
|
|
52
|
+
"pg": "^8.20.0",
|
|
53
|
+
"pino-http": "^11.0.0",
|
|
54
|
+
"pino-pretty": "^13.1.3",
|
|
55
|
+
"pino-roll": "^4.0.0",
|
|
56
|
+
"reflect-metadata": "^0.2.2",
|
|
57
|
+
"rxjs": "^7.8.1"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
61
|
+
"@eslint/js": "^9.18.0",
|
|
62
|
+
"@nestjs/cli": "^11.0.0",
|
|
63
|
+
"@nestjs/schematics": "^11.0.0",
|
|
64
|
+
"@nestjs/testing": "^11.0.1",
|
|
65
|
+
"@types/express": "^5.0.0",
|
|
66
|
+
"@types/ioredis": "^5.0.0",
|
|
67
|
+
"@types/jest": "^30.0.0",
|
|
68
|
+
"@types/node": "^22.10.7",
|
|
69
|
+
"@types/supertest": "^6.0.2",
|
|
70
|
+
"eslint": "^9.18.0",
|
|
71
|
+
"eslint-config-prettier": "^10.0.1",
|
|
72
|
+
"eslint-plugin-prettier": "^5.2.2",
|
|
73
|
+
"globals": "^16.0.0",
|
|
74
|
+
"jest": "^30.0.0",
|
|
75
|
+
"prettier": "^3.4.2",
|
|
76
|
+
"prisma": "^7.5.0",
|
|
77
|
+
"source-map-support": "^0.5.21",
|
|
78
|
+
"supertest": "^7.0.0",
|
|
79
|
+
"ts-jest": "^29.2.5",
|
|
80
|
+
"ts-loader": "^9.5.2",
|
|
81
|
+
"ts-node": "^10.9.2",
|
|
82
|
+
"tsconfig-paths": "^4.2.0",
|
|
83
|
+
"typescript": "^5.7.3",
|
|
84
|
+
"typescript-eslint": "^8.20.0"
|
|
85
|
+
},
|
|
86
|
+
"jest": {
|
|
87
|
+
"moduleFileExtensions": [
|
|
88
|
+
"js",
|
|
89
|
+
"json",
|
|
90
|
+
"ts"
|
|
91
|
+
],
|
|
92
|
+
"rootDir": "src",
|
|
93
|
+
"testRegex": ".*\\.spec\\.ts$",
|
|
94
|
+
"transform": {
|
|
95
|
+
"^.+\\.(t|j)s$": "ts-jest"
|
|
96
|
+
},
|
|
97
|
+
"collectCoverageFrom": [
|
|
98
|
+
"**/*.(t|j)s"
|
|
99
|
+
],
|
|
100
|
+
"coverageDirectory": "../coverage",
|
|
101
|
+
"testEnvironment": "node"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig, env } from 'prisma/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
schema: 'prisma/schema.prisma',
|
|
5
|
+
migrations: {
|
|
6
|
+
path: 'prisma/migrations',
|
|
7
|
+
},
|
|
8
|
+
datasource: {
|
|
9
|
+
url: process.env.DATABASE_URL ?? 'postgresql://localhost:5432/app',
|
|
10
|
+
},
|
|
11
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
|
2
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
3
|
+
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
|
|
4
|
+
import { AppConfigModule } from './infrastructure/config/config.module';
|
|
5
|
+
import { DatabaseModule } from './infrastructure/database/prisma.module';
|
|
6
|
+
import { CacheModule } from './infrastructure/cache/redis.module';
|
|
7
|
+
import { HealthModule } from './infrastructure/health/health.module';
|
|
8
|
+
import { ExampleModule } from './modules/example/example.module';
|
|
9
|
+
import { CorrelationIdMiddleware } from './common/middleware/correlation-id.middleware';
|
|
10
|
+
import { LoggerModule } from './shared/logger/logger.module';
|
|
11
|
+
|
|
12
|
+
@Module({
|
|
13
|
+
imports: [
|
|
14
|
+
AppConfigModule,
|
|
15
|
+
DatabaseModule,
|
|
16
|
+
CacheModule,
|
|
17
|
+
HealthModule,
|
|
18
|
+
ExampleModule,
|
|
19
|
+
LoggerModule,
|
|
20
|
+
ThrottlerModule.forRoot([
|
|
21
|
+
{
|
|
22
|
+
ttl: parseInt(process.env.THROTTLE_TTL ?? '60', 10) * 1000,
|
|
23
|
+
limit: parseInt(process.env.THROTTLE_LIMIT ?? '100', 10),
|
|
24
|
+
},
|
|
25
|
+
]),
|
|
26
|
+
],
|
|
27
|
+
controllers: [],
|
|
28
|
+
providers: [{ provide: APP_GUARD, useClass: ThrottlerGuard }],
|
|
29
|
+
})
|
|
30
|
+
export class AppModule implements NestModule {
|
|
31
|
+
configure(consumer: MiddlewareConsumer) {
|
|
32
|
+
consumer.apply(CorrelationIdMiddleware).forRoutes('*');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SetMetadata } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
export const PUBLIC_API_KEY = 'public_api';
|
|
4
|
+
/**
|
|
5
|
+
* Decorator to mark an endpoint as Public API.
|
|
6
|
+
* This will trigger the TransformInterceptor to wrap the response in { success: true, data: T }.
|
|
7
|
+
* Without this decorator, the response remains raw.
|
|
8
|
+
*/
|
|
9
|
+
export const PublicApi = () => SetMetadata(PUBLIC_API_KEY, true);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { HttpException } from '@nestjs/common';
|
|
2
|
+
import { AppException } from '../../shared/exceptions/app.exception';
|
|
3
|
+
import { ErrorCodes } from '../../shared/exceptions/error-codes';
|
|
4
|
+
import { HttpExceptionFilter } from './http-exception.filter';
|
|
5
|
+
|
|
6
|
+
function createMockHost(jsonMock: jest.Mock) {
|
|
7
|
+
const statusMock = jest.fn().mockReturnValue({ json: jsonMock });
|
|
8
|
+
const response = { status: statusMock };
|
|
9
|
+
const request = { method: 'GET', url: '/test' };
|
|
10
|
+
return {
|
|
11
|
+
switchToHttp: () => ({
|
|
12
|
+
getResponse: () => response,
|
|
13
|
+
getRequest: () => request,
|
|
14
|
+
}),
|
|
15
|
+
} as any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('HttpExceptionFilter', () => {
|
|
19
|
+
let filter: HttpExceptionFilter;
|
|
20
|
+
let jsonMock: jest.Mock;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
filter = new HttpExceptionFilter();
|
|
24
|
+
jsonMock = jest.fn();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should format AppException(NOT_FOUND) to correct error shape', () => {
|
|
28
|
+
const exception = new AppException({
|
|
29
|
+
code: ErrorCodes.NOT_FOUND,
|
|
30
|
+
message: 'Item not found',
|
|
31
|
+
statusCode: 404,
|
|
32
|
+
});
|
|
33
|
+
const host = createMockHost(jsonMock);
|
|
34
|
+
|
|
35
|
+
filter.catch(exception, host);
|
|
36
|
+
|
|
37
|
+
expect(jsonMock).toHaveBeenCalledWith({
|
|
38
|
+
success: false,
|
|
39
|
+
error: {
|
|
40
|
+
code: 'NOT_FOUND',
|
|
41
|
+
message: 'Item not found',
|
|
42
|
+
statusCode: 404,
|
|
43
|
+
details: null,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should include details array from AppException', () => {
|
|
49
|
+
const details = [{ field: 'email', constraints: { isEmail: 'must be email' } }];
|
|
50
|
+
const exception = new AppException({
|
|
51
|
+
code: ErrorCodes.VALIDATION_FAILED,
|
|
52
|
+
message: 'Validation failed',
|
|
53
|
+
statusCode: 400,
|
|
54
|
+
details,
|
|
55
|
+
});
|
|
56
|
+
const host = createMockHost(jsonMock);
|
|
57
|
+
|
|
58
|
+
filter.catch(exception, host);
|
|
59
|
+
|
|
60
|
+
expect(jsonMock).toHaveBeenCalledWith({
|
|
61
|
+
success: false,
|
|
62
|
+
error: {
|
|
63
|
+
code: 'VALIDATION_FAILED',
|
|
64
|
+
message: 'Validation failed',
|
|
65
|
+
statusCode: 400,
|
|
66
|
+
details,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should handle generic HttpException(403) with FORBIDDEN code', () => {
|
|
72
|
+
const exception = new HttpException('Forbidden', 403);
|
|
73
|
+
const host = createMockHost(jsonMock);
|
|
74
|
+
|
|
75
|
+
filter.catch(exception, host);
|
|
76
|
+
|
|
77
|
+
const call = jsonMock.mock.calls[0][0];
|
|
78
|
+
expect(call.success).toBe(false);
|
|
79
|
+
expect(call.error.statusCode).toBe(403);
|
|
80
|
+
expect(call.error.code).toBe('FORBIDDEN');
|
|
81
|
+
expect(call.error.details).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle generic HttpException(500) with INTERNAL_SERVER_ERROR code', () => {
|
|
85
|
+
const exception = new HttpException('Internal Server Error', 500);
|
|
86
|
+
const host = createMockHost(jsonMock);
|
|
87
|
+
|
|
88
|
+
filter.catch(exception, host);
|
|
89
|
+
|
|
90
|
+
const call = jsonMock.mock.calls[0][0];
|
|
91
|
+
expect(call.success).toBe(false);
|
|
92
|
+
expect(call.error.statusCode).toBe(500);
|
|
93
|
+
expect(call.error.code).toBe('INTERNAL_SERVER_ERROR');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArgumentsHost,
|
|
3
|
+
Catch,
|
|
4
|
+
ExceptionFilter,
|
|
5
|
+
HttpException,
|
|
6
|
+
HttpStatus,
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import { Request, Response } from 'express';
|
|
9
|
+
import { LoggerService } from '../../shared/logger/logger.service';
|
|
10
|
+
|
|
11
|
+
@Catch(HttpException)
|
|
12
|
+
export class HttpExceptionFilter implements ExceptionFilter {
|
|
13
|
+
constructor(private readonly logger: LoggerService) {}
|
|
14
|
+
|
|
15
|
+
catch(exception: HttpException, host: ArgumentsHost): void {
|
|
16
|
+
const ctx = host.switchToHttp();
|
|
17
|
+
const response = ctx.getResponse<Response>();
|
|
18
|
+
const request = ctx.getRequest<Request>();
|
|
19
|
+
|
|
20
|
+
const statusCode = exception.getStatus();
|
|
21
|
+
const res = exception.getResponse() as any;
|
|
22
|
+
|
|
23
|
+
const code = res.error || HttpStatus[statusCode] || 'INTERNAL_ERROR';
|
|
24
|
+
const message = res.message || exception.message;
|
|
25
|
+
|
|
26
|
+
this.logger.error(
|
|
27
|
+
`[${request.method}] ${request.url} - ${statusCode} ${code}: ${message}`,
|
|
28
|
+
exception.stack,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
// Standardized Error Response
|
|
32
|
+
response.status(statusCode).json({
|
|
33
|
+
success: false,
|
|
34
|
+
error: {
|
|
35
|
+
code,
|
|
36
|
+
message,
|
|
37
|
+
statusCode,
|
|
38
|
+
timestamp: new Date().toISOString(),
|
|
39
|
+
path: request.url,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// RPC Exception Filter — placeholder for microservice completeness.
|
|
2
|
+
// Requires @nestjs/microservices to be installed.
|
|
3
|
+
// When @nestjs/microservices is added, uncomment the implementation below.
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
|
|
7
|
+
import { RpcException } from '@nestjs/microservices';
|
|
8
|
+
import { throwError } from 'rxjs';
|
|
9
|
+
|
|
10
|
+
@Catch(RpcException)
|
|
11
|
+
export class RpcExceptionFilter implements ExceptionFilter {
|
|
12
|
+
catch(exception: RpcException, _host: ArgumentsHost) {
|
|
13
|
+
return throwError(() => exception.getError());
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
|
2
|
+
import { Observable, throwError, TimeoutError } from 'rxjs';
|
|
3
|
+
import { catchError, timeout } from 'rxjs/operators';
|
|
4
|
+
import { AppException } from '../../shared/exceptions/app.exception';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class TimeoutInterceptor implements NestInterceptor {
|
|
10
|
+
intercept(_context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
|
11
|
+
const timeoutMs = process.env.REQUEST_TIMEOUT
|
|
12
|
+
? parseInt(process.env.REQUEST_TIMEOUT, 10)
|
|
13
|
+
: DEFAULT_TIMEOUT_MS;
|
|
14
|
+
|
|
15
|
+
return next.handle().pipe(
|
|
16
|
+
timeout(timeoutMs),
|
|
17
|
+
catchError((err) => {
|
|
18
|
+
if (err instanceof TimeoutError) {
|
|
19
|
+
return throwError(
|
|
20
|
+
() =>
|
|
21
|
+
new AppException({
|
|
22
|
+
code: 'REQUEST_TIMEOUT',
|
|
23
|
+
message: 'Request timeout',
|
|
24
|
+
statusCode: 408,
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return throwError(() => err);
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|