chore:init project template
This commit is contained in:
21
.env.example
Normal file
21
.env.example
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
NODE_ENV=development
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
CORS_ORIGINS=http://localhost:5173,http://localhost:3000
|
||||||
|
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=postgres
|
||||||
|
DB_NAME=nest_db
|
||||||
|
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_USER=
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
|
||||||
|
JWT_SECRET=change-me
|
||||||
|
JWT_EXPIRES_IN=7d
|
||||||
|
|
||||||
|
THROTTLE_TTL=60000
|
||||||
|
THROTTLE_LIMIT=60
|
||||||
61
.gitignore
vendored
Normal file
61
.gitignore
vendored
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
/build
|
||||||
|
|
||||||
|
# logs
|
||||||
|
/logs
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
.claude
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# temp directory
|
||||||
|
.temp
|
||||||
|
.tmp
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
256
CLAUDE.md
Normal file
256
CLAUDE.md
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
# NestJS Template — 開發規範
|
||||||
|
|
||||||
|
## 技術棧
|
||||||
|
|
||||||
|
| 分類 | 套件 |
|
||||||
|
|---|---|
|
||||||
|
| Runtime | Node.js(ESM,`"type": "module"`) |
|
||||||
|
| 框架 | NestJS 11 |
|
||||||
|
| 語言 | TypeScript 5(`NodeNext` 模組解析) |
|
||||||
|
| 套件管理 | pnpm |
|
||||||
|
| 資料庫 | PostgreSQL(`@nestjs/typeorm` + `typeorm` + `pg`) |
|
||||||
|
| 快取/佇列 | Redis(`ioredis`) |
|
||||||
|
| 設定管理 | `@nestjs/config`(`.env` 載入) |
|
||||||
|
| 驗證 | `class-validator` + `class-transformer` |
|
||||||
|
| 日誌 | `nestjs-pino` + `pino-roll` |
|
||||||
|
| API 文件 | `@nestjs/swagger`(僅非 production,路徑 `/api/docs`) |
|
||||||
|
| 資安 | `helmet` + NestJS 內建 CORS |
|
||||||
|
| 流量控制 | `@nestjs/throttler` |
|
||||||
|
| 認證 | `@nestjs/jwt` |
|
||||||
|
|
||||||
|
## 目錄結構
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── core/ # NestJS 生命週期類別 + 基礎設施模組
|
||||||
|
│ ├── db/
|
||||||
|
│ │ └── db.module.ts
|
||||||
|
│ ├── redis/
|
||||||
|
│ │ └── redis.module.ts
|
||||||
|
│ ├── logger/
|
||||||
|
│ │ └── logger.module.ts
|
||||||
|
│ ├── throttler/
|
||||||
|
│ │ └── throttler.module.ts
|
||||||
|
│ ├── filter/
|
||||||
|
│ │ └── http-exception.filter.ts
|
||||||
|
│ ├── interceptor/
|
||||||
|
│ │ └── transform.interceptor.ts
|
||||||
|
│ ├── pipe/
|
||||||
|
│ │ └── validation-exception.factory.ts
|
||||||
|
│ ├── guard/
|
||||||
|
│ │ └── jwt.guard.ts
|
||||||
|
│ └── <類型>/ # middleware …
|
||||||
|
├── common/ # 共用但不掛生命週期的內容
|
||||||
|
│ ├── token/
|
||||||
|
│ │ └── tokens.ts # 注入 token 常數(REDIS_CLIENT 等)
|
||||||
|
│ ├── type/
|
||||||
|
│ │ ├── response.ts # ApiResponse<T> 統一回傳型別
|
||||||
|
│ │ └── jwt-payload.ts # JWT payload 型別
|
||||||
|
│ ├── decorator/
|
||||||
|
│ │ └── public.decorator.ts # @Public() 跳過 JWT 驗證
|
||||||
|
│ └── <類型>/ # util、constant …
|
||||||
|
├── module/ # 功能模組(每個領域一個子目錄)
|
||||||
|
│ └── <模組名>/
|
||||||
|
│ ├── dtos/
|
||||||
|
│ ├── entities/
|
||||||
|
│ ├── <模組名>.controller.ts
|
||||||
|
│ ├── <模組名>.service.ts
|
||||||
|
│ └── <模組名>.module.ts
|
||||||
|
├── app.module.ts
|
||||||
|
└── main.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### `core/` — 生命週期類別與基礎設施模組
|
||||||
|
|
||||||
|
**任何檔案都必須放在對應的子目錄中,不可直接置於 `core/` 根層。**
|
||||||
|
|
||||||
|
| 子目錄 | 內容 |
|
||||||
|
|---|---|
|
||||||
|
| `filter/` | 例外處理,`@Catch()` |
|
||||||
|
| `interceptor/` | 請求/回應攔截,`NestInterceptor` |
|
||||||
|
| `guard/` | 路由守衛,`CanActivate` |
|
||||||
|
| `pipe/` | 輸入轉換與驗證相關,`PipeTransform` 及 factory |
|
||||||
|
| `middleware/` | Express 中介層,`NestMiddleware` |
|
||||||
|
| `db/` | TypeORM / PostgreSQL 設定 |
|
||||||
|
| `redis/` | Redis 設定 |
|
||||||
|
| `logger/` | Pino logger 設定 |
|
||||||
|
| `throttler/` | Rate limiting 設定 |
|
||||||
|
|
||||||
|
### `common/` — 共用工具
|
||||||
|
|
||||||
|
不掛生命週期、跨模組共用的內容。**任何檔案都必須放在對應的子目錄中,不可直接置於 `common/` 根層。**
|
||||||
|
|
||||||
|
| 子目錄 | 內容 |
|
||||||
|
|---|---|
|
||||||
|
| `token/` | 注入 token 常數(`REDIS_CLIENT` 等) |
|
||||||
|
| `type/` | 共用 TypeScript 型別/interface/enum |
|
||||||
|
| `decorator/` | 自訂裝飾器 |
|
||||||
|
| `util/` | 純函式工具 |
|
||||||
|
| `constant/` | 一般常數 |
|
||||||
|
|
||||||
|
### `module/` — 功能模組
|
||||||
|
|
||||||
|
每個業務領域一個子目錄,結構依複雜度選擇:
|
||||||
|
|
||||||
|
簡單模組(單一 controller / service):
|
||||||
|
|
||||||
|
```
|
||||||
|
module/<模組名>/
|
||||||
|
├── dtos/
|
||||||
|
├── entities/
|
||||||
|
├── <模組名>.controller.ts
|
||||||
|
├── <模組名>.service.ts
|
||||||
|
└── <模組名>.module.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
複雜模組(多個 controller / service):
|
||||||
|
|
||||||
|
```
|
||||||
|
module/<模組名>/
|
||||||
|
├── dtos/
|
||||||
|
├── entities/
|
||||||
|
├── controllers/
|
||||||
|
│ ├── <子功能>.controller.ts
|
||||||
|
│ └── ...
|
||||||
|
├── services/
|
||||||
|
│ ├── <子功能>.service.ts
|
||||||
|
│ └── ...
|
||||||
|
└── <模組名>.module.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
> 單一檔案與目錄切分可混用:service 需要拆分但 controller 不需要時,controller 維持單檔即可。
|
||||||
|
|
||||||
|
## 全域基礎設施
|
||||||
|
|
||||||
|
以下透過 `APP_*` token 在 `AppModule` 全域註冊,所有模組自動套用。執行順序如下:
|
||||||
|
|
||||||
|
1. `ThrottlerGuard` — rate limiting
|
||||||
|
2. `JwtAuthGuard` — JWT 驗證
|
||||||
|
3. `ValidationPipe` — 請求驗證
|
||||||
|
4. `TransformInterceptor` — 回應包裝
|
||||||
|
5. `HttpExceptionFilter` — 例外捕捉
|
||||||
|
|
||||||
|
### 統一回傳格式(`ApiResponse<T>`)
|
||||||
|
|
||||||
|
所有端點一律回傳 `{ code, message, data }` 結構:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 成功
|
||||||
|
{ "code": 200, "message": "ok", "data": { ... } }
|
||||||
|
|
||||||
|
// 失敗
|
||||||
|
{ "code": 404, "message": "...", "data": null }
|
||||||
|
```
|
||||||
|
|
||||||
|
- `TransformInterceptor`(`core/interceptor/`)— 包裝成功回應
|
||||||
|
- `HttpExceptionFilter`(`core/filter/`)— 捕捉所有例外並統一格式
|
||||||
|
|
||||||
|
### ValidationPipe
|
||||||
|
|
||||||
|
| 選項 | 值 | 說明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `whitelist` | `true` | 自動剔除 DTO 未宣告的屬性 |
|
||||||
|
| `forbidNonWhitelisted` | `true` | 傳入未宣告屬性時回傳 400 |
|
||||||
|
| `transform` | `true` | 自動將 payload 轉為 DTO 實例 |
|
||||||
|
| `exceptionFactory` | `validationExceptionFactory` | forbidden 欄位顯示 `不允許的欄位:<field>` |
|
||||||
|
|
||||||
|
### JWT 認證
|
||||||
|
|
||||||
|
- 全域啟用 `JwtAuthGuard`,預設所有路由需要有效 Bearer token
|
||||||
|
- 公開路由加上 `@Public()` decorator 跳過驗證
|
||||||
|
- Payload 型別定義於 `common/type/jwt-payload.ts`,`sub` 為 user ID
|
||||||
|
- 需要 Swagger 測試時在端點加上 `@ApiBearerAuth()`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Public() // 跳過 JWT
|
||||||
|
@SkipThrottle() // 跳過 rate limiting(@nestjs/throttler 提供)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
|
||||||
|
透過 `THROTTLE_TTL`(毫秒)與 `THROTTLE_LIMIT`(次數)設定,預設 60 秒內最多 60 次請求。
|
||||||
|
|
||||||
|
## 日誌策略
|
||||||
|
|
||||||
|
使用 `nestjs-pino`,以 `NODE_ENV` 切換行為:
|
||||||
|
|
||||||
|
| 環境 | 輸出 |
|
||||||
|
|---|---|
|
||||||
|
| 非 production | `pino-pretty`,colorize,輸出至 stdout |
|
||||||
|
| production | `pino-roll` 寫入檔案 |
|
||||||
|
|
||||||
|
**production 檔案規則:**
|
||||||
|
|
||||||
|
| 類型 | 路徑 | 檔名格式 | 保留天數 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| app log | `logs/app/` | `app.yyyy-MM-dd.log` | 14 天 |
|
||||||
|
| error log | `logs/error/` | `error.yyyy-MM-dd.log` | 30 天 |
|
||||||
|
|
||||||
|
**每筆 request 自動記錄:** `method`、`url`、`query`、`params`、`ip`(含 X-Forwarded-For)、`userAgent`、`statusCode`、`responseTime`
|
||||||
|
|
||||||
|
## 資安
|
||||||
|
|
||||||
|
### Helmet
|
||||||
|
|
||||||
|
全域套用 `helmet()` Express middleware,設定標準安全 HTTP headers(CSP、HSTS、X-Frame-Options 等)。
|
||||||
|
|
||||||
|
### CORS
|
||||||
|
|
||||||
|
透過 `CORS_ORIGINS` 環境變數控制允許的來源,多個來源以逗號分隔。預設空陣列(全封)。
|
||||||
|
|
||||||
|
| 設定 | 值 |
|
||||||
|
|---|---|
|
||||||
|
| `methods` | GET、POST、PUT、PATCH、DELETE |
|
||||||
|
| `credentials` | `true` |
|
||||||
|
|
||||||
|
## Swagger
|
||||||
|
|
||||||
|
- 僅在非 production 環境啟用,路徑為 `/api/docs`
|
||||||
|
- 預設已加入 `addBearerAuth()`,需要保護的端點加上 `@ApiBearerAuth()`
|
||||||
|
- DTO 屬性使用 `@ApiProperty()` 補充文件
|
||||||
|
|
||||||
|
## 環境變數
|
||||||
|
|
||||||
|
參考 `.env.example`:
|
||||||
|
|
||||||
|
```
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
CORS_ORIGINS=http://localhost:5173,http://localhost:3000
|
||||||
|
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=postgres
|
||||||
|
DB_NAME=nest_db
|
||||||
|
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_USER= # 選填
|
||||||
|
REDIS_PASSWORD= # 選填
|
||||||
|
|
||||||
|
JWT_SECRET=change-me
|
||||||
|
JWT_EXPIRES_IN=7d
|
||||||
|
|
||||||
|
THROTTLE_TTL=60000 # 毫秒
|
||||||
|
THROTTLE_LIMIT=60 # 次數
|
||||||
|
```
|
||||||
|
|
||||||
|
## 編碼規範
|
||||||
|
|
||||||
|
- 所有相對路徑 import 必須加 `.js` 副檔名(NodeNext 規定)
|
||||||
|
- 禁止 `any`,型別須明確宣告
|
||||||
|
- 無註解 — 程式碼透過命名與結構自我表達
|
||||||
|
- 正式程式碼不留 `console.log`(使用注入的 `Logger`)
|
||||||
|
- 類別屬性在適用情況下使用 `readonly`
|
||||||
|
|
||||||
|
## 常用指令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm start:dev # 開發模式(watch)
|
||||||
|
pnpm build # 編譯
|
||||||
|
pnpm start:prod # 執行編譯後的產物
|
||||||
|
pnpm test # 單元測試
|
||||||
|
pnpm test:cov # 測試覆蓋率
|
||||||
|
```
|
||||||
120
README.md
Normal file
120
README.md
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# NestJS Template
|
||||||
|
|
||||||
|
生產就緒的 NestJS 起始模板,內建常用基礎設施與規範,開新專案時直接 fork 使用。
|
||||||
|
|
||||||
|
## 技術棧
|
||||||
|
|
||||||
|
| 分類 | 套件 |
|
||||||
|
|---|---|
|
||||||
|
| 框架 | NestJS 11 |
|
||||||
|
| 語言 | TypeScript 5(ESM + NodeNext) |
|
||||||
|
| 資料庫 | PostgreSQL + TypeORM |
|
||||||
|
| 快取 | Redis(ioredis) |
|
||||||
|
| 設定管理 | @nestjs/config |
|
||||||
|
| 驗證 | class-validator + class-transformer |
|
||||||
|
| 日誌 | nestjs-pino + pino-roll |
|
||||||
|
| 認證 | @nestjs/jwt |
|
||||||
|
| API 文件 | @nestjs/swagger |
|
||||||
|
| 流量控制 | @nestjs/throttler |
|
||||||
|
| 資安 | helmet + CORS |
|
||||||
|
|
||||||
|
## 內建功能
|
||||||
|
|
||||||
|
- **統一回傳格式** — 所有端點自動包裝為 `{ code, message, data }`
|
||||||
|
- **全域 JWT 驗證** — 預設所有路由需要 Bearer token,公開路由加 `@Public()`
|
||||||
|
- **全域 Rate Limiting** — 透過環境變數設定 TTL 與上限次數
|
||||||
|
- **全域 ValidationPipe** — 自動過濾多餘欄位、型別轉換,自訂錯誤訊息
|
||||||
|
- **統一例外處理** — 所有例外回傳一致的錯誤格式
|
||||||
|
- **結構化日誌** — 開發環境 pretty print,production 寫檔並自動輪替
|
||||||
|
- **Swagger UI** — 非 production 環境自動啟用,路徑 `/api/docs`
|
||||||
|
- **Graceful Shutdown** — 服務停止時正常關閉 DB / Redis 連線
|
||||||
|
|
||||||
|
## 快速開始
|
||||||
|
|
||||||
|
### 前置需求
|
||||||
|
|
||||||
|
- Node.js 22+
|
||||||
|
- pnpm
|
||||||
|
- PostgreSQL
|
||||||
|
- Redis
|
||||||
|
|
||||||
|
### 安裝
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
編輯 `.env`,填入資料庫、Redis、JWT 等設定值。
|
||||||
|
|
||||||
|
### 啟動
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 開發模式
|
||||||
|
pnpm start:dev
|
||||||
|
|
||||||
|
# 生產模式(需先 build)
|
||||||
|
pnpm build
|
||||||
|
pnpm start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## 環境變數
|
||||||
|
|
||||||
|
| 變數 | 說明 | 預設值 |
|
||||||
|
|---|---|---|
|
||||||
|
| `NODE_ENV` | 環境 | `development` |
|
||||||
|
| `PORT` | 監聽埠 | `3000` |
|
||||||
|
| `CORS_ORIGINS` | 允許的 CORS 來源,逗號分隔 | 空(全封) |
|
||||||
|
| `DB_HOST` | PostgreSQL 主機 | — |
|
||||||
|
| `DB_PORT` | PostgreSQL 埠 | `5432` |
|
||||||
|
| `DB_USER` | 使用者名稱 | — |
|
||||||
|
| `DB_PASSWORD` | 密碼 | — |
|
||||||
|
| `DB_NAME` | 資料庫名稱 | — |
|
||||||
|
| `REDIS_HOST` | Redis 主機 | — |
|
||||||
|
| `REDIS_PORT` | Redis 埠 | `6379` |
|
||||||
|
| `REDIS_USER` | Redis 使用者(選填) | — |
|
||||||
|
| `REDIS_PASSWORD` | Redis 密碼(選填) | — |
|
||||||
|
| `JWT_SECRET` | JWT 簽名金鑰 | — |
|
||||||
|
| `JWT_EXPIRES_IN` | Token 有效期 | `7d` |
|
||||||
|
| `THROTTLE_TTL` | Rate limit 時間窗口(毫秒) | — |
|
||||||
|
| `THROTTLE_LIMIT` | 時間窗口內最大請求數 | — |
|
||||||
|
|
||||||
|
## 目錄結構
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── core/ # 生命週期類別 + 基礎設施模組
|
||||||
|
│ ├── db/ # TypeORM 設定
|
||||||
|
│ ├── redis/ # Redis 設定
|
||||||
|
│ ├── logger/ # Pino 日誌設定
|
||||||
|
│ ├── throttler/ # Rate limiting 設定
|
||||||
|
│ ├── filter/ # 全域例外 filter
|
||||||
|
│ ├── interceptor/ # 回應包裝 interceptor
|
||||||
|
│ ├── guard/ # JWT guard
|
||||||
|
│ └── pipe/ # Validation exception factory
|
||||||
|
├── common/ # 跨模組共用(不掛生命週期)
|
||||||
|
│ ├── decorator/ # @Public() 等自訂裝飾器
|
||||||
|
│ ├── token/ # 注入 token 常數
|
||||||
|
│ └── type/ # 共用型別(ApiResponse、JwtPayload)
|
||||||
|
└── module/ # 業務功能模組(每個領域一個子目錄)
|
||||||
|
```
|
||||||
|
|
||||||
|
新業務模組放在 `src/module/<模組名>/`,結構參考 `CLAUDE.md`。
|
||||||
|
|
||||||
|
## 常用指令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm start:dev # 開發模式(watch)
|
||||||
|
pnpm start:debug # 除錯模式
|
||||||
|
pnpm build # 編譯
|
||||||
|
pnpm typecheck # 型別檢查
|
||||||
|
pnpm lint # Lint 檢查
|
||||||
|
pnpm format # 格式化
|
||||||
|
pnpm test # 單元測試
|
||||||
|
pnpm test:cov # 測試覆蓋率
|
||||||
|
pnpm test:e2e # E2E 測試
|
||||||
|
```
|
||||||
|
|
||||||
|
## 開發規範
|
||||||
|
|
||||||
|
詳見 [CLAUDE.md](./CLAUDE.md)。
|
||||||
34
eslint.config.mjs
Normal file
34
eslint.config.mjs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// @ts-check
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['eslint.config.mjs'],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
8
nest-cli.json
Normal file
8
nest-cli.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
101
package.json
Normal file
101
package.json
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"name": "nest-template",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
||||||
|
"lint": "eslint \"{src,test}/**/*.ts\"",
|
||||||
|
"format": "prettier --write \"{src,test}/**/*.ts\"",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main.js",
|
||||||
|
"_jest": "NODE_OPTIONS=--experimental-vm-modules jest --passWithNoTests",
|
||||||
|
"test": "pnpm _jest",
|
||||||
|
"test:watch": "pnpm _jest --watch",
|
||||||
|
"test:cov": "pnpm _jest --coverage",
|
||||||
|
"test:e2e": "pnpm _jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^11.0.1",
|
||||||
|
"@nestjs/config": "^4.0.4",
|
||||||
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/jwt": "^11.0.2",
|
||||||
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/swagger": "^11.2.7",
|
||||||
|
"@nestjs/throttler": "^6.5.0",
|
||||||
|
"@nestjs/typeorm": "^11.0.1",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.15.1",
|
||||||
|
"helmet": "^8.1.0",
|
||||||
|
"ioredis": "^5.10.1",
|
||||||
|
"nestjs-pino": "^4.6.1",
|
||||||
|
"pg": "^8.20.0",
|
||||||
|
"pino-roll": "^4.0.0",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"typeorm": "^0.3.28"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@swc/cli": "^0.6.0",
|
||||||
|
"@swc/core": "^1.10.7",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/pg": "^8.20.0",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"pino-pretty": "^13.1.3",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"extensionsToTreatAsEsm": [
|
||||||
|
".ts"
|
||||||
|
],
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^(\\.{1,2}/.*)\\.js$": "$1"
|
||||||
|
},
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
"useESM": true,
|
||||||
|
"diagnostics": { "ignoreCodes": [151002] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
8063
pnpm-lock.yaml
generated
Normal file
8063
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
src/app.controller.ts
Normal file
11
src/app.controller.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common'
|
||||||
|
import { Public } from './common/decorator/public.decorator.js'
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
@Public()
|
||||||
|
@Get('health')
|
||||||
|
health(): { status: string } {
|
||||||
|
return { status: 'ok' }
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/app.module.ts
Normal file
49
src/app.module.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { Module, ValidationPipe } from '@nestjs/common'
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config'
|
||||||
|
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'
|
||||||
|
import { JwtModule } from '@nestjs/jwt'
|
||||||
|
import { ThrottlerGuard } from '@nestjs/throttler'
|
||||||
|
import { AppController } from './app.controller.js'
|
||||||
|
import { DbModule } from './core/db/db.module.js'
|
||||||
|
import { HttpExceptionFilter } from './core/filter/http-exception.filter.js'
|
||||||
|
import { JwtAuthGuard } from './core/guard/jwt.guard.js'
|
||||||
|
import { TransformInterceptor } from './core/interceptor/transform.interceptor.js'
|
||||||
|
import { LoggerModule } from './core/logger/logger.module.js'
|
||||||
|
import { validationExceptionFactory } from './core/pipe/validation-exception.factory.js'
|
||||||
|
import { RedisModule } from './core/redis/redis.module.js'
|
||||||
|
import { ThrottlerModule } from './core/throttler/throttler.module.js'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
|
LoggerModule,
|
||||||
|
DbModule,
|
||||||
|
RedisModule,
|
||||||
|
ThrottlerModule,
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
global: true,
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (config: ConfigService) => ({
|
||||||
|
secret: config.getOrThrow<string>('JWT_SECRET'),
|
||||||
|
signOptions: { expiresIn: config.get<string>('JWT_EXPIRES_IN', '7d') as `${number}${'s' | 'm' | 'h' | 'd' | 'w' | 'y'}` },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [
|
||||||
|
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
|
||||||
|
{ provide: APP_INTERCEPTOR, useClass: TransformInterceptor },
|
||||||
|
{
|
||||||
|
provide: APP_PIPE,
|
||||||
|
useValue: new ValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
forbidNonWhitelisted: true,
|
||||||
|
transform: true,
|
||||||
|
exceptionFactory: validationExceptionFactory,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{ provide: APP_GUARD, useClass: ThrottlerGuard },
|
||||||
|
{ provide: APP_GUARD, useClass: JwtAuthGuard },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
||||||
4
src/common/decorator/public.decorator.ts
Normal file
4
src/common/decorator/public.decorator.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common'
|
||||||
|
|
||||||
|
export const IS_PUBLIC_KEY = 'isPublic'
|
||||||
|
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true)
|
||||||
1
src/common/token/tokens.ts
Normal file
1
src/common/token/tokens.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const REDIS_CLIENT = 'REDIS_CLIENT';
|
||||||
5
src/common/type/jwt-payload.ts
Normal file
5
src/common/type/jwt-payload.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface JwtPayload {
|
||||||
|
sub: number
|
||||||
|
iat?: number
|
||||||
|
exp?: number
|
||||||
|
}
|
||||||
5
src/common/type/response.ts
Normal file
5
src/common/type/response.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface ApiResponse<T = unknown> {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data: T | null;
|
||||||
|
}
|
||||||
22
src/core/db/db.module.ts
Normal file
22
src/core/db/db.module.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forRootAsync({
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (config: ConfigService) => ({
|
||||||
|
type: 'postgres',
|
||||||
|
host: config.getOrThrow<string>('DB_HOST'),
|
||||||
|
port: config.getOrThrow<number>('DB_PORT'),
|
||||||
|
username: config.getOrThrow<string>('DB_USER'),
|
||||||
|
password: config.getOrThrow<string>('DB_PASSWORD'),
|
||||||
|
database: config.getOrThrow<string>('DB_NAME'),
|
||||||
|
autoLoadEntities: true,
|
||||||
|
synchronize: config.get<string>('NODE_ENV') !== 'production',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class DbModule {}
|
||||||
25
src/core/filter/http-exception.filter.ts
Normal file
25
src/core/filter/http-exception.filter.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import type { ApiResponse } from '../../common/type/response.js';
|
||||||
|
|
||||||
|
@Catch()
|
||||||
|
export class HttpExceptionFilter implements ExceptionFilter {
|
||||||
|
catch(exception: unknown, host: ArgumentsHost): void {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
const request = ctx.getRequest<Request>();
|
||||||
|
|
||||||
|
const isHttp = exception instanceof HttpException;
|
||||||
|
const status = isHttp ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
|
||||||
|
let message = 'Internal server error';
|
||||||
|
if (isHttp) {
|
||||||
|
const body = exception.getResponse();
|
||||||
|
message = typeof body === 'string' ? body : ((body as { message?: string }).message ?? exception.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body: ApiResponse<null> = { code: status, message, data: null };
|
||||||
|
|
||||||
|
response.status(status).json(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/core/guard/jwt.guard.ts
Normal file
41
src/core/guard/jwt.guard.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'
|
||||||
|
import { Reflector } from '@nestjs/core'
|
||||||
|
import { JwtService } from '@nestjs/jwt'
|
||||||
|
import type { Request } from 'express'
|
||||||
|
import { IS_PUBLIC_KEY } from '../../common/decorator/public.decorator.js'
|
||||||
|
import type { JwtPayload } from '../../common/type/jwt-payload.js'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private readonly jwtService: JwtService,
|
||||||
|
private readonly reflector: Reflector,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
])
|
||||||
|
|
||||||
|
if (isPublic) return true
|
||||||
|
|
||||||
|
const request = context.switchToHttp().getRequest<Request & { user: JwtPayload }>()
|
||||||
|
const token = this.extractToken(request)
|
||||||
|
|
||||||
|
if (!token) throw new UnauthorizedException()
|
||||||
|
|
||||||
|
try {
|
||||||
|
request.user = this.jwtService.verify<JwtPayload>(token)
|
||||||
|
} catch {
|
||||||
|
throw new UnauthorizedException()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractToken(request: Request): string | undefined {
|
||||||
|
const [type, token] = request.headers.authorization?.split(' ') ?? []
|
||||||
|
return type === 'Bearer' ? token : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/core/interceptor/transform.interceptor.ts
Normal file
17
src/core/interceptor/transform.interceptor.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import type { ApiResponse } from '../../common/type/response.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
|
||||||
|
intercept(_context: ExecutionContext, next: CallHandler<T>): Observable<ApiResponse<T>> {
|
||||||
|
return next.handle().pipe(
|
||||||
|
map((data) => ({
|
||||||
|
code: 200,
|
||||||
|
message: 'ok',
|
||||||
|
data: data ?? null,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/core/logger/logger.module.ts
Normal file
78
src/core/logger/logger.module.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
import { LoggerModule as PinoLoggerModule } from 'nestjs-pino'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
PinoLoggerModule.forRootAsync({
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (config: ConfigService) => {
|
||||||
|
const isProd = config.get<string>('NODE_ENV') === 'production'
|
||||||
|
|
||||||
|
return {
|
||||||
|
pinoHttp: {
|
||||||
|
level: isProd ? 'info' : 'debug',
|
||||||
|
serializers: {
|
||||||
|
req: (req: {
|
||||||
|
id: string
|
||||||
|
method: string
|
||||||
|
url: string
|
||||||
|
query: Record<string, unknown>
|
||||||
|
params: Record<string, unknown>
|
||||||
|
headers: Record<string, string>
|
||||||
|
socket: { remoteAddress?: string }
|
||||||
|
}) => ({
|
||||||
|
id: req.id,
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
query: req.query,
|
||||||
|
params: req.params,
|
||||||
|
ip: req.headers['x-forwarded-for'] ?? req.socket?.remoteAddress,
|
||||||
|
userAgent: req.headers['user-agent'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
transport: isProd
|
||||||
|
? {
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
target: 'pino-roll',
|
||||||
|
level: 'info',
|
||||||
|
options: {
|
||||||
|
file: join('logs', 'app'),
|
||||||
|
frequency: 'daily',
|
||||||
|
dateFormat: 'yyyy-MM-dd',
|
||||||
|
extension: '.log',
|
||||||
|
limit: { count: 14 },
|
||||||
|
mkdir: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
target: 'pino-roll',
|
||||||
|
level: 'error',
|
||||||
|
options: {
|
||||||
|
file: join('logs', 'error'),
|
||||||
|
frequency: 'daily',
|
||||||
|
dateFormat: 'yyyy-MM-dd',
|
||||||
|
extension: '.log',
|
||||||
|
limit: { count: 30 },
|
||||||
|
mkdir: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
target: 'pino-pretty',
|
||||||
|
options: {
|
||||||
|
colorize: true,
|
||||||
|
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss',
|
||||||
|
ignore: 'pid,hostname',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class LoggerModule {}
|
||||||
14
src/core/pipe/validation-exception.factory.ts
Normal file
14
src/core/pipe/validation-exception.factory.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common'
|
||||||
|
import type { ValidationError } from 'class-validator'
|
||||||
|
|
||||||
|
export function validationExceptionFactory(errors: ValidationError[]): BadRequestException {
|
||||||
|
const forbidden = errors.filter((e) => e.constraints?.whitelistValidation)
|
||||||
|
|
||||||
|
if (forbidden.length > 0) {
|
||||||
|
const fields = forbidden.map((e) => e.property).join(', ')
|
||||||
|
return new BadRequestException(`不允許的欄位:${fields}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = errors.flatMap((e) => Object.values(e.constraints ?? {}))
|
||||||
|
return new BadRequestException(messages.join('; '))
|
||||||
|
}
|
||||||
22
src/core/redis/redis.module.ts
Normal file
22
src/core/redis/redis.module.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
import { Redis } from 'ioredis'
|
||||||
|
import { REDIS_CLIENT } from '../../common/token/tokens.js'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: REDIS_CLIENT,
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (config: ConfigService) =>
|
||||||
|
new Redis({
|
||||||
|
host: config.getOrThrow<string>('REDIS_HOST'),
|
||||||
|
port: config.getOrThrow<number>('REDIS_PORT'),
|
||||||
|
username: config.get<string>('REDIS_USER'),
|
||||||
|
password: config.get<string>('REDIS_PASSWORD'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: [REDIS_CLIENT],
|
||||||
|
})
|
||||||
|
export class RedisModule { }
|
||||||
20
src/core/throttler/throttler.module.ts
Normal file
20
src/core/throttler/throttler.module.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Module } from '@nestjs/common'
|
||||||
|
import { ConfigService } from '@nestjs/config'
|
||||||
|
import { ThrottlerModule as NestThrottlerModule } from '@nestjs/throttler'
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
NestThrottlerModule.forRootAsync({
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (config: ConfigService) => ({
|
||||||
|
throttlers: [
|
||||||
|
{
|
||||||
|
ttl: config.getOrThrow<number>('THROTTLE_TTL'),
|
||||||
|
limit: config.getOrThrow<number>('THROTTLE_LIMIT'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class ThrottlerModule {}
|
||||||
34
src/main.ts
Normal file
34
src/main.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
|
||||||
|
import { NestFactory } from '@nestjs/core'
|
||||||
|
import helmet from 'helmet'
|
||||||
|
import { Logger } from 'nestjs-pino'
|
||||||
|
import { AppModule } from './app.module.js'
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule, { bufferLogs: true })
|
||||||
|
app.useLogger(app.get(Logger))
|
||||||
|
app.enableShutdownHooks()
|
||||||
|
app.setGlobalPrefix('api')
|
||||||
|
|
||||||
|
app.use(helmet())
|
||||||
|
|
||||||
|
app.enableCors({
|
||||||
|
origin: process.env.CORS_ORIGINS?.split(',').map((o) => o.trim()) ?? [],
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
||||||
|
credentials: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
const config = new DocumentBuilder()
|
||||||
|
.setTitle('API')
|
||||||
|
.setDescription('API 文件')
|
||||||
|
.setVersion('1.0')
|
||||||
|
.addBearerAuth()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
SwaggerModule.setup('api/docs', app, SwaggerModule.createDocument(app, config))
|
||||||
|
}
|
||||||
|
|
||||||
|
await app.listen(process.env.PORT ?? 3000)
|
||||||
|
}
|
||||||
|
bootstrap()
|
||||||
0
src/module/.gitkeep
Normal file
0
src/module/.gitkeep
Normal file
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2023",
|
||||||
|
"sourceMap": true,
|
||||||
|
"types": ["node","jest"],
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user