ExpressJS (TypeScript) – 部署與環境分離
Production vs Development 設定
簡介
在開發 ExpressJS 應用時,我們常會把程式碼直接跑在本機環境,然後把同樣的程式碼直接部署到正式機器。這種做法雖然簡單,但 開發環境 與 正式環境 的需求差異很大:開發時需要大量除錯資訊、熱重載與寬鬆的錯誤容忍;正式環境則要求效能、資源安全與穩定性。若不把兩者的設定分離,會導致:
- 效能浪費:開發時才需要的日誌、錯誤堆疊在正式環境仍被輸出,浪費 I/O 與 CPU。
- 安全隱憂:開發環境常把敏感資訊(如測試 API 金鑰)寫死在程式碼,正式環境若未切換就會外洩。
- 維護困難:同一份程式碼同時兼顧兩種需求,會讓設定變得雜亂不易追蹤。
因此,將設定依據 NODE_ENV(或自訂環境變數)切換,是每個想要把 ExpressJS 運行在生產環境的開發者必備的基礎功。本文將以 TypeScript 為例,說明如何在不同環境間切換設定,並提供實作範例、常見陷阱與最佳實踐,讓你在部署時更得心應手。
核心概念
1. 為什麼要使用 NODE_ENV?
NODE_ENV 是 Node.js 社群慣用的環境變數,約定俗成的值有:
| 值 | 意義 |
|---|---|
development |
本機開發、開啟除錯資訊 |
production |
正式部署、關閉除錯、開啟效能優化 |
test |
單元/整合測試專用 |
在程式碼中只要判斷 process.env.NODE_ENV,即可決定要載入哪套設定檔或啟用哪些中介軟體(middleware)。
2. 使用 .env 檔案管理環境變數
dotenv 套件可以把 .env 檔中的 KEY=VALUE 讀入 process.env,讓設定與程式碼分離。建議在根目錄放置:
.env.development
.env.production
.env.test
並在啟動指令中指定要載入哪個檔案,例如:
// package.json scripts
{
"scripts": {
"dev": "cross-env NODE_ENV=development ts-node-dev src/index.ts",
"start": "cross-env NODE_ENV=production node dist/index.js",
"test": "cross-env NODE_ENV=test jest"
}
}
⚠️ 注意:千萬不要把
.env.*檔案直接提交到 Git,應在.gitignore中排除,並使用 GitHub Secrets 或 CI/CD 工具注入正式環境變數。
3. 設定檔的結構化
將設定抽成 config 目錄,依環境分別匯出設定物件,最終再由一個 index.ts 統一載入:
src/
└─ config/
├─ default.ts
├─ development.ts
├─ production.ts
└─ index.ts
default.ts
// src/config/default.ts
export default {
port: 3000,
corsOrigin: "*",
logLevel: "info",
};
development.ts
// src/config/development.ts
import defaultConfig from "./default";
export default {
...defaultConfig,
corsOrigin: "http://localhost:3000",
logLevel: "debug",
enableMorgan: true, // 開啟 HTTP request logger
};
production.ts
// src/config/production.ts
import defaultConfig from "./default";
export default {
...defaultConfig,
corsOrigin: "https://myapp.com",
logLevel: "warn",
enableMorgan: false, // 關閉開發用 logger
enableCompression: true,
enableHelmet: true,
};
index.ts
// src/config/index.ts
import development from "./development";
import production from "./production";
import defaultConfig from "./default";
type Config = typeof defaultConfig;
let config: Config;
switch (process.env.NODE_ENV) {
case "production":
config = production;
break;
case "development":
config = development;
break;
default:
config = defaultConfig;
break;
}
export default config;
重點:所有設定都集中在
config,程式碼只需要import config from "./config",即可根據環境自動取得正確值。
4. 程式碼範例:依環境載入 Middleware
以下示範在 src/app.ts 中根據設定動態注入中介軟體。
// src/app.ts
import express, { Request, Response, NextFunction } from "express";
import cors from "cors";
import helmet from "helmet";
import compression from "compression";
import morgan from "morgan";
import config from "./config";
const app = express();
// 1️⃣ 基本設定
app.use(express.json());
app.use(cors({ origin: config.corsOrigin }));
// 2️⃣ 開發環境專屬:Morgan 日誌
if (config.enableMorgan) {
app.use(morgan("dev"));
}
// 3️⃣ 正式環境安全與效能:Helmet + Compression
if (config.enableHelmet) {
app.use(helmet());
}
if (config.enableCompression) {
app.use(compression());
}
// 4️⃣ 範例路由
app.get("/", (req: Request, res: Response) => {
res.send("Hello Express with TypeScript!");
});
// 5️⃣ 錯誤處理(依環境調整回傳訊息)
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
const status = (err as any).status || 500;
const message =
process.env.NODE_ENV === "production"
? "Internal Server Error"
: err.message;
res.status(status).json({ error: message });
});
export default app;
5. 程式碼範例:使用 dotenv 讀入環境變數
// src/server.ts
import "dotenv/config"; // 會自動載入根目錄的 .env
import http from "http";
import app from "./app";
import config from "./config";
const server = http.createServer(app);
server.listen(config.port, () => {
console.log(
`[${process.env.NODE_ENV?.toUpperCase() || "UNKNOWN"}] Server listening on port ${config.port}`
);
});
說明:
dotenv/config會根據NODE_ENV自動尋找.env.{NODE_ENV},若找不到則載入根目錄的.env。
6. 程式碼範例:在 Docker 中切換環境
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --only=production
# 設定環境變數
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/server.js"]
在 docker-compose.yml 中可為開發環境覆寫:
version: "3.9"
services:
api:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
volumes:
- .:/app
- /app/node_modules
command: npm run dev
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方案 / 最佳實踐 |
|---|---|---|
忘記在正式環境設定 NODE_ENV=production |
Express 仍會以開發模式啟動(例如 trust proxy 為 false、view cache 為 false),導致效能下降。 |
在 Dockerfile、PM2、systemd 或 CI/CD pipeline 中明確設定 NODE_ENV=production。 |
| 在程式碼中硬編碼敏感資訊(如 DB 密碼) | 正式環境的程式碼洩漏會造成資安風險。 | 所有機密資訊皆放在環境變數或密鑰管理服務(AWS Secrets Manager、Azure Key Vault)。 |
在 .env.production 中留下測試用設定 |
測試資料不小心寫入正式資料庫。 | 使用 不同的資料庫帳號、不同的資料庫,在 CI/CD 中嚴格檢查環境檔內容。 |
| 開發用 logger 未關閉 | 大量日誌寫入磁碟或 CloudWatch,增加成本。 | 依 config.enableMorgan 控制 morgan,或在 production 設定 logLevel: "warn"。 |
未啟用 helmet 或 compression |
缺少安全標頭、回應體積過大。 | 在 production 設定 enableHelmet、enableCompression 為 true。 |
| 在測試環境仍使用真實外部服務 | 測試不穩定、成本上升。 | 使用 nock、msw 或 mock 服務,並在 test 設定檔中關閉外部連線。 |
最佳實踐總結:
- 環境變數永遠外部化:
.env.*、CI/CD、K8s ConfigMap/Secret。 - 設定檔模組化:
default+environment-specific,避免重複。 - 中介軟體依環境載入:只在需要的環境啟用。
- 錯誤訊息隱藏:正式環境只回傳通用訊息,避免資訊外泄。
- 自動化測試:在
test環境驗證所有設定切換正確。
實際應用場景
1. 多租戶 SaaS 平台的藍綠部署
在藍綠部署(Blue‑Green Deployment)中,兩套相同的服務同時運行,分別指向不同的環境變數(例如不同的資料庫連線)。只要在 docker-compose 或 Kubernetes 的 ConfigMap 中切換 NODE_ENV,即可讓新版本自動使用 production 設定,而舊版本仍保留 development 設定供測試。
2. 需要即時偵錯的 Staging 環境
Staging 常需要 開發者除錯,但又不希望影響正式流量。可以在 staging 環境自訂一個 NODE_ENV=staging,在 config 中繼承 production 再加上 enableMorgan: true、logLevel: "debug",即兼顧效能與除錯。
3. 伺服器less(如 Vercel、Netlify)上部署 Express
這類平台會自動設定 NODE_ENV=production,但仍可在 vercel.json 中加入自訂環境變數,並在程式碼中使用 process.env.VERCEL_URL 來動態決定 CORS 設定,確保開發與正式環境的差異不會造成跨域錯誤。
總結
- 環境分離 是提升 ExpressJS(配合 TypeScript)應用可維護性與安全性的關鍵。
- 透過
NODE_ENV、.env、模組化設定檔,我們可以在同一套程式碼中自動切換 開發、測試 與 正式 的行為。 - 依環境載入 logger、helmet、compression 等中介軟體,讓開發時資訊完整、正式時效能與安全兼備。
- 注意 敏感資訊外部化、錯誤訊息遮蔽,以及 Docker/K8s 中的環境變數設定,避免常見的資安與效能陷阱。
只要掌握以上概念與實作範例,你就能在任何部署平台上,快速切換環境、保持程式碼乾淨,並確保 Production 具備最佳的效能與安全性。祝開發順利,部署無慮!