

บทนำ: ความท้าทายของ Business Continuity ในยุค TypeScript และ Zod
ในปี 2026 ระบบซอฟต์แวร์ที่ขับเคลื่อนธุรกิจต้องเผชิญกับความท้าทายที่ไม่เคยมีมาก่อน ความซับซ้อนของข้อมูลที่ไหลเวียนระหว่างระบบไมโครเซอร์วิส (Microservices), API ภายนอก, และฐานข้อมูลที่กระจายตัว ทำให้การรับประกันความต่อเนื่องทางธุรกิจ (Business Continuity) จำเป็นต้องมีเครื่องมือที่แข็งแกร่งกว่าเดิม TypeScript ซึ่งเป็นภาษาที่ได้รับความนิยมสูงสุดสำหรับการพัฒนาแอปพลิเคชันระดับองค์กร มาพร้อมกับระบบ Type System ที่ช่วยจับข้อผิดพลาดในเวลาคอมไพล์ แต่ในโลกแห่งความเป็นจริง ข้อมูลที่เข้ามาในระบบมักไม่เป็นไปตามที่ TypeScript คาดหวังเสมอไป
นี่คือจุดที่ Zod เข้ามามีบทบาทสำคัญ Zod เป็นไลบรารีสำหรับการประกาศ Schema และ Validation ข้อมูลที่ทำงานได้อย่างลงตัวกับ TypeScript แต่ปัญหาที่นักพัฒนาหลายคนมองข้ามคือ “Business Continuity” ของระบบ Validation เอง หาก Schema การตรวจสอบข้อมูลล้มเหลวเมื่อมีการเปลี่ยนแปลงโครงสร้างข้อมูลกะทันหัน (Schema Drift) หรือเมื่อระบบต้องทำงานต่อเนื่องตลอด 24/7 โดยไม่มี downtime ระบบทั้งองค์กรอาจหยุดชะงักได้
บทความนี้จะพาคุณไปสำรวจกลยุทธ์และแนวปฏิบัติที่ดีที่สุดในการใช้ Zod เพื่อสร้าง Business Continuity ที่แข็งแกร่งในระบบ TypeScript ครอบคลุมตั้งแต่การออกแบบ Schema ที่ยืดหยุ่น การจัดการข้อผิดพลาดแบบ Graceful Degradation ไปจนถึงการทำ Data Migration และ Versioning ที่ปลอดภัย โดยอ้างอิงจากประสบการณ์จริงของทีมพัฒนา SiamCafe ที่ต้องดูแลระบบหลังบ้านที่มีคำขอ API มากกว่า 50 ล้านครั้งต่อวัน
1. ทำความเข้าใจ Business Continuity ในบริบทของ Data Validation
ก่อนที่เราจะลงลึกในรายละเอียดทางเทคนิค เราต้องเข้าใจก่อนว่า “Business Continuity” ในมุมมองของ Data Validation หมายถึงอะไร ในระบบที่มีความสำคัญสูง (Mission-Critical Systems) การที่ Schema Validation ล้มเหลวไม่ได้หมายความว่าระบบควรหยุดทำงานทันที แต่หมายถึงระบบต้องสามารถ:
- ตรวจจับความผิดปกติของข้อมูล (Anomaly Detection) โดยไม่ทำให้ธุรกรรมหลักล้มเหลว
- Fallback ไปยังรูปแบบข้อมูลเก่า เมื่อพบว่า Schema ใหม่ไม่ถูกต้อง
- บันทึกและแจ้งเตือน (Logging & Alerting) เพื่อให้ทีมงานสามารถแก้ไขได้ทันที
- รองรับการเปลี่ยนแปลง Schema (Schema Evolution) โดยไม่ต้อง Deploy ใหม่ทั้งระบบ
1.1 สถานการณ์จริงที่ Business Continuity ล้มเหลว
ทีม SiamCafe เคยประสบปัญหาหนักเมื่อผู้ให้บริการ API ภายนอกเปลี่ยนโครงสร้างข้อมูลโดยไม่ได้แจ้งล่วงหน้า ฟิลด์ `user.address.zipcode` ที่เคยเป็น string กลายเป็น object ที่มีฟิลด์ `code` และ `extension` การเปลี่ยนแปลงนี้ทำให้ Zod Schema ที่เราเขียนไว้ตรวจสอบข้อมูลล้มเหลว ส่งผลให้ฟังก์ชันสร้างคำสั่งซื้อ (Order Creation) หยุดทำงานนานกว่า 30 นาที สร้างความเสียหายทางธุรกิจนับล้านบาท
บทเรียนสำคัญที่เราได้คือ: การ Validation ที่เข้มงวดเกินไป (Overly Strict Validation) โดยไม่มีกลไก Business Continuity อาจเป็นอันตรายต่อธุรกิจมากกว่าการไม่มี Validation เลย
2. การออกแบบ Zod Schema เพื่อความยืดหยุ่น (Resilient Schema Design)
หัวใจของ Business Continuity คือการออกแบบ Schema ที่สามารถรับมือกับการเปลี่ยนแปลงได้ โดยไม่ต้องเขียนโค้ดใหม่ทั้งหมด Zod มีเครื่องมือที่ทรงพลังสำหรับการสร้าง Schema แบบ “ยืดหยุ่น” หรือ “Adaptive Schema”
2.1 หลักการ Defensive Schema Design
หลักการสำคัญคือการสมมติว่าข้อมูลที่เข้ามาอาจไม่สมบูรณ์หรือผิดรูปแบบเสมอ (Assume the worst) แทนที่จะใช้ `z.object()` แบบตายตัว เราควรใช้เทคนิคดังนี้:
import { z } from 'zod';
// Schema ที่ยืดหยุ่น - รองรับการเปลี่ยนแปลง
const ResilientUserSchema = z.object({
id: z.union([z.string(), z.number()]), // รองรับทั้ง string และ number
name: z.string().min(1).catch('Unknown User'), // Fallback เมื่อข้อมูลผิด
email: z.string().email().nullable().default(null), // ค่า default เมื่อไม่มีข้อมูล
address: z.object({
street: z.string().optional(),
city: z.string().optional(),
zipcode: z.union([
z.string(),
z.object({
code: z.string().optional(),
extension: z.string().optional()
}).transform(val => {
// แปลง object zipcode กลับเป็น string
if (typeof val === 'object') {
return `${val.code || ''}-${val.extension || ''}`;
}
return val;
}).optional()
]).optional()
}).partial().default({}), // partial() ทำให้ทุกฟิลด์เป็น optional
metadata: z.record(z.unknown()).optional() // เก็บข้อมูลเพิ่มเติมที่ยังไม่รู้จัก
});
// ตัวอย่างการใช้งาน
const result = ResilientUserSchema.safeParse({
id: "12345",
name: "สมชาย",
email: null,
address: {
zipcode: { code: "10110", extension: "1234" }
},
metadata: { source: "mobile_app", version: "2.1" }
});
console.log(result.success); // true
ข้อดีของ Schema ข้างต้นคือ:
- ใช้
.catch()เพื่อกำหนดค่า Fallback เมื่อข้อมูลไม่ผ่าน Validation - ใช้
.nullable().default(null)เพื่อจัดการกับข้อมูลที่อาจหายไป - ใช้
.partial()เพื่อทำให้ nested object ยืดหยุ่นขึ้น - ใช้
z.record(z.unknown())เพื่อเก็บข้อมูลที่ไม่รู้จักไว้ก่อน
2.2 การใช้ Zod Effects เพื่อ Migration อัตโนมัติ
หนึ่งในฟีเจอร์ที่ทรงพลังที่สุดของ Zod คือ .transform() และ .preprocess() ซึ่งช่วยให้เราสามารถแปลงข้อมูลจากรูปแบบเก่าไปเป็นรูปแบบใหม่ได้โดยอัตโนมัติ โดยไม่ต้องเปลี่ยนโค้ด Business Logic หลัก
import { z } from 'zod';
// Schema เวอร์ชันเก่า (v1)
const OldUserSchema = z.object({
full_name: z.string(),
age: z.number(),
phone_numbers: z.array(z.string())
});
// Schema เวอร์ชันใหม่ (v2) พร้อม migration
const NewUserSchema = z.object({
firstName: z.string(),
lastName: z.string(),
age: z.number().min(0).max(150),
contacts: z.array(
z.object({
type: z.enum(['phone', 'email']).default('phone'),
value: z.string()
})
)
}).preprocess((data) => {
// ตรวจจับว่าเป็นข้อมูลรูปแบบเก่าหรือไม่
if (typeof data === 'object' && data !== null && 'full_name' in data) {
const old = data as any;
const nameParts = old.full_name?.split(' ') || ['Unknown', 'User'];
return {
firstName: nameParts[0] || 'Unknown',
lastName: nameParts.slice(1).join(' ') || 'User',
age: old.age || 0,
contacts: (old.phone_numbers || []).map((phone: string) => ({
type: 'phone' as const,
value: phone
}))
};
}
return data;
});
// ทดสอบกับข้อมูลเก่า
const legacyData = {
full_name: "สมชาย ใจดี",
age: 30,
phone_numbers: ["081-234-5678", "02-123-4567"]
};
const migrated = NewUserSchema.parse(legacyData);
console.log(migrated);
// Output: { firstName: "สมชาย", lastName: "ใจดี", age: 30, contacts: [...] }
3. กลยุทธ์การจัดการข้อผิดพลาดแบบ Graceful Degradation
เมื่อ Validation ล้มเหลว ระบบไม่ควรล่มทันที แนวคิดของ Graceful Degradation คือการให้ระบบยังคงทำงานต่อไปได้ โดยอาจลดฟังก์ชันบางอย่างลง หรือใช้ข้อมูลที่เชื่อถือได้น้อยกว่า (แต่ยังปลอดภัย) แทน
3.1 การใช้ SafeParse กับ Fallback Pipeline
แทนที่จะใช้ .parse() ซึ่งจะ throw error ทันที เราควรใช้ .safeParse() ร่วมกับกลไก Fallback แบบหลายชั้น (Multi-layer Fallback)
import { z, ZodError } from 'zod';
// ระบบ Fallback แบบหลายชั้น
class ResilientValidator<T> {
private schemas: z.ZodType<T>[];
private fallbackData: T;
private logger: (error: ZodError, schemaVersion: number) => void;
constructor(options: {
schemas: z.ZodType<T>[];
fallbackData: T;
logger?: (error: ZodError, schemaVersion: number) => void;
}) {
this.schemas = options.schemas;
this.fallbackData = options.fallbackData;
this.logger = options.logger || console.error;
}
validate(data: unknown): {
success: boolean;
data: T;
usedFallback: boolean;
schemaVersion: number;
errors?: ZodError[];
} {
// ลอง validate กับ schema แต่ละเวอร์ชัน
for (let i = 0; i < this.schemas.length; i++) {
const result = this.schemas[i].safeParse(data);
if (result.success) {
return {
success: true,
data: result.data,
usedFallback: false,
schemaVersion: i
};
}
// บันทึกข้อผิดพลาดของ schema นี้
this.logger(result.error, i);
}
// ถ้า schema ทั้งหมดล้มเหลว ใช้ fallback data
return {
success: false,
data: this.fallbackData,
usedFallback: true,
schemaVersion: -1,
errors: this.schemas.map((_, i) => {
const result = this.schemas[i].safeParse(data);
return result.success ? undefined : result.error;
}).filter(Boolean) as ZodError[]
};
}
}
// ตัวอย่างการใช้งาน
const v1Schema = z.object({
id: z.number(),
name: z.string()
});
const v2Schema = z.object({
id: z.union([z.number(), z.string().transform(Number)]),
name: z.string(),
email: z.string().email().optional()
});
const validator = new ResilientValidator({
schemas: [v2Schema, v1Schema], // ลองเวอร์ชันใหม่ก่อนเสมอ
fallbackData: { id: 0, name: 'Guest User' }
});
// ข้อมูลจาก API ภายนอกที่เปลี่ยนรูปแบบ
const externalData = {
id: "ABC123",
name: "John",
email: "[email protected]"
};
const result = validator.validate(externalData);
console.log(result.data); // ได้ข้อมูลที่ถูกต้อง
console.log(result.usedFallback); // false
3.2 การแยก Business Logic ออกจาก Validation Logic
หนึ่งในข้อผิดพลาดที่พบบ่อยคือการผูก Business Logic ไว้กับ Schema Validation โดยตรง ซึ่งทำให้เมื่อ Validation ล้มเหลว Business Logic ก็ล้มเหลวตามไปด้วย วิธีแก้คือการแยกชั้น (Separation of Concerns) โดยใช้รูปแบบ Repository Pattern หรือ Service Layer
| แนวทาง | ข้อดี | ข้อเสีย | เหมาะกับ |
|---|---|---|---|
| Validation First (ตรวจสอบก่อนประมวลผล) | ป้องกันข้อมูลเสียเข้าสู่ระบบ, ตรรกะชัดเจน | ถ้า Validation ล้มเหลว งานทั้งหมดหยุด | ระบบการเงิน, การลงทะเบียน |
| Validation Inline (ตรวจสอบระหว่างประมวลผล) | ยืดหยุ่น, Fallback ได้บางส่วน | โค้ดซับซ้อน, Debug ยาก | ระบบที่ต้องทำงานต่อเนื่องสูง |
| Validation Async (ตรวจสอบแบบไม่บล็อก) | ไม่กระทบประสิทธิภาพ, รองรับ Big Data | ต้องใช้ Queue/Message Broker | ระบบวิเคราะห์ข้อมูล, ETL |
4. การทำ Versioning และ Schema Migration ที่ปลอดภัย
ในระบบจริง Schema ของข้อมูลมักจะเปลี่ยนแปลงอยู่เสมอ การมีกลยุทธ์ Versioning ที่ดีจะช่วยให้เราสามารถอัปเกรดระบบได้โดยไม่กระทบต่อ Business Continuity
4.1 กลยุทธ์ Backward Compatibility
หลักการสำคัญคือ Schema ใหม่ต้องสามารถอ่านข้อมูลเก่าได้เสมอ (Backward Compatible) และควรมีกลไกที่ทำให้ระบบเก่าสามารถทำงานกับข้อมูลใหม่ได้บ้าง (Forward Compatible) Zod ช่วยให้เราทำสิ่งนี้ได้ด้วยการใช้ .or() และ .and()
import { z } from 'zod';
// Schema พื้นฐานที่ใช้ร่วมกันทุกเวอร์ชัน
const BaseProductSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
price: z.number().positive()
});
// Schema เวอร์ชัน 1: ราคาเป็นตัวเลขทศนิยม
const ProductV1Schema = BaseProductSchema.extend({
version: z.literal(1).default(1),
price: z.number().positive()
});
// Schema เวอร์ชัน 2: ราคาสามารถเป็น string ได้ (เช่น "1,299.00")
const ProductV2Schema = BaseProductSchema.extend({
version: z.literal(2).default(2),
price: z.union([
z.number().positive(),
z.string().transform(val => {
const cleaned = val.replace(/[^0-9.]/g, '');
const num = parseFloat(cleaned);
if (isNaN(num)) throw new Error('Invalid price format');
return num;
})
])
});
// Schema เวอร์ชัน 3: เพิ่มฟิลด์ discount และ currency
const ProductV3Schema = BaseProductSchema.extend({
version: z.literal(3).default(3),
price: z.union([
z.number().positive(),
z.string().transform(val => parseFloat(val.replace(/[^0-9.]/g, '')))
]),
discount: z.number().min(0).max(100).default(0),
currency: z.enum(['THB', 'USD', 'EUR']).default('THB')
});
// Schema รวมที่รองรับทุกเวอร์ชัน
const ProductSchema = z.union([
ProductV3Schema,
ProductV2Schema,
ProductV1Schema
]).transform(data => {
// ทำให้ข้อมูลทุกเวอร์ชันมีโครงสร้างเหมือนกัน
return {
...data,
finalPrice: data.price * (1 - (data as any).discount / 100 || 0)
};
});
// ตัวอย่างข้อมูลจากหลายเวอร์ชัน
const products = [
{ id: "a1b2c3d4-...", name: "Coffee", price: 150 }, // v1
{ id: "e5f6g7h8-...", name: "Tea", price: "1,200.50" }, // v2
{ id: "i9j0k1l2-...", name: "Cake", price: 250, discount: 10, currency: "THB" } // v3
];
products.forEach(product => {
const result = ProductSchema.safeParse(product);
if (result.success) {
console.log(`Product: ${result.data.name}, Final Price: ${result.data.finalPrice}`);
} else {
console.error('Validation failed:', result.error.issues);
}
});
4.2 การใช้ Zod Schema Registry สำหรับการจัดการหลายเวอร์ชัน
ในระบบที่มีการเรียกใช้ Schema จากหลายทีมหรือหลายบริการ การมี Schema Registry กลางจะช่วยให้ทุกคนสามารถอ้างอิงเวอร์ชันของ Schema ได้อย่างถูกต้อง
import { z } from 'zod';
// Schema Registry
class SchemaRegistry {
private schemas: Map<string, Map<number, z.ZodType<any>>> = new Map();
register(entityName: string, version: number, schema: z.ZodType<any>): void {
if (!this.schemas.has(entityName)) {
this.schemas.set(entityName, new Map());
}
this.schemas.get(entityName)!.set(version, schema);
}
getSchema(entityName: string, version: number): z.ZodType<any> | undefined {
return this.schemas.get(entityName)?.get(version);
}
getLatestVersion(entityName: string): number {
const versions = this.schemas.get(entityName);
if (!versions) return -1;
return Math.max(...Array.from(versions.keys()));
}
// สร้าง Schema ที่รองรับหลายเวอร์ชัน
createMultiVersionSchema(entityName: string): z.ZodType<any> {
const versions = this.schemas.get(entityName);
if (!versions || versions.size === 0) {
throw new Error(`No schemas registered for entity: ${entityName}`);
}
// เรียงลำดับจากเวอร์ชันล่าสุดไปเก่าสุด
const sortedVersions = Array.from(versions.entries())
.sort(([a], [b]) => b - a);
// สร้าง union schema
const schemas = sortedVersions.map(([_, schema]) => schema);
if (schemas.length === 1) return schemas[0];
return z.union(schemas as [z.ZodType<any>, z.ZodType<any>, ...z.ZodType<any>[]]);
}
}
// ตัวอย่างการใช้งาน
const registry = new SchemaRegistry();
// ทีม A ลงทะเบียน Schema ผู้ใช้
registry.register('User', 1, z.object({
id: z.string(),
name: z.string(),
email: z.string().email()
}));
registry.register('User', 2, z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
phone: z.string().optional()
}));
// ทีม B ใช้ Schema จาก Registry โดยไม่ต้องรู้รายละเอียด
const userSchema = registry.createMultiVersionSchema('User');
const rawData = { id: "123", name: "สมชาย", email: "[email protected]" };
const validated = userSchema.parse(rawData);
console.log('Validated:', validated);
5. การ Monitoring และ Alerting สำหรับ Schema Drift
Business Continuity ที่ดีต้องมีระบบที่คอยตรวจสอบสุขภาพของ Schema อย่างต่อเนื่อง (Schema Health Monitoring) และแจ้งเตือนเมื่อเกิดความผิดปกติ Zod สามารถทำงานร่วมกับเครื่องมือ Monitoring เช่น Prometheus, Datadog, หรือ Sentry เพื่อสร้างระบบ Early Warning
5.1 การเก็บ Metrics การ Validation
เราควรเก็บสถิติเกี่ยวกับการ Validation ทุกครั้งเพื่อนำมาวิเคราะห์แนวโน้ม:
- Validation Success Rate – อัตราความสำเร็จของการตรวจสอบข้อมูล
- Schema Drift Detection – จำนวนครั้งที่ข้อมูลไม่ตรงกับ Schema ที่คาดหวัง
- Fallback Usage Rate – ความถี่ในการใช้ Fallback Data
- Validation Latency – เวลาที่ใช้ในการตรวจสอบข้อมูล
import { z } from 'zod';
import { Counter, Histogram } from 'prom-client'; // สมมติว่าใช้ Prometheus
// สร้าง Metrics
const validationSuccessCounter = new Counter({
name: 'zod_validation_success_total',
help: 'Total number of successful validations',
labelNames: ['schema_name', 'version']
});
const validationFailureCounter = new Counter({
name: 'zod_validation_failure_total',
help: 'Total number of failed validations',
labelNames: ['schema_name', 'version', 'error_type']
});
const validationDurationHistogram = new Histogram({
name: 'zod_validation_duration_seconds',
help: 'Duration of Zod validation in seconds',
labelNames: ['schema_name'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1]
});
// Wrapper Function ที่บันทึก Metrics
function monitoredValidate<T>(
schema: z.ZodType<T>,
data: unknown,
schemaName: string,
version: number = 1
): { success: boolean; data?: T; error?: z.ZodError } {
const start = process.hrtime.bigint();
try {
const result = schema.safeParse(data);
const duration = Number(process.hrtime.bigint() - start) / 1e9;
validationDurationHistogram.observe({ schema_name: schemaName }, duration);
if (result.success) {
validationSuccessCounter.inc({ schema_name: schemaName, version });
return { success: true, data: result.data };
} else {
// จำแนกประเภทข้อผิดพลาดเพื่อการวิเคราะห์
const errorTypes = new Set(result.error.issues.map(issue => issue.code));
errorTypes.forEach(errorType => {
validationFailureCounter.inc({
schema_name: schemaName,
version,
error_type: errorType
});
});
// ถ้ามีข้อผิดพลาดมากกว่า threshold ให้ Trigger Alert
if (result.error.issues.length > 5) {
triggerAlert({
schemaName,
version,
errorCount: result.error.issues.length,
sampleError: result.error.issues[0]
});
}
return { success: false, error: result.error };
}
} catch (err) {
// จับข้อผิดพลาดที่ไม่คาดคิด
console.error('Unexpected validation error:', err);
return { success: false, error: undefined };
}
}
// ฟังก์ชันแจ้งเตือน
function triggerAlert(details: any) {
// ส่งไปยัง Slack, Email, หรือ PagerDuty
console.warn('🚨 Schema Drift Alert:', details);
// ในระบบจริง: sendToAlertingSystem(details);
}
// ตัวอย่างการใช้งาน
const orderSchema = z.object({
orderId: z.string(),
amount: z.number().positive(),
status: z.enum(['pending', 'completed', 'cancelled'])
});
const testData = { orderId: "ORD-001", amount: -100, status: "unknown" };
const result = monitoredValidate(orderSchema, testData, 'OrderSchema', 2);
console.log('Validation result:', result.success);
5.2 การตั้งค่า Alert Threshold
| Metric | Warning Threshold | Critical Threshold | การตอบสนอง |
|---|---|---|---|
| Validation Failure Rate | > 5% ใน 5 นาที | > 20% ใน 1 นาที | ตรวจสอบ API Provider, Rollback Schema |
| Fallback Usage Rate | > 1% ใน 15 นาที | > 10% ใน 5 นาที | ตรวจสอบ Data Pipeline, แจ้งทีม Data Engineering |
| Validation Latency P99 | > 100ms | > 500ms | ตรวจสอบประสิทธิภาพ Schema, พิจารณา Caching |
| Schema Drift Count | > 10 ครั้ง/ชั่วโมง | > 50 ครั้ง/ชั่วโมง | ประชุมทีม, วิเคราะห์ Root Cause |
6. กรณีศึกษาจากโลกจริง: SiamCafe Payment Gateway
เพื่อให้เห็นภาพชัดเจนยิ่งขึ้น ขอยกตัวอย่างระบบ Payment Gateway ของ SiamCafe ที่ต้องรองรับธุรกรรมกว่า 500 รายการต่อวินาที และต้องทำงานต่อเนื่อง 99.99% ของเวลา
6.1 ปัญหาที่พบ
ระบบ Payment Gateway ของเราต้องรับข้อมูลจากธนาคารพันธมิตรหลายแห่ง แต่ละแห่งมีรูปแบบข้อมูลที่แตกต่างกัน และบางครั้งก็เปลี่ยนรูปแบบโดยไม่แจ้งล่วงหน้า เราเคยประสบปัญหาหนักเมื่อธนาคารแห่งหนึ่งเปลี่ยนฟิลด์ transaction_id จาก string 10 ตัวอักษร เป็น string 20 ตัวอักษรที่มีตัวเลขและตัวอักษรผสมกัน ซึ่งทำให้ระบบของเราที่ใช้ Schema แบบตายตัวไม่สามารถประมวลผลธุรกรรมได้นานถึง 45 นาที
6.2 วิธีแก้ไข
เราใช้แนวทาง “Adaptive Schema with Circuit Breaker” ซึ่งประกอบด้วย:
- Multi-Version Schema: สร้าง Schema หลายเวอร์ชันสำหรับธนาคารแต่ละแห่ง โดยเรียงลำดับจากเวอร์ชันล่าสุดไปเก่าสุด
- Graceful Degradation: ถ้า Schema ล่าสุดใช้ไม่ได้ ให้ลองใช้ Schema ก่อนหน้า ถ้ายังไม่ได้ ให้ใช้ข้อมูลพื้นฐานที่จำเป็นเท่านั้น (Minimum Viable Data)
- Circuit Breaker: ถ้า Validation ล้มเหลวเกิน 10 ครั้งใน 1 นาที ให้หยุดพยายามและเปลี่ยนไปใช้โหมด “Manual Review” ทันที
- Auto-Recovery: เมื่อตรวจพบว่า Schema ใหม่สามารถใช้ได้ (ผ่านการทดสอบใน Sandbox Environment) ให้ Deploy Schema ใหม่อัตโนมัติผ่าน Feature Flag
import { z } from 'zod';
// ระบบ Circuit Breaker สำหรับ Validation
class ValidationCircuitBreaker {
private failureCount: number = 0;
private lastFailureTime: number = 0;
private readonly threshold: number;
private readonly timeWindow: number; // milliseconds
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
constructor(threshold: number = 10, timeWindowMs: number = 60000) {
this.threshold = threshold;
this.timeWindow = timeWindowMs;
}
isOpen(): boolean {
if (this.state === 'OPEN') {
// ตรวจสอบว่าถึงเวลา Half-Open หรือยัง
if (Date.now() - this.lastFailureTime > this.timeWindow) {
this.state = 'HALF_OPEN';
return false; // ให้ลองอีกครั้ง
}
return true;
}
return false;
}
recordFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.threshold) {
this.state = 'OPEN';
console.error(`⚠️ Circuit Breaker OPEN: ${this.failureCount} failures in window`);
}
}
recordSuccess(): void {
this.failureCount = 0;
this.state = 'CLOSED';
}
}
// ระบบ Payment Gateway ที่ใช้ Adaptive Schema
class PaymentGatewayProcessor {
private bankSchemas: Map<string, z.ZodType[]>;
private circuitBreakers: Map<string, ValidationCircuitBreaker>;
private fallbackHandler: (rawData: unknown) => any;
constructor() {
this.bankSchemas = new Map();
this.circuitBreakers = new Map();
this.fallbackHandler = this.createFallbackHandler();
}
registerBank(bankCode: string, schemas: z.ZodType[]): void {
this.bankSchemas.set(bankCode, schemas);
this.circuitBreakers.set(bankCode, new ValidationCircuitBreaker(5, 30000));
}
private createFallbackHandler() {
return (rawData: any) => {
// Fallback: ดึงเฉพาะข้อมูลที่จำเป็นที่สุด
return {
transactionId: rawData.transaction_id || rawData.txnId || rawData.id || 'UNKNOWN',
amount: rawData.amount || rawData.total || 0,
currency: rawData.currency || rawData.curr || 'THB',
status: rawData.status || 'pending',
rawData: rawData, // เก็บข้อมูลดิบไว้ตรวจสอบภายหลัง
_fallback: true
};
};
}
processPayment(bankCode: string, rawData: unknown): any {
const breaker = this.circuitBreakers.get(bankCode);
const schemas = this.bankSchemas.get(bankCode);
if (!breaker || !schemas || schemas.length === 0) {
return this.fallbackHandler(rawData);
}
// ถ้า Circuit Breaker เปิดอยู่ ให้ใช้ Fallback ทันที
if (breaker.isOpen()) {
console.warn(`Circuit breaker OPEN for ${bankCode}, using fallback`);
return this.fallbackHandler(rawData);
}
// ลอง validate กับ schema แต่ละตัว
for (let i = 0; i < schemas.length; i++) {
const result = schemas[i].safeParse(rawData);
if (result.success) {
breaker.recordSuccess();
return {
...result.data,
_fallback: false,
_schemaVersion: i
};
}
}
// ถ้า schema ทั้งหมดล้มเหลว
breaker.recordFailure();
return this.fallbackHandler(rawData);
}
}
// ตัวอย่างการใช้งาน
const gateway = new PaymentGatewayProcessor();
// ลงทะเบียนธนาคาร A พร้อม Schema หลายเวอร์ชัน
gateway.registerBank('BANK_A', [
z.object({ // เวอร์ชันล่าสุด
transactionId: z.string().min(5).max(30),
amount: z.number().positive(),
currency: z.string().length(3)
}),
z.object({ // เวอร์ชันเก่า
transaction_id: z.string().min(5).max(20),
amount: z.number().positive(),
curr: z.string().length(3)
})
]);
// ทดสอบกับข้อมูลที่ผิดปกติ
const badData = { txn_id: "123", amount: -50, currency: "THB" };
const result = gateway.processPayment('BANK_A', badData);
console.log('Payment processed:', result);
// ได้ข้อมูล fallback แทนที่จะ throw error
7. แนวปฏิบัติที่ดีที่สุด (Best Practices) สำหรับ Zod Business Continuity
จากประสบการณ์ของทีม SiamCafe เราได้สรุปแนวปฏิบัติที่ดีที่สุดที่ควรนำไปใช้ในทุกโครงการ TypeScript ที่ใช้ Zod:
7.1 การออกแบบ Schema
- ใช้
.catch()และ.default()เสมอ สำหรับฟิลด์ที่อาจมีค่า null หรือ undefined - หลีกเลี่ยงการใช้
.strict()ใน Production เพราะจะ reject ข้อมูลที่มีฟิลด์เกินมา ซึ่งอาจเป็นฟิลด์ใหม่ที่ยังไม่รู้จัก - ใช้
z.union()สำหรับฟิลด์ที่อาจมีหลายรูปแบบ เช่น id ที่เป็นได้ทั้ง string และ number - เพิ่มฟิลด์
metadataหรือextrasประเภทz.record(z.unknown())เพื่อเก็บข้อมูลเพิ่มเติมที่ไม่รู้จัก
7.2 การจัดการข้อผิดพลาด
- ใช้
.safeParse()แทน.parse()เสมอ ใน Production Code - สร้าง Fallback Pipeline ที่มีหลายชั้น ตั้งแต่ Schema ล่าสุดไปจนถึงข้อมูลพื้นฐาน
- บันทึกข้อผิดพลาดทุกครั้ง พร้อม Context เช่น Source, Timestamp, และ Raw Data
- ตั้งค่า Alert สำหรับ Validation Failure