讓錯誤不可能被忽略
好的 API 設計讓正確用法是最簡單的路徑,錯誤用法很難不小心就觸發:
// ❌ 容易忘記 await
userService.create(dto); // 誰知道這個要不要 await?
// ✅ 讓型別系統幫你:永遠 return Promise,讓 caller 必須決定
async create(dto: CreateUserDto): Promise<User> {
// ...
}使用 Result<T, E> 型別(或 Go-style [data, error] tuple)讓錯誤路徑變成必須處理的:
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function parseConfig(raw: unknown): Promise<Result<Config>> {
const result = configSchema.safeParse(raw);
if (!result.success) return { ok: false, error: new Error(result.error.message) };
return { ok: true, value: result.data };
}
// caller 必須 check ok 才能用 value,不能忽略錯誤
const result = await parseConfig(raw);
if (!result.ok) {
logger.error('Config parse failed', { error: result.error });
process.exit(1);
}把依賴注入,不要在內部 new
// ❌ UserService 自己建立 dependencies,測試很難 mock
class UserService {
private repo = new UserRepository();
private emailer = new EmailService();
}
// ✅ 依賴從外部注入,測試時可以換掉
class UserService {
constructor(
private repo: UserRepository,
private emailer: EmailService,
) {}
}這不是為了框架——是為了讓 unit test 不需要啟動 DB 或打外部 API。
型別說話,不要用 string 代表一切
// ❌ string 可以是任何東西
function sendEmail(to: string, type: string) { }
sendEmail('alice@example.com', 'welcme'); // 打錯了,runtime 才發現
// ✅ union type 讓 compiler 幫你檢查
type EmailType = 'welcome' | 'password-reset' | 'order-confirmation';
function sendEmail(to: string, type: EmailType) { }
sendEmail('alice@example.com', 'welcme'); // compile error,立刻發現同樣適用於 status、role、event type——這些有限集合的值都應該是 union type 或 enum,不是 string。
一個函式做一件事,名字說清楚
// ❌ 名字說一件事,但做了三件事
async function validateAndSaveUser(dto: CreateUserDto) {
const errors = validate(dto);
if (errors.length) throw new ValidationError(errors);
const user = await userRepo.create(dto);
await emailService.sendWelcome(user.email);
return user;
}
// ✅ 分開,每個函式名字就是它做的事
async function createUser(dto: CreateUserDto) {
const user = await userRepo.create(dto);
await emailQueue.add('welcome-email', { userId: user.id });
return user;
}
// validation 在 middleware 做,不混進 service如果一個函式名字裡有 “and” 或 “then”,通常是一個信號:它在做兩件事。
讓 Guard/Validation 失敗的越早越好(Fail Fast)
// ❌ 做了一堆工作,最後才驗證
async function processOrder(dto: CreateOrderDto) {
const inventory = await inventoryService.check(dto.productId);
const pricing = await pricingService.calculate(dto);
const user = await userService.get(dto.userId);
// 最後才驗 dto,前面三個 await 都白做了
const errors = orderSchema.safeParse(dto);
if (!errors.success) throw new ValidationError(errors.error);
}
// ✅ 先驗資料格式,合法的才繼續
async function processOrder(dto: CreateOrderDto) {
orderSchema.parse(dto); // 先 validate,不合法直接 throw
// 以下才做有成本的操作
const inventory = await inventoryService.check(dto.productId);
// ...
}Fail fast 的原則:一旦知道這個 request 處理不了,立刻回傳——不要繼續做後面的操作。
每個 public method 都有一個清楚的「前置條件」
class OrderService {
// 前置條件隱含在型別系統:user 是已認證的 AuthUser,不是可能為 undefined 的東西
async create(dto: CreateOrderDto, user: AuthUser): Promise<Order> {
// 不需要 if (!user) throw,型別保證了 user 存在
}
}把條件推進型別,比在函式內部 guard 更省力。null / undefined 只有在系統邊界(external data)才需要出現,內部傳遞應該已經是確定的型別。
Response 只暴露必要的欄位
不要用 entity 直接當 response,要過 serializer:
class UserSerializer {
static toPublicResponse(user: User) {
return {
id: user.id,
name: user.name,
createdAt: user.createdAt,
};
}
static toAdminResponse(user: User) {
return {
...this.toPublicResponse(user),
email: user.email,
role: user.role,
lastLoginAt: user.lastLoginAt,
};
}
}DB schema 和 API contract 獨立演進,DB 加新欄位不會自動洩漏出去。
寫有意義的 log,而不是 console.log
// ❌ 完全沒用
console.log('user created');
console.log(error);
// ✅ 有 context,能 debug
logger.info('User created', {
userId: user.id,
email: user.email,
requestId: getRequestId(),
});
logger.error('Payment failed', {
error: error.message,
stack: error.stack,
userId,
amount,
provider: 'stripe',
});log 的讀者是未來的你在凌晨兩點排查 incident。讓他能從一條 log 知道發生什麼事、在哪裡發生、影響誰。
Transaction scope 由 Service 決定
Repository 提供操作,但 transaction 的邊界由 Service 控制——因為只有 Service 知道哪些操作要 atomic:
class OrderService {
async create(dto: CreateOrderDto, userId: string): Promise<Order> {
return db.transaction(async (trx) => {
const order = await orderRepo.create(dto, userId, trx);
await inventoryRepo.decrement(dto.productId, dto.quantity, trx);
// 兩個操作在同一個 transaction
return order;
});
}
}Repository 不應該開 transaction——它不知道自己是不是更大 transaction 的一部分。
配置驗證在啟動時做
// ✅ 啟動時驗證所有必要配置,缺少就不啟動
const config = z.object({
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
REDIS_URL: z.string().url(),
}).parse(process.env);
// 如果 DATABASE_URL 沒設定,process.exit(1),不要讓服務跑起來卻在 runtime 才掛在啟動時 fail 比在 runtime 才 fail 好:deploy 的時候就發現配置問題,不是用戶用到一半才噴。
冪等操作要明確設計,不是靠運氣
任何可能被重複觸發的操作(webhook、queue retry、用戶重複點擊),都要主動設計冪等性:
// 用資料庫的唯一約束作為最後防線
CREATE UNIQUE INDEX ON orders (idempotency_key);
// 應用層也要做 check
async createOrder(dto: CreateOrderDto, idempotencyKey: string): Promise<Order> {
const existing = await Order.findOne({ where: { idempotencyKey } });
if (existing) return existing; // 回傳既有的,不重複建立
// ...
}詳見 Idempotency 設計。
重的外部操作要有 timeout 和 fallback
// ❌ 外部 API 沒回應,你的 request 就卡住等
const result = await externalApi.call(data);
// ✅ 設 timeout,失敗有 fallback
try {
const result = await externalApi.call(data, {
signal: AbortSignal.timeout(5000), // 5 秒 timeout
});
return result;
} catch (error) {
if (error.name === 'TimeoutError') {
logger.warn('External API timeout', { api: 'externalApi' });
return fallbackValue; // 或 throw,讓 caller 決定
}
throw error;
}第三方服務可以隨時變慢或掛掉。你的系統不能因為別人慢就跟著掛。
