ExpressJS (TypeScript) – Request 與 Response 操作:設定 Headers
簡介
在 Web 開發中,HTTP Header 是客戶端與伺服器之間傳遞額外資訊的關鍵橋樑。
無論是告訴瀏覽器回傳資料的類型、設定快取策略,或是實作跨來源資源共享(CORS),正確且安全地操作 Header 都是打造高效 API 的基礎。
本篇文章以 ExpressJS + TypeScript 為例,說明在 Request 與 Response 物件上如何設定、讀取與管理 Header,提供實務範例、常見陷阱與最佳實踐,幫助從初學者到中級開發者快速上手並寫出更安全、可維護的程式碼。
核心概念
1. Header 的基本特性
| 特性 | 說明 |
|---|---|
| 大小寫不敏感 | Content-Type、content-type、CONTENT-TYPE 都是同一個欄位。 |
| 可重複 | 同名 Header 可以出現多次(如 Set-Cookie),Express 會把它們合併為陣列。 |
| 傳輸限制 | Header 的總長度受瀏覽器與伺服器限制(通常在 8KB 左右),過長會導致 431 Request Header Fields Too Large 錯誤。 |
2. Express 中的 Header API
| 方法 | 位置 | 說明 |
|---|---|---|
req.get(name) / req.header(name) |
Request | 取得單一 Header 的值(不區分大小寫)。 |
req.headers |
Request | 取得所有 Header 的物件(型別為 IncomingHttpHeaders)。 |
res.set(name, value) / res.header(name, value) |
Response | 設定或覆寫 Header。 |
res.append(name, value) |
Response | 在同名 Header 後追加值(常用於 Set-Cookie)。 |
res.type(mime) |
Response | 快速設定 Content-Type(如 res.type('json'))。 |
res.removeHeader(name) |
Response | 移除已設定的 Header(Node 原生 API)。 |
Tip:在 TypeScript 中,
req.get會回傳string | undefined,建議使用空值合併(??)或型別斷言。
3. 為什麼要在 TypeScript 中明確宣告 Header?
使用 TypeScript 可以讓我們:
- 避免拼字錯誤:
res.set('Conten-Type', ...)會在編譯期提示Argument of type '"Conten-Type"' is not assignable...。 - 取得自動補全:IDE 會根據
http標準提供常見 Header 列表。 - 確保值的型別:例如
Cache-Control必須是字串,res.set('Cache-Control', 3600)會直接報錯。
程式碼範例
以下範例均以 Express 5.x + TypeScript 為前提,假設已安裝 express、@types/express、typescript,並在 src/app.ts 中編寫。
1️⃣ 基本的 Response Header 設定
// src/app.ts
import express, { Request, Response } from 'express';
const app = express();
app.get('/hello', (req: Request, res: Response) => {
// 設定 Content-Type 為 JSON
res.type('json'); // 等同於 res.set('Content-Type', 'application/json')
// 設定自訂 Header
res.set('X-Powered-By', 'ExpressTS'); // 建議使用自訂前綴避免與標準 Header 混淆
// 回傳資料
res.send({ message: 'Hello, World!' });
});
說明:
res.type('json')會自動把 MIME type 設為application/json; charset=utf-8,省去手寫字串的麻煩。
2️⃣ 讀取 Request Header 並依條件回應
app.get('/auth', (req: Request, res: Response) => {
// 從 Authorization Header 取得 Bearer Token
const authHeader = req.get('Authorization'); // 可能回傳 undefined
const token = authHeader?.split(' ')[1];
if (!token) {
// 若沒有 token,回傳 401 並設定 WWW-Authenticate
res.set('WWW-Authenticate', 'Bearer realm="Access to the protected resource"');
return res.status(401).send('Missing token');
}
// 假設驗證成功
res.send(`Your token is ${token}`);
});
技巧:使用
?.(optional chaining) 可以安全取得 token,避免null或undefined的錯誤。
3️⃣ 設定快取相關 Header(Cache-Control、ETag)
app.get('/static/:file', (req: Request, res: Response) => {
const { file } = req.params;
// 假設檔案位於 public 資料夾
const filePath = `${__dirname}/public/${file}`;
// 設定快取 1 小時
res.set('Cache-Control', 'public, max-age=3600');
// 設定 ETag(Express 會自動產生,但示範手動設定)
const etag = `"${Date.now().toString(16)}-${file.length}"`;
res.set('ETag', etag);
// 若客戶端送來 If-None-Match,直接回 304
if (req.get('If-None-Match') === etag) {
return res.status(304).end();
}
res.sendFile(filePath);
});
重點:
Cache-Control能大幅減少伺服器負載;ETag搭配If-None-Match能讓瀏覽器只在內容變更時才重新下載。
4️⃣ 使用 res.append 設定多個 Set-Cookie
app.post('/login', (req: Request, res: Response) => {
// 假設驗證成功後產生 JWT
const accessToken = 'jwt-access-token';
const refreshToken = 'jwt-refresh-token';
// 使用 append 讓 Set-Cookie 可以同時出現多次
res.append('Set-Cookie', `access=${accessToken}; HttpOnly; Path=/; Max-Age=3600`);
res.append('Set-Cookie', `refresh=${refreshToken}; HttpOnly; Path=/refresh; Max-Age=604800`);
res.send({ status: 'ok' });
});
說明:若直接使用
res.set('Set-Cookie', ...),後一次會覆寫前一次。append能保留多個 cookie。
5️⃣ CORS 與安全 Header(使用 cors 與 helmet)
import cors from 'cors';
import helmet from 'helmet';
// 只允許特定前端來源
const corsOptions: cors.CorsOptions = {
origin: ['https://myfrontend.com', 'https://admin.myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true, // 允許傳送 Cookie
};
app.use(cors(corsOptions)); // 自動設定 Access-Control-* Header
app.use(helmet()); // 設定 X-Content-Type-Options、X-Frame-Options 等安全 Header
app.get('/data', (req: Request, res: Response) => {
res.json({ secret: 'only for allowed origins' });
});
最佳實踐:絕不要在正式環境使用
origin: '*',這會讓任意網站都能讀取你的 API,容易造成資料外洩。
常見陷阱與最佳實踐
| 陷阱 | 影響 | 解決方案 / 最佳實踐 |
|---|---|---|
| 重複設定同名 Header | 後一次會覆寫前一次,導致 Set-Cookie、Cache-Control 等失效。 |
使用 res.append 或先取得舊值再合併 (res.get('Header')?.split(', ')). |
| 大小寫不一致 | 雖然大小寫不敏感,但在程式碼中混用會降低可讀性。 | 統一使用 Pascal-Case(如 Content-Type)或全小寫,並在 IDE 中啟用自動補全。 |
| 過長的 Header | 超過伺服器或瀏覽器限制會回傳 431 錯誤。 | 盡量簡潔,將大量資料搬到 Body(如 JSON)或使用壓縮。 |
| 安全資訊外洩 | X-Powered-By、Server 等標頭會透露技術棧。 |
使用 helmet.hidePoweredBy() 或自行移除 (app.disable('x-powered-by'))。 |
| CORS 設定過寬 | Access-Control-Allow-Origin: * 讓任何網站都能呼叫 API。 |
只允許可信任的 origin,並在生產環境使用白名單。 |
| 未正確處理預檢請求 (OPTIONS) | 前端發出跨域請求時會先送 OPTIONS,若未回應會導致請求失敗。 |
使用 cors() 中介軟體自動回應,或自行在路由中處理 app.options('*', cors(corsOptions))。 |
其他實用技巧
- 使用型別別名:
type HeaderMap = Record<string, string | number | string[]>;,讓自訂 Header 的傳遞更安全。 - 集中管理 Header 常數:在
src/constants/headers.ts中定義export const HEADER_X_REQUEST_ID = 'X-Request-Id';,避免硬編碼。 - 在測試時驗證 Header:使用
supertest搭配expect('Header-Name', /value/)確保回傳正確。
實際應用場景
API 版本管理
在回應中加入API-Version或X-API-Version,讓前端可以根據版本做相容性處理。分頁與排序資訊
透過X-Total-Count、LinkHeader(RFC 5988)傳遞分頁總筆數與前後頁 URL,減少 body 內的冗餘資料。檔案下載與內容處理
使用Content-Disposition: attachment; filename="report.pdf"指示瀏覽器下載檔案,搭配Content-Type設定正確 MIME。服務間追蹤 (Tracing)
在微服務架構下,將X-Request-Id、X-Trace-Id於每次請求傳遞,方便在日誌與 APM 系統中串聯。安全防護
Strict-Transport-Security(HSTS)強制 HTTPS。X-Content-Type-Options: nosniff防止瀏覽器 MIME Sniffing。X-Frame-Options: DENY防止 Clickjacking。
總結
設定 HTTP Header 看似簡單,卻是 Web API 安全、效能與可維護性 的根本。透過 ExpressJS + TypeScript,我們可以:
- 安全地讀寫 Header:利用
req.get、res.set、res.append等 API,配合 TypeScript 型別保證正確性。 - 掌握快取與跨域:使用
Cache-Control、ETag、cors、helmet等標頭,提升使用者體驗與資安防護。 - 避免常見坑:統一命名、避免重複、控制長度、嚴格 CORS 白名單,讓應用更穩定。
只要把 Header 的設計 當成 API 合約的一部份,並遵循上述最佳實踐,就能寫出 高效、可追蹤、且安全 的 Express + TypeScript 伺服器。祝開發順利,Happy Coding! 🚀