1. Home
  2. Learn
  3. OpenAPI
  4. OpenAPI สำหรับงานจริง: ออกแบบ API แบบ contract-first ให้ทีมคุยกันรู้เรื่อง
OpenAPI

OpenAPI สำหรับงานจริง: ออกแบบ API แบบ contract-first ให้ทีมคุยกันรู้เรื่อง

อธิบายการใช้ OpenAPI ในงานจริงแบบเป็นระบบ ตั้งแต่การออกแบบ schema, request/response contract, validation, versioning, documentation และตัวอย่างใช้งานกับ Node.js/Express

OpenAPI สำหรับงานจริง: ออกแบบ API แบบ contract-first ให้ทีมคุยกันรู้เรื่อง

หลายทีมเริ่มทำ API จากการเขียน route ก่อน แล้วค่อยตามมาเขียน documentation ทีหลัง หรือบางครั้งก็ไม่มี documentation ที่อัปเดตตามโค้ดจริงเลย ผลลัพธ์ที่เกิดขึ้นคือ frontend เรียก field ไม่ตรง backend, QA ไม่แน่ใจว่าค่าไหนบังคับหรือไม่บังคับ, ฝั่ง integration อ่านจาก wiki เก่าแล้วใช้งานผิด และเมื่อมีการเปลี่ยน shape ของ response ก็เริ่มกระทบหลายจุดพร้อมกันโดยไม่มีใครเห็นภาพรวม

ปัญหาแบบนี้ไม่ได้เกิดจากทีมเขียนโค้ดไม่เก่ง แต่เกิดจากระบบยังไม่มี contract กลาง ที่ทุกฝ่ายใช้ยึดร่วมกันอย่างจริงจัง

OpenAPI เข้ามาแก้ปัญหานี้โดยทำให้ API ไม่ใช่แค่ route ที่รันได้ แต่เป็น ข้อตกลงที่ตรวจสอบได้ ว่า endpoint นี้รับอะไร ส่งอะไร, field ไหนจำเป็น, status code ไหนควรเกิด, auth แบบไหนถูกต้อง, และเมื่อมีการเปลี่ยนแปลงจะกระทบใครบ้าง

บทความนี้จะพาไปดู OpenAPI ในมุมของงานจริง เน้นวิธีคิดแบบ contract-first และตัวอย่างโค้ดที่ทีมเอาไปปรับใช้ต่อได้ทันที


OpenAPI คืออะไร

OpenAPI คือมาตรฐานสำหรับอธิบาย API ในรูปแบบ machine-readable โดยทั่วไปจะเขียนเป็น YAML หรือ JSON จุดสำคัญไม่ใช่แค่ “มีเอกสาร” แต่คือเอกสารนั้นสามารถถูกนำไปใช้ต่อได้หลายทาง เช่น

  • render หน้า docs ให้ทีมอ่าน
  • generate client SDK
  • ใช้ตรวจ schema ของ request/response
  • ใช้เทียบ breaking change ระหว่าง version
  • ใช้เป็น input สำหรับ mock server หรือ contract testing

พูดอีกแบบคือ OpenAPI ทำให้ API spec กลายเป็น artifact ที่ใช้งานได้จริง ไม่ใช่แค่ข้อความอธิบาย


ทำไมงานจริงควรคิดแบบ contract-first

แนวคิด contract-first คือเขียนหรือกำหนด spec ก่อน แล้วให้ implementation วิ่งตาม spec แทนที่จะปล่อยให้โค้ดเป็นต้นฉบับเพียงอย่างเดียว

ข้อดีในงานจริงมีชัดมาก

1) frontend กับ backend คุยกันได้เร็วขึ้น

ก่อน backend ทำเสร็จ frontend ก็อ่าน spec แล้วเริ่มทำ integration layer หรือ mock data ได้เลย

2) ลดข้อถกเถียงเรื่อง field

เวลาไม่มี spec กลาง การคุยจะกลายเป็น “นึกว่า endpoint นี้ส่งมาแบบนั้น” แต่ถ้ามี schema ชัด ทุกคนดูเอกสารชุดเดียวกัน

3) ลด regression ตอนแก้ response

ถ้าระบบมี validation และ contract diff checking จะเห็นทันทีว่าเปลี่ยน field แบบ breaking หรือไม่

4) QA และ integrator ทำงานง่ายขึ้น

ทีมทดสอบเห็นได้ว่าควรลองค่าอะไรบ้าง, required field คืออะไร, status code ที่ควรคาดหวังคืออะไร


โครงสร้างสำคัญของ OpenAPI

ตัวอย่างไฟล์ OpenAPI แบบย่อ:

openapi: 3.1.0
info:
  title: Orders API
  version: 1.0.0
servers:
  - url: https://api.example.com
paths:
  /orders:
    post:
      summary: Create an order
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
      responses:
        '201':
          description: Order created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderResponse'
components:
  schemas:
    CreateOrderRequest:
      type: object
      required: [customerId, items]
      properties:
        customerId:
          type: string
        items:
          type: array
          minItems: 1
          items:
            type: object
            required: [sku, quantity]
            properties:
              sku:
                type: string
              quantity:
                type: integer
                minimum: 1
    OrderResponse:
      type: object
      required: [id, status]
      properties:
        id:
          type: string
        status:
          type: string
          enum: [pending, confirmed, cancelled]

ส่วนที่ควรรู้ให้แม่นคือ

  • info อธิบายชื่อ API และ version
  • servers ระบุ base URL
  • paths ระบุ endpoint และ method
  • requestBody อธิบาย payload ที่รับ
  • responses อธิบายผลลัพธ์ที่ส่งกลับ
  • components.schemas รวม schema ที่ reuse ได้

หลักคิดก่อนลงมือเขียน spec

หลายทีมพลาดตรงที่รีบเขียน YAML ก่อนตอบคำถามเชิงออกแบบให้ชัด ทำให้ spec สวยแต่ใช้งานจริงลำบาก

ก่อนเขียน ควรถามให้ชัดว่า

  • resource หลักของระบบคืออะไร
  • endpoint นี้แทน action หรือ resource state
  • response ต้องเสถียรแค่ไหน
  • field ไหนเป็น internal field ที่ไม่ควร expose
  • validation rule ไหนควรอยู่ใน contract และ rule ไหนอยู่ใน business logic
  • breaking change แบบไหนยอมรับได้หรือไม่ได้

ตัวอย่างเช่น ถ้ากำลังทำระบบ booking อย่าเพิ่งเริ่มจาก route ยาว ๆ แบบ

POST /create-booking-for-customer

แต่ควรถอยมาคิดก่อนว่า resource หลักคือ bookings และ operation หลักคือ create, read, update status, cancel หรือไม่

แนวนี้จะทำให้ API โตต่อได้ง่ายกว่า เช่น

POST /bookings
GET /bookings/{bookingId}
POST /bookings/{bookingId}/cancel

ตั้งชื่อ schema และ field ให้คงเส้นคงวา

OpenAPI ช่วยไม่ได้ถ้าทีมตั้งชื่อไม่เป็นระบบ

ตัวอย่างที่ควรระวัง:

  • endpoint หนึ่งใช้ customerId แต่อีก endpoint ใช้ customer_id
  • response หนึ่งใช้ createdAt อีกอันใช้ created_at
  • บางที่ส่ง true/false แต่บางที่ใช้ YES/NO

ในงานจริงควรกำหนด style กลาง เช่น

  • JSON field ใช้ camelCase
  • path parameter ใช้ชื่อชัด เช่น bookingId, orderId
  • วันเวลาใช้ ISO 8601
  • enum ต้องกำหนดค่าตายตัวและอธิบายความหมาย

ตัวอย่าง schema ที่ชัด:

components:
  schemas:
    BookingStatus:
      type: string
      enum:
        - pending_payment
        - confirmed
        - assigned
        - in_progress
        - completed
        - cancelled

การแยก enum ออกมาเป็น schema กลางช่วยให้หลาย endpoint ใช้สถานะชุดเดียวกันได้


แยกให้ชัดระหว่าง validation ใน contract กับ business rule

นี่เป็นจุดที่สำคัญมาก

OpenAPI เหมาะกับการบอกว่า payload ต้องมีรูปแบบอะไร เช่น

  • เป็น string หรือ integer
  • ต้องมี field ไหนบ้าง
  • min/max length เท่าไร
  • enum อะไรบ้าง
  • format เป็น email หรือ date-time หรือไม่

แต่กฎอย่างเช่น

  • ลูกค้าคนนี้ถูก blacklist หรือไม่
  • worker คนนี้รับงานซ้อนเวลาได้หรือไม่
  • booking นี้ยกเลิกได้หลังชำระเงินหรือไม่

สิ่งเหล่านี้เป็น business rule ไม่ใช่ schema validation ธรรมดา อย่าพยายามยัดทุกอย่างลง OpenAPI เพราะสุดท้าย spec จะรกและคนอ่านจะสับสน


ตัวอย่าง OpenAPI สำหรับ endpoint สมัคร inquiry

openapi: 3.1.0
info:
  title: Inquiry API
  version: 1.0.0
paths:
  /api/v1/inquiries:
    post:
      summary: Create a new inquiry
      tags: [Inquiries]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateInquiryRequest'
      responses:
        '201':
          description: Inquiry created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateInquiryResponse'
        '400':
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
components:
  schemas:
    CreateInquiryRequest:
      type: object
      required:
        - name
        - businessType
        - contact
        - websiteType
        - goal
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 120
        businessType:
          type: string
          minLength: 2
          maxLength: 120
        contact:
          type: string
          minLength: 3
          maxLength: 255
        websiteType:
          type: string
          minLength: 2
          maxLength: 120
        goal:
          type: string
          minLength: 2
          maxLength: 500
        pageEstimate:
          type: string
          maxLength: 120
        features:
          type: string
          maxLength: 1000
        existingWebsite:
          type: string
          maxLength: 255
        budget:
          type: string
          maxLength: 120
        timeline:
          type: string
          maxLength: 120
        details:
          type: string
          maxLength: 3000
    CreateInquiryResponse:
      type: object
      required: [success, inquiry]
      properties:
        success:
          type: boolean
        inquiry:
          type: object
          required: [id, createdAt]
          properties:
            id:
              type: string
            createdAt:
              type: string
              format: date-time
    ErrorResponse:
      type: object
      required: [success, error]
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
            message:
              type: string

จุดสำคัญคือ request/response ถูกนิยามชัด ไม่ต้องเดาว่าฟิลด์ไหนอาจจะมีหรือไม่มี


ใช้ OpenAPI กับ Express/Node.js อย่างไร

ในโลก Node.js มีได้หลายแนว แต่แนวที่ใช้ได้จริงบ่อยคือ

  1. เขียนไฟล์ spec แยกไว้เอง
  2. ใช้ middleware validate request จาก spec
  3. ใช้ Swagger UI แสดง docs
  4. ทำ test ให้เช็คว่า implementation ยังตรงกับ contract

ตัวอย่างโครงสร้างไฟล์

src/
  app.ts
  routes/
    inquiries.ts
openapi/
  openapi.yaml

ติดตั้ง package

npm install express swagger-ui-express yamljs express-openapi-validator

app.ts

import express from "express";
import swaggerUi from "swagger-ui-express";
import YAML from "yamljs";
import * as OpenApiValidator from "express-openapi-validator";

import { inquiryRouter } from "./routes/inquiries";

const app = express();
const apiSpec = YAML.load("./openapi/openapi.yaml");

app.use(express.json());

app.use(
  OpenApiValidator.middleware({
    apiSpec: "./openapi/openapi.yaml",
    validateRequests: true,
    validateResponses: false,
  })
);

app.use("/docs", swaggerUi.serve, swaggerUi.setup(apiSpec));
app.use("/api/v1/inquiries", inquiryRouter);

app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
  res.status(err.status || 500).json({
    success: false,
    error: {
      code: err.code || "INTERNAL_ERROR",
      message: err.message || "Unexpected error",
      details: err.errors || undefined,
    },
  });
});

export { app };

routes/inquiries.ts

import { Router } from "express";

const inquiryRouter = Router();

inquiryRouter.post("/", async (req, res, next) => {
  try {
    const payload = req.body;

    const inquiry = {
      id: crypto.randomUUID(),
      createdAt: new Date().toISOString(),
      ...payload,
    };

    return res.status(201).json({
      success: true,
      inquiry: {
        id: inquiry.id,
        createdAt: inquiry.createdAt,
      },
    });
  } catch (error) {
    return next(error);
  }
});

export { inquiryRouter };

เมื่อมี request ที่ไม่ตรง schema เช่น name สั้นเกินไป หรือไม่มี goal middleware จะ reject ก่อนเข้า business logic ทำให้ validation กลายเป็นระบบกลางแทนการเขียนเช็คกระจัดกระจายทุก route


ควร validate response ด้วยไหม

ในระบบที่เริ่มมีหลายคนแก้ backend พร้อมกัน การเปิด response validation ในบาง environment มีประโยชน์มาก เพราะช่วยจับได้ว่า implementation เผลอส่ง field ผิด type หรือส่ง structure ไม่ตรง contract

แต่ใน production ต้องพิจารณาเรื่อง overhead ด้วย บางทีมจะเปิดเฉพาะ test/staging เพื่อจับปัญหาก่อน deploy

แนวทางที่ใช้งานได้จริงคือ

  • validate request ใน runtime เสมอ
  • validate response ใน test หรือ staging
  • ใช้ schema-based tests กัน regression

การแยก spec ให้อ่านง่ายเมื่อระบบเริ่มโต

พอ API ใหญ่ขึ้น ไม่ควรยัดทุกอย่างไว้ไฟล์เดียวจนอ่านยาก ควรแยกเป็นหลายไฟล์ เช่น

openapi/
  openapi.yaml
  paths/
    inquiries.yaml
    bookings.yaml
    payments.yaml
  schemas/
    inquiry.yaml
    booking.yaml
    error.yaml

ตัวอย่าง openapi.yaml แบบรวม reference:

openapi: 3.1.0
info:
  title: Internal Platform API
  version: 1.0.0
paths:
  /api/v1/inquiries:
    $ref: './paths/inquiries.yaml#/inquiriesCollection'
components:
  schemas:
    CreateInquiryRequest:
      $ref: './schemas/inquiry.yaml#/CreateInquiryRequest'
    CreateInquiryResponse:
      $ref: './schemas/inquiry.yaml#/CreateInquiryResponse'
    ErrorResponse:
      $ref: './schemas/error.yaml#/ErrorResponse'

การแยกแบบนี้ช่วยให้แต่ละโดเมนดูแลง่ายขึ้นและ review ง่ายกว่ามาก


เวอร์ชันของ API ควรจัดการอย่างไร

OpenAPI ไม่ได้แก้เรื่อง versioning ให้อัตโนมัติ แต่ช่วยทำให้การเปลี่ยนแปลงถูกมองเห็นชัดขึ้น

หลักคิดที่ควรใช้คือ

  • เพิ่ม optional field มักเป็น non-breaking
  • ลบ field, เปลี่ยน type, เปลี่ยน enum เป็น breaking
  • เปลี่ยนความหมายของ field เดิมแม้ชื่อไม่เปลี่ยน ก็ถือว่าอันตราย

ตัวอย่าง breaking change ที่เจอบ่อย:

{
  "status": "paid"
}

แล้วเปลี่ยนเป็น

{
  "paymentStatus": "paid"
}

frontend เดิมพังทันทีถ้ายังอ่าน status

เพราะฉะนั้นควรมีวินัยว่าเมื่อมี breaking change จริง ให้

  • แตก version เช่น /api/v2/...
  • หรือทำ deprecation period ก่อนถอด field เก่า
  • บันทึก changelog ให้ integrator ตามได้

ตัวอย่าง error response ที่ควรมาตรฐาน

หลายระบบเอกสารสวย แต่ error response ไม่สม่ำเสมอ จน frontend ต้องเขียนเงื่อนไขพิเศษเต็มไปหมด

ควรกำหนด shape กลาง เช่น

ErrorResponse:
  type: object
  required: [success, error]
  properties:
    success:
      type: boolean
      example: false
    error:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          example: VALIDATION_FAILED
        message:
          type: string
          example: Request validation failed
        details:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              message:
                type: string

เมื่อทุก endpoint ส่ง error ใกล้เคียงกัน ระบบฝั่ง client จะจัดการได้ง่ายขึ้นมาก


OpenAPI ไม่ใช่แค่ docs แต่เป็นฐานของ automation

เมื่อ spec เริ่มนิ่ง คุณจะเริ่มต่อยอดได้อีกหลายอย่าง เช่น

1) Generate typed client

frontend ไม่ต้องเขียน type ซ้ำเองทุก endpoint

2) Mock server

ใช้ทดสอบ flow ก่อน backend เสร็จจริง

3) Contract testing

เช็คว่า implementation ยังตรง spec หลังมีการแก้ endpoint

4) Change review

เวลาจะ merge PR ที่แก้ spec ทีมเห็นได้ทันทีว่า field ไหนถูกเพิ่ม ลบ หรือเปลี่ยน


ตัวอย่าง test เช็ค contract แบบง่าย

ถ้าอยากเช็คอย่างน้อยว่า route สำคัญยังส่ง shape ถูก สามารถเขียน integration test ได้ เช่น

import request from "supertest";
import { app } from "../src/app";

describe("POST /api/v1/inquiries", () => {
  it("should create inquiry successfully", async () => {
    const response = await request(app)
      .post("/api/v1/inquiries")
      .send({
        name: "Apichet Janya",
        businessType: "Insurance Technology",
        contact: "apichet@example.com",
        websiteType: "Corporate Website",
        goal: "Generate leads and build trust",
      });

    expect(response.status).toBe(201);
    expect(response.body.success).toBe(true);
    expect(typeof response.body.inquiry.id).toBe("string");
    expect(typeof response.body.inquiry.createdAt).toBe("string");
  });

  it("should reject invalid payload", async () => {
    const response = await request(app)
      .post("/api/v1/inquiries")
      .send({
        name: "A",
      });

    expect(response.status).toBe(400);
    expect(response.body.success).toBe(false);
  });
});

แม้ test ชุดนี้จะไม่ validate spec ตรง ๆ แบบเต็มระบบ แต่ช่วยบังคับให้ implementation ไม่ drift จาก contract ง่ายเกินไป


ข้อผิดพลาดที่ทีมเจอบ่อยเมื่อเริ่มใช้ OpenAPI

เขียน spec หลังโค้ดเสร็จตลอด

สุดท้าย spec จะตามไม่ทันของจริง และกลายเป็นเอกสารเก่า

เขียน schema หยาบเกินไป

เช่นใช้ type: object กว้าง ๆ โดยไม่กำหนด required field หรือ enum เลย ทำให้เอกสารแทบไม่มีพลัง

ยัด business rule ทุกอย่างลง spec

ทำให้ spec อ่านยากและยังบังคับใช้ไม่ได้จริงทั้งหมด

ไม่กำหนด error shape กลาง

client integration จะยุ่งมากเมื่อแต่ละ endpoint ล้มเหลวคนละรูปแบบ

ไม่มีระบบ review การเปลี่ยนแปลง contract

เปลี่ยน response แล้ว merge เลย โดยไม่รู้ว่ามี consumer พังหรือไม่


แนวทางใช้งานในทีมแบบค่อย ๆ โต

ถ้าทีมยังไม่ได้ใช้ OpenAPI อย่างจริงจัง ไม่จำเป็นต้องพลิกทั้งระบบในวันเดียว ลำดับที่ทำได้จริงคือ

  1. เริ่มจาก endpoint สำคัญที่มีคนใช้ร่วมหลายฝ่าย
  2. กำหนด request/response schema ให้ชัด
  3. เปิด docs ให้ทีมใช้ spec เดียวกัน
  4. เพิ่ม request validation
  5. ค่อยเพิ่ม response validation และ contract testing
  6. สร้างกติกาว่า PR ที่เปลี่ยน contract ต้องแก้ spec เสมอ

แนวทางนี้ทำให้ adoption เกิดได้จริงกว่าการประกาศว่าทุก endpoint ต้องสมบูรณ์ 100% ตั้งแต่วันแรก


สรุป

OpenAPI มีคุณค่าไม่ใช่เพราะมันทำให้เอกสาร API ดูสวย แต่เพราะมันทำให้ทีมมี สัญญากลาง ที่ชัดเจนและตรวจสอบได้

เมื่อใช้แบบจริงจัง มันจะช่วยให้

  • frontend, backend, QA, integrator คุยกันง่ายขึ้น
  • validation มีมาตรฐาน
  • breaking change ถูกมองเห็นเร็วขึ้น
  • เอกสารไม่หลุดจาก implementation ง่าย
  • การขยายระบบในระยะยาวมีวินัยมากขึ้น

ถ้าระบบของคุณเริ่มโต มีหลาย service หรือมีหลายคนแตะ API พร้อมกัน การทำ OpenAPI แบบ contract-first ไม่ใช่งานเอกสารเกินจำเป็น แต่เป็นเครื่องมือควบคุมความเสี่ยงของระบบโดยตรง

💬 Chat (ตอบเร็ว)