Anti-pattern 一:Controller 塞商業邏輯
這是最常見的問題,在任何框架都會出現:
// ❌ 錯誤:Controller 變成上帝 class
class UserController {
async store(req: Request, res: Response) {
const { name, email, password } = req.body;
// 業務規則直接在 Controller
const existing = await db.query('SELECT * FROM users WHERE email = ?', [email]);
if (existing.length > 0) {
return res.status(409).json({ error: 'Email already exists' });
}
const hashedPassword = await bcrypt.hash(password, 10);
// 直接操作 DB
const [id] = await db.query(
'INSERT INTO users (name, email, password) VALUES (?, ?, ?)',
[name, email, hashedPassword]
);
// 發送 email 也在這裡
await sendWelcomeEmail(email, name);
// 更新快取也在這裡
await redis.del(`user:${id}`);
res.status(201).json({ id, name, email });
}
}這個 Controller 做了五件事:驗重複 email、hash 密碼、插入 DB、發 email、清 cache。
為什麼這是問題:
- 無法 unit test(測試要模擬 DB、Redis、Email 三個外部依賴)
- 複用不可能(另一個地方也要「建立 user」就要複製貼上)
- 改一個步驟的邏輯,要在 Controller 裡找到對的地方
// ✅ 正確:Controller 只做 HTTP 的事
class UserController {
constructor(private userService: UserService) {}
async store(req: Request, res: Response) {
const dto = this.validate(req); // 驗 HTTP input
const user = await this.userService.create(dto); // 業務邏輯在 Service
res.status(201).json(user);
}
}Controller 的工作只有兩件:解析 HTTP input、呼叫 Service、格式化 HTTP response。所有業務邏輯在 Service 層。
Anti-pattern 二:DI Container 當 Service Locator
Service Locator 是一個反 DI 的 pattern——不是「依賴被注入進來」,而是「需要的時候主動去容器裡拿」:
// ❌ Service Locator:主動去 container 拿
class UserService {
async create(dto: CreateUserDto) {
const emailService = Container.get(EmailService); // ← 主動取
const cacheService = Container.get(CacheService); // ← 主動取
const user = await this.userRepo.create(dto);
await emailService.sendWelcome(user.email);
await cacheService.invalidate(`user:${user.id}`);
return user;
}
}這比真正的 DI 差在哪:
- 依賴不可見:看 class signature 不知道它需要哪些依賴
- 測試困難:要 mock
Container.get而不是直接注入 mock 物件 - 耦合 Container 實作:換 DI container 就要改所有地方
// ✅ 正確 DI:依賴在 constructor 宣告
class UserService {
constructor(
private userRepo: UserRepository,
private emailService: EmailService, // ← 依賴在 signature 清楚可見
private cacheService: CacheService,
) {}
}Anti-pattern 三:全部靠 Annotation Magic,看不見流程
NestJS / Spring 大量使用 decorator / annotation,這讓行為「神奇地發生了」但你不知道為什麼:
// ❌ Annotation 堆疊,流程不清楚
@Controller('users')
@UseGuards(JwtAuthGuard, RolesGuard)
@UseInterceptors(CacheInterceptor, LoggingInterceptor, TransformInterceptor)
@UsePipes(ValidationPipe)
export class UserController {
@Get(':id')
@Roles('admin', 'user')
@CacheKey('user')
@CacheTTL(60)
@HttpCode(200)
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.userService.findById(id);
}
}這個 endpoint 有 auth guard、roles check、cache、logging、response transform、validation、TTL——全部靠 annotation 堆疊。出了問題,你很難從這裡判斷「請求到底按什麼順序被哪些東西處理了」。
這不是說不要用 annotation,而是:annotation 應該讓意圖更清楚,不是讓行為更神秘。每加一個 annotation,問自己:「如果這個 annotation 消失了,會出什麼問題?這個問題顯而易見嗎?」
Anti-pattern 四:忽略 Graceful Shutdown
在 Kubernetes 裡滾動更新時,舊的 Pod 會收到 SIGTERM,然後有一段時間(terminationGracePeriodSeconds)讓它完成現有請求再退出。
如果你沒有處理 SIGTERM:
// ❌ 沒有 graceful shutdown
const server = app.listen(3000);
// K8s 送 SIGTERM → process 直接結束 → in-flight request 被切斷 → 用戶看到 502後果是每次部署都有一小段時間的 502 錯誤。
// ✅ 正確:處理 SIGTERM
const server = app.listen(3000);
process.on('SIGTERM', async () => {
server.close(async () => {
await db.close();
await redis.quit();
process.exit(0);
});
// 超時保護:25 秒後強制退出
setTimeout(() => process.exit(1), 25000);
});23. Lifecycle 與 Hook 有各框架的 graceful shutdown 實作對照。
Anti-pattern 五:Production 自動跑 Migration
在 app 啟動時自動執行 migration:
// ❌ 危險:app 啟動自動 migrate
async function bootstrap() {
await dataSource.runMigrations(); // ← 每次啟動都跑
await app.listen(3000);
}問題一:K8s 同時起多個 Pod,多個 Pod 同時執行 migration,race condition。
問題二:Migration 失敗讓 app 無法啟動,整個服務停掉。
問題三:無法做 rollback(migration 跑了就跑了)。
正確做法:Migration 是獨立的部署步驟,在 app 起動之前的 CI/CD pipeline 裡執行(K8s Job),而不是在 app 啟動時執行。
Anti-pattern 六:DB Connection Pool 設太大
// ❌ 把 pool size 設很大以為效能更好
const dataSource = new DataSource({
type: 'postgres',
poolSize: 100, // ← 100 個連線
});PostgreSQL 預設 max_connections 是 100。如果你起了 5 個 Pod,每個 pool 100 個連線,就是 500 個連線——超過 PostgreSQL 的上限,後面的連線請求全部排隊,整個 pool 卡死。
正確做法:pool size = max_connections / Pod 數量 × 0.8(留 20% buffer)。如果 PostgreSQL max_connections 是 100,你有 5 個 Pod,每個 pool 應該設 16 個連線左右。
