OpenAPI Validation ใน Express ควรอยู่ตรงไหนของ flow
หลายทีมเริ่มใช้ OpenAPI ด้วยเหตุผลที่ดีมาก เช่น อยากมีเอกสาร API ที่ชัด อยากให้ frontend กับ backend คุยกันตรงขึ้น หรืออยากมี schema กลางไว้ใช้ generate types และ docs แต่พอเริ่มทำจริง คำถามที่มักตามมาคือ
OpenAPI ควรเป็นแค่เอกสารหรือควรใช้ validate request จริงด้วย
ถ้าจะ validate จริง ควรตรวจตรงไหนของ flow
ควรเช็กก่อนเข้า controller หรือค่อยไปเช็กใน service
ถ้า schema ผ่านแล้ว ยังต้อง validate อย่างอื่นอีกไหม
ถ้าเอาทุกอย่างไปยัดใน OpenAPI จะดีขึ้นหรือจะยิ่งมั่ว
ปัญหานี้สำคัญมาก เพราะหลายระบบมี OpenAPI อยู่จริง แต่ใช้แค่เป็นเอกสารประกอบ ในขณะที่ validation จริงกระจายอยู่เต็ม controller, service และ middleware แบบไม่เป็นชั้น พอระบบโตขึ้นก็เริ่มเกิดอาการที่คุ้นเคย
- request shape ไม่ตรงแต่หลุดเข้า business logic
- controller แต่ละตัว validate ไม่เหมือนกัน
- response หลุด contract โดยไม่มีใครรู้
- frontend เชื่อ doc คนละชุดกับที่ backend รับจริง
- เปลี่ยน API แล้วไม่มี guard คอยบอกว่า breaking change หลุดเข้ามาแล้ว
บทความนี้อธิบายว่า OpenAPI validation ในระบบ Express ควรอยู่ตรงไหนของ flow ควรตรวจอะไรบ้าง และอะไรที่ไม่ควรโยนให้ OpenAPI ทำแทน
TL;DR
สรุปให้สั้นที่สุด
OpenAPI validation ควรอยู่ก่อน business logic
เพื่อให้ request ที่ผิด contract ถูกปฏิเสธตั้งแต่ต้นทาง ไม่ใช่ปล่อยให้หลุดเข้า service แล้วค่อยพังข้างใน
แต่ OpenAPI validation ไม่ได้แทน business validation ทั้งหมด มันเหมาะกับการตรวจเรื่อง contract เช่น
- path params
- query params
- headers
- request body shape
- response shape ในกรณีที่ต้องการ strictness สูงขึ้น
ส่วนเรื่องอย่างสิทธิ์การใช้งาน, state transition, idempotency, business invariants และ workflow rules ยังต้องอยู่ในชั้นอื่นตามเดิม
OpenAPI Validation คืออะไร
OpenAPI validation คือการใช้ schema ที่นิยามไว้ใน OpenAPI document มาตรวจว่า request และ response ของ API ตรงกับ contract ที่ประกาศไว้หรือไม่
ในเชิงปฏิบัติ มันมักตอบคำถามแบบนี้
- path parameter ตัวนี้ต้องเป็น string หรือ integer
- query parameter นี้มีได้เฉพาะค่ากลุ่มใดบ้าง
- body ต้องมี field อะไรบ้าง
- field ไหน required
- field ไหนเป็น enum
- รูปแบบข้อมูล เช่น email, uuid, date-time ถูกต้องหรือไม่
- response ที่ route นี้คืนกลับมาตรงกับ schema หรือไม่
จุดแข็งของมันคือช่วยแยกเรื่อง “รูปแบบของ contract” ออกจาก “ตรรกะธุรกิจ” ซึ่งเป็นการแยกความรับผิดชอบที่คุ้มค่ามากในระยะยาว
ทำไม Validation ควรอยู่ก่อน Business Logic
ถ้า request shape ยังไม่ผ่าน แต่ระบบปล่อยให้หลุดเข้า controller หรือ service ต่อ ปัญหาที่ตามมาจะเริ่มสะสมเร็วมาก
อย่างแรกคือ business logic ต้องมาแบกภาระตรวจข้อมูลขั้นพื้นฐานเอง ทั้งที่ไม่ใช่หน้าที่หลัก
อย่างที่สองคือ error message จะเริ่มไม่สม่ำเสมอ เพราะแต่ละ route ตรวจไม่เหมือนกัน
อย่างที่สามคือ service ภายในต้องระวัง null, undefined, type mismatch หรือ field structure แปลก ๆ ตลอดเวลา
อย่างที่สี่คือ contract ระหว่างทีมเริ่มไม่น่าเชื่อถือ เพราะ doc บอกอย่างหนึ่ง runtime รับอีกอย่างหนึ่ง
ถ้า validation อยู่ก่อน business logic ผลที่ได้คือ controller และ service จะทำงานบนข้อมูลที่ “ผ่าน contract มาแล้ว” ทำให้ reasoning ง่ายขึ้นมาก
OpenAPI Validation ควรอยู่ตรงไหนของ flow
ลำดับที่ดีสำหรับระบบ Express โดยทั่วไปมักเป็นประมาณนี้
- รับ request เข้ามา
- สร้าง request context เช่น request ID, correlation ID
- ทำงานระดับ transport หรือ generic middleware ที่จำเป็น
- ทำ authentication เมื่อ route ต้องการ
- ทำ OpenAPI validation กับ params, query, headers, body
- ผ่านเข้า authorization หรือ policy checks
- ผ่านเข้า business validation และ workflow rules
- เข้า controller / service logic
- สร้าง response
- validate response ถ้าระบบต้องการ enforce response contract ด้วย
สิ่งที่สำคัญคือ OpenAPI validation ควรอยู่ก่อน business logic แต่ไม่จำเป็นต้องอยู่ก่อน authentication เสมอไป เพราะบางระบบไม่ต้องการเสียต้นทุน validate request ทั้งก้อนกับคนที่ไม่มีสิทธิ์เรียก route นั้นตั้งแต่แรก
สิ่งที่ OpenAPI Validation เหมาะจะตรวจ
OpenAPI validation เหมาะมากกับสิ่งที่เป็น contract-level concerns เช่น
- route params
- query params
- request headers ที่ประกาศไว้
- request body structure
- required fields
- data types
- enum values
- array/object shapes
- nullable rules
- response schema
สิ่งเหล่านี้เป็นเรื่องของ “หน้าตา API” มากกว่า “กติกาของธุรกิจ” และเมื่อมันถูกรวมศูนย์อยู่ใน contract กลาง ทีมจะคุยกันง่ายขึ้นมาก
สิ่งที่ไม่ควรยัดลง OpenAPI จนเกินไป
จุดที่หลายทีมพลาดคือพอเริ่มใช้ schema validation แล้วพยายามให้ OpenAPI ตัดสินทุกอย่างแทนระบบ ซึ่งจะเริ่มทำให้ contract ปนกับ policy และ business logic
ตัวอย่างสิ่งที่ยังควรอยู่ในชั้นอื่น เช่น
- user นี้มีสิทธิ์ทำ action นี้หรือไม่
- order นี้อยู่ในสถานะที่จ่ายเงินได้หรือไม่
- refund นี้ถูก approve ไปแล้วหรือยัง
- idempotency key นี้เคยใช้แล้วหรือไม่
- resource นี้เป็นของ tenant เดียวกันหรือไม่
- amount นี้สอดคล้องกับข้อมูลในฐานข้อมูลหรือไม่
- action นี้ทำได้เฉพาะช่วงเวลาใดช่วงเวลาหนึ่งหรือไม่
พูดอีกแบบคือ OpenAPI validation ช่วยตรวจว่า “request หน้าตาถูกไหม” แต่ไม่ได้ตัดสินว่า “request นี้ควรได้รับอนุญาตหรือไม่ในบริบทธุรกิจจริง”
ถ้าไม่มีชั้นนี้จะเกิดอะไรขึ้น
ระบบที่ไม่มี contract validation กลางมักเริ่มเจอปัญหาแบบเดิม ๆ
frontend ส่ง field เกินมาแต่ backend บาง route รับ บาง route ไม่รับ
query params เป็นชนิดผิดแต่หลุดเข้า logic ข้างใน
เอกสารบอกว่า field required แต่ runtime ไม่ได้บังคับ
response ของ route เดียวกันเปลี่ยน shape ไปเรื่อย ๆ
ทีมแก้บั๊กโดยเพิ่ม if (!x) ตรงโน้นทีตรงนี้ที จน logic เริ่มปนกันไปหมด
ปัญหาเหล่านี้ไม่ได้ดูร้ายแรงทีเดียว แต่พอสะสมในระบบที่โตขึ้น มันจะเริ่มบั่นทอนความเชื่อมั่นของ API ทั้งชุด
การแยก Contract Validation ออกจาก Business Validation สำคัญยังไง
นี่เป็นจุดที่สำคัญที่สุดข้อหนึ่ง
Contract Validation
ถามว่า request นี้ “ตรงกับรูปแบบที่ API รับหรือไม่”
ตัวอย่างเช่น
amountต้องเป็น numbercurrencyต้องเป็น stringstatusต้องเป็นหนึ่งใน enum ที่กำหนดcustomerIdต้องมีlimitต้องเป็น integer
Business Validation
ถามว่า request นี้ “สมเหตุสมผลในโลกของธุรกิจหรือไม่”
ตัวอย่างเช่น
amountต้องมากกว่า balance คงเหลือหรือไม่currencyนี้รองรับสำหรับ merchant นี้ไหมcustomerIdนี้มีสิทธิ์ทำรายการนี้หรือไม่statusนี้เปลี่ยนจากสถานะเดิมได้ไหม- order นี้ถูกจ่ายไปแล้วหรือยัง
ถ้าสองอย่างนี้ปนกัน ระบบจะเริ่มอ่านยากและทดสอบยาก เพราะคนดู schema ก็ไม่รู้ว่าตกลงกติกานี้เป็น contract หรือเป็น business rule
ควร validate response ด้วยไหม
คำตอบคือ “ควรพิจารณา” โดยเฉพาะ route สำคัญหรือระบบที่ต้องคุม contract จริงจัง
การ validate response มีข้อดีคือช่วยจับปัญหาแบบนี้ได้เร็ว
- backend หลุด field ที่ไม่ตั้งใจ expose
- field ที่ doc บอกว่ามี ดันหายไป
- type ของ field เปลี่ยนโดยไม่รู้ตัว
- refactor ภายในทำให้ response shape เพี้ยน
แต่ข้อควรระวังคือ response validation มีต้นทุน runtime และอาจไม่จำเป็นต้องเปิดใช้ทุก route ทุก environment เท่ากัน บางทีมเลือกเปิดเต็มใน test/staging และเปิดเฉพาะ route สำคัญใน production
OpenAPI Validation ควรผูกกับ error format กลาง
อีกเรื่องที่สำคัญคือ เมื่อ validation fail ระบบควรตอบกลับใน format ที่สม่ำเสมอ ไม่ใช่บาง route ตอบ string ธรรมดา บาง route ตอบ array บาง route ตอบ field คนละชื่อ
อย่างน้อยควรสื่อให้ชัดว่า
- fail เพราะอะไร
- field ไหนมีปัญหา
- ตรงส่วนใดของ request เช่น params, query, body, headers
- request id คืออะไร ถ้าระบบใช้ request context
ความสม่ำเสมอของ error shape จะช่วยทั้ง frontend, support และ debugging มากกว่าแค่ “มี validation” อย่างเดียว
ตัวอย่าง OpenAPI Schema
ตัวอย่างนี้สาธิต route สำหรับสร้าง payment แบบเรียบง่าย
openapi: 3.0.3
info:
title: Payments API
version: 1.0.0
paths:
/payments:
post:
summary: Create payment
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- amount
- currency
- orderId
properties:
amount:
type: number
minimum: 0.01
currency:
type: string
minLength: 3
maxLength: 3
orderId:
type: string
responses:
"201":
description: Payment created
content:
application/json:
schema:
type: object
required:
- success
- paymentId
properties:
success:
type: boolean
paymentId:
type: string
schema แบบนี้เหมาะกับการตรวจเรื่อง shape ของ request และ response แต่ยังไม่ได้แตะ business rules ภายใน เช่น order นี้มีอยู่จริงไหม หรือ order นี้ถูกจ่ายไปแล้วหรือยัง
ตัวอย่าง Express/Node.js แบบเริ่มต้น
ตัวอย่างนี้แสดง flow ที่แยก OpenAPI-style contract validation ออกจาก business logic ให้ชัด แม้จะไม่ได้ใช้ library เต็มรูปแบบจริงในตัวอย่างนี้
const express = require("express");
const crypto = require("crypto");
const app = express();
app.use(express.json());
app.use((req, res, next) => {
req.requestId = req.get("X-Request-Id") || `req_${crypto.randomUUID()}`;
res.setHeader("X-Request-Id", req.requestId);
next();
});
app.post(
"/payments",
authenticateRequest,
validateCreatePaymentRequest,
async (req, res) => {
try {
const result = await createPayment(req.body, {
actorId: req.actor.id,
requestId: req.requestId
});
const responseBody = {
success: true,
paymentId: result.paymentId
};
validateCreatePaymentResponse(responseBody);
return res.status(201).json(responseBody);
} catch (error) {
console.error("Create payment failed:", {
requestId: req.requestId,
message: error.message
});
return res.status(500).json({
error: "Create payment failed",
requestId: req.requestId
});
}
}
);
function authenticateRequest(req, res, next) {
const actorId = req.get("X-Actor-Id");
if (!actorId) {
return res.status(401).json({
error: "Unauthorized",
requestId: req.requestId
});
}
req.actor = { id: actorId };
next();
}
function validateCreatePaymentRequest(req, res, next) {
const body = req.body;
if (!body || typeof body !== "object") {
return validationError(res, req.requestId, "body", "Request body must be an object");
}
if (typeof body.amount !== "number" || body.amount <= 0) {
return validationError(res, req.requestId, "body.amount", "amount must be a positive number");
}
if (typeof body.currency !== "string" || body.currency.length !== 3) {
return validationError(res, req.requestId, "body.currency", "currency must be a 3-letter string");
}
if (typeof body.orderId !== "string" || !body.orderId.trim()) {
return validationError(res, req.requestId, "body.orderId", "orderId is required");
}
next();
}
function validateCreatePaymentResponse(body) {
if (!body || typeof body !== "object") {
throw new Error("Invalid response body");
}
if (typeof body.success !== "boolean") {
throw new Error("Response success must be boolean");
}
if (typeof body.paymentId !== "string" || !body.paymentId.trim()) {
throw new Error("Response paymentId is required");
}
}
function validationError(res, requestId, field, message) {
return res.status(400).json({
error: "Validation failed",
field,
message,
requestId
});
}
async function createPayment(payload, context) {
/**
* ตัวอย่าง business validation:
* - order มีอยู่จริงไหม
* - actor นี้มีสิทธิ์กับ order นี้ไหม
* - order นี้เคยถูกจ่ายไปแล้วหรือยัง
* - ต้องใช้ idempotency key หรือไม่
*/
if (payload.orderId === "ord_paid") {
throw new Error("Order already paid");
}
return {
paymentId: `pay_${crypto.randomUUID()}`
};
}
app.listen(3000, () => {
console.log("Server listening on port 3000");
});
โค้ดชุดนี้กำลังช่วยอะไร
จุดสำคัญของตัวอย่างนี้ไม่ใช่ความสมบูรณ์ของ validator แต่คือการแยกชั้นความรับผิดชอบให้ชัด
authenticateRequest จัดการเรื่องตัวตนvalidateCreatePaymentRequest จัดการเรื่อง contract ของ requestcreatePayment จัดการเรื่อง business rulesvalidateCreatePaymentResponse ช่วยเช็กว่า response ที่ปล่อยออกไปยังตรงกับ contract ที่ประกาศไว้
การแยกแบบนี้ทำให้เวลา route โตขึ้น ระบบยังพอ maintain ได้ และไม่เกิดภาวะที่ controller หนึ่งตัวต้องแบกทั้ง auth, contract validation, business validation และ response shaping แบบปนกันหมด
จุดที่ต้องระวังใน production
1) อย่าให้ OpenAPI เป็นแค่เอกสารที่ไม่ enforce จริง
ถ้า schema อยู่คนละโลกกับ runtime contract สุดท้าย doc จะค่อย ๆ หมดความน่าเชื่อถือ
2) อย่ายัด business rules ลง schema จนอ่านยาก
OpenAPI ควรอธิบาย contract ให้ชัด ไม่ใช่แบกตรรกะ workflow ทั้งระบบ
3) อย่าลืมเรื่อง unknown fields
บางระบบยอมให้ field เกินมาได้ บางระบบไม่ยอม จุดนี้ควรตัดสินใจให้ชัด ไม่อย่างนั้น behavior จะไม่สม่ำเสมอ
4) อย่าให้ validation error format กระจัดกระจาย
เมื่อ schema fail ทุก route ควรตอบ error shape ที่ใกล้เคียงกัน ไม่ใช่แล้วแต่คนเขียน
5) อย่าคิดว่า schema ผ่านแล้ว route ปลอดภัยแล้ว
request อาจผ่าน schema แต่ยังผิด business state, ผิดสิทธิ์, หรือเสี่ยง side effect ซ้ำได้อยู่ ดังนั้น idempotency, authorization และ workflow guards ยังจำเป็น
OpenAPI Validation เกี่ยวกับ Breaking Changes ยังไง
ยิ่งระบบใช้ OpenAPI validation จริงมากเท่าไร คุณจะยิ่งเห็น breaking changes ชัดขึ้นมาก เช่น
- ลบ field ที่ client ยังใช้อยู่
- เปลี่ยน type ของ field
- เปลี่ยน enum values
- เปลี่ยน error format
- ทำ response shape ไม่ตรงของเดิม
นี่เป็นข้อดี เพราะมันทำให้การเปลี่ยน contract ไม่หลุดเข้า production ง่าย ๆ และทำให้การคุยเรื่อง versioning มีฐานข้อมูลจริงมากขึ้น ไม่ใช่เดาเอาว่าคงไม่กระทบใคร
OpenAPI Validation เกี่ยวกับ Idempotency ยังไง
OpenAPI validation อาจเช็กได้ว่า header Idempotency-Key ถูกส่งมาหรือไม่ใน route ที่กำหนด แต่การตัดสินว่า key นี้เคยใช้แล้วหรือไม่ หรือ payload เดิมกับ key เดิมตรงกันหรือไม่ ยังเป็นหน้าที่ของ application logic และ storage
พูดอีกแบบคือ
- OpenAPI ช่วยบอกว่า route นี้ “ต้องมี header นี้”
- application logic ช่วยตัดสินว่า header นี้ “ใช้ได้จริงหรือไม่”
รีวิวแนวทางนี้แบบ production-minded
Correctness
การวาง contract validation ไว้ก่อน business logic ช่วยลดความคลุมเครือของ input และทำให้ service ทำงานบนข้อมูลที่เชื่อถือได้มากขึ้น
Security
มันไม่ใช่ security control โดยตรงแบบ authorization แต่ช่วยลด input ambiguity และลดพื้นที่ที่ malformed request จะหลุดเข้า logic ภายใน
Efficiency
validation กลางช่วยลด duplicate logic ระหว่าง route และลดภาระ debugging เพราะ error ถูกจับเร็วและจับในชั้นที่เหมาะกว่า
Error handling
ถ้า error จาก validation ถูกทำให้สม่ำเสมอ ทั้งทีม frontend, backend และ support จะทำงานง่ายขึ้นมาก โดยเฉพาะเมื่อมี request ID แนบกลับไปด้วย
Checklist สั้น ๆ ก่อนใช้ OpenAPI validation ใน production
- route สำคัญมี schema ชัดเจน
- validation อยู่ก่อน business logic
- auth และ validation มีลำดับที่ตกลงกันชัด
- request body, params, query และ headers ถูกตรวจในชั้นเดียวกันอย่างสม่ำเสมอ
- response สำคัญมีการ validate หรืออย่างน้อยมี test coverage
- error format จาก validation เป็นมาตรฐานเดียวกัน
- unknown fields มี policy ชัดเจน
- business rules ไม่ถูกยัดจนปนกับ schema เกินไป
- breaking changes ถูกตรวจจาก contract จริง
- route ที่มี side effect ยังใช้ idempotency และ business guards ตามความเหมาะสม
บทความที่ควรอ่านต่อ
- OpenAPI Breaking Changes มีอะไรบ้าง และกันยังไงก่อนขึ้น production
- API Versioning ควรใช้ path, header หรือ date-based แบบไหนดี
- Webhook vs Polling vs WebSocket ควรเลือกแบบไหนในระบบจริง
- Idempotency Key คืออะไร และทำไม API ที่มี side effect ต้องมี
สรุป
OpenAPI validation ที่มีประโยชน์จริงไม่ใช่แค่การมี schema สวย ๆ แต่คือการทำให้ contract ถูก enforce ตั้งแต่ก่อน request จะหลุดเข้า business logic
เมื่อทีมแยก contract validation ออกจาก business validation ได้ชัด ระบบจะอ่านง่ายขึ้น เปลี่ยนง่ายขึ้น และตรวจจับความเพี้ยนของ API ได้เร็วขึ้นมาก
สรุปสั้นที่สุดคือ
OpenAPI validation ควรทำหน้าที่เฝ้าประตูของ contract ส่วน business logic ควรทำหน้าที่ตัดสินโลกของธุรกิจ