iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
Build on AWS

AWS架構師的自我修養:30天雲端系統思維實戰指南系列 第 26

Day 13-1 | 跨團隊協作設計:技術文件、OpenAPI、共用契約 : API 文檔化與團隊協作標準建立 - 可文檔版控的視覺化商業邏輯具象(1):共用契約 (Shared Contract)

  • 分享至 

  • xImage
  •  

可文檔版控的視覺化商業邏輯具象

在經歷第一階段商業邏輯的素描後,我們大致知道了我們的系統與他的系統邊界,為了讓不同背景的團隊成員能夠高效協作,我們需要有一個實體的、非抽象化的文件檔作為依據。就像樂團演奏時需要樂譜一樣,軟體開發團隊需要清晰的技術規範來確保和諧協作。想像一下,我們正在建造一座大型購物中心,需要建築師、電工、水管工、室內設計師等多個專業團隊協作,如果沒有統一的藍圖和溝通標準,每個團隊都按自己的理解施工,最終會是一場災難。

在軟體開發中也是如此:

  • 前端團隊需要知道如何向後端請求資料
  • 後端團隊需要明確提供什麼格式的資料
  • 測試團隊需要了解系統如何運作
  • 產品團隊需要確認功能是否符合需求

接下來就可以將實際業務情境具象化成文檔協助我們在開發流程中不斷地比照路徑圖確認自己並沒有跑偏。大致上的參考順序會是:

共用契約 (Shared Contract) => 系統交互介面(OpenAPI) => 技術文件 (Technical Documentation)

以下將按照順序分別說明應用情境與範例

1. 共用契約 (Shared Contract)

簡單來說,共用契約就像是建築藍圖中的「IPLC 電路規格」或「國際 PVC-U 水管尺寸標準」。它有著最基礎的規格依據讓負責不同部分的工班(開發團隊)可以獨立施工,確保最終所有零件都能完美地組裝在一起,從而避免溝通不良和整合時的混亂。常見的約定層面有 : 資料格式約定通訊協定約定錯誤處理約定

例如:

  • 使用 JSON SchemaGraphQL Schema 來明確規定一個 respond 物件必須包含 isSuccesselementerrorMessage... 等欄位以及它們的資料型別。
  • 使用常見的 HTTP/RESTful API 還是效能更好的 gRPC。
  • 所有 API 發生錯誤時,都回傳固定的錯誤碼和訊息格式,讓呼叫方能一致地處理。

這個共同的認知文件是一份跨團隊的共同語言,幾乎所有參與產品開發的技術和產品團隊都會以不同方式使用到它。

前端團隊 (Frontend Team) / **行動應用團隊 (Mobile App Team) ** 可以使用契約產生 Mock Data (模擬資料),在後端 API 還沒完成時也能獨立開發和測試,避免在整合時才發現「後端給的資料格式跟我想的不一樣」。契約告訴他們可以向後端請求什麼資料、需要用什麼格式發送請求,以及會收到什麼格式的回應,接下來才能根據契約定義的資料結構來開發 UI 介面。

後端團隊 (Backend Team) 是 API 的「提供者」,契約是他們需要履行的承諾與規格書。一旦出現差異,所有團隊包括且不限於前端 、其它後端(AI 專長或是圖像專長)、測試、產品與 DevOps 全部都會受到影響,所以作為 API 開發的明確指引,確保提供的資料格式、路徑、錯誤碼都符合約定。

測試團隊 (Test Team / QA) 需要根據契約中的請求/回應格式、HTTP 狀態碼和錯誤定義,來撰寫自動化測試驗證標準(例如:smoking test)驗證 API 的實際行為是否與契約描述的完全一致 - 契約是撰寫測試案例的黃金標準

產品團隊 (Product Team / PM) 是需求的「定義者」,契約文件幫助他們確認技術實現是否符合業務需求。雖然他們不看程式碼,但可以透過 OpenAPI (Swagger) 文件這種視覺化的契約來了解 API 功能並確認 API 提供的欄位是否滿足前端畫面的需求,避免功能遺漏。

DevOps / SRE 團隊 契約文件能夠幫助他們理解系統間的互動,了解服務之間的通訊協定 (HTTP/gRPC),以配置正確的網路規則和監控,特別是在發生問題時,可以根據契約快速定位是哪個服務的溝通環節出了問題。

實作範例

openapi: 3.0.3
info:
  title: Shared Contract Library
  description: Common reusable components for API contracts
  version: 1.0.0

components:
  # =================
  # Common Parameters
  # =================
  parameters:
    PageNumber:
      name: page
      in: query
      description: Page number
      required: true
      schema:
        type: integer
        minimum: 1

    PageSize:
      name: pageSize
      in: query
      description: Number of items per page
      required: true
      schema:
        type: integer
        minimum: 1
        maximum: 100

    SortColumn:
      name: sortColumn
      in: query
      description: Sort column
      required: false
      schema:
        type: string
        default: id

    SortOrder:
      name: orderType
      in: query
      description: Sort order
      required: false
      schema:
        type: integer
        format: int32
        enum:
          - 0 # Ascending
          - 1 # Descending
        default: 0

    StartDate:
      name: startDate
      in: query
      description: Start date filter
      required: false
      schema:
        type: string
        format: date

    EndDate:
      name: endDate
      in: query
      description: End date filter
      required: false
      schema:
        type: string
        format: date

    EntityId:
      name: id
      in: path
      description: Entity ID
      required: true
      schema:
        type: string

  # =================
  # Common Responses
  # =================
  responses:
    Success:
      description: Operation successful
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/BaseResponse"

    Created:
      description: Resource created successfully
      content:
        application/json:
          schema:
            allOf:
              - $ref: "#/components/schemas/BaseResponse"
              - type: object
                properties:
                  id:
                    type: integer
                    description: The ID of the newly created resource

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

    BadRequest:
      description: Invalid input
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

    ValidationError:
      description: Validation failed
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ValidationErrorResponse"

  # =================
  # Common Schemas
  # =================
  schemas:
    # Base Response Structure
    BaseResponse:
      type: object
      required:
        - isSuccess
        - message
      properties:
        isSuccess:
          type: boolean
          description: Indicates if the operation was successful
        message:
          type: string
          description: Response message
        errors:
          type: object
          nullable: true
          default: null
          description: Error details if any

    # Error Response
    ErrorResponse:
      allOf:
        - $ref: "#/components/schemas/BaseResponse"
        - type: object
          properties:
            isSuccess:
              enum: [false]

    # Validation Error Response
    ValidationErrorResponse:
      allOf:
        - $ref: "#/components/schemas/BaseResponse"
        - type: object
          properties:
            isSuccess:
              enum: [false]
            errors:
              type: object
              additionalProperties:
                type: array
                items:
                  type: string

    # Common Status Enum
    EntityStatus:
      type: integer
      format: int32
      enum:
        - 0 # Draft/Template
        - 1 # Pending Review
        - 2 # Approved/Active
        - 3 # Rejected
        - 4 # Processing/Executing
        - 5 # Completed/Executed
        - 6 # Disabled/Inactive
      description: |
        Standard entity status codes:
        - 0: Draft/Template
        - 1: Pending Review
        - 2: Approved/Active
        - 3: Rejected
        - 4: Processing/Executing
        - 5: Completed/Executed
        - 6: Disabled/Inactive

    # Date Range Filter
    DateRangeFilter:
      type: object
      properties:
        startDate:
          type: string
          format: date
          description: Filter start date
        endDate:
          type: string
          format: date
          description: Filter end date

    # Basic Entity Properties
    BaseEntity:
      type: object
      required:
        - id
      properties:
        id:
          type: integer
          description: Unique identifier
        status:
          $ref: "#/components/schemas/EntityStatus"

  # =================
  # Common Examples
  # =================
  examples:
    SuccessResponse:
      summary: Successful operation
      value:
        isSuccess: true
        message: "Operation completed successfully"
        errors: null

    ErrorResponse:
      summary: Error response
      value:
        isSuccess: false
        message: "Operation failed"
        errors:
          general: ["An error occurred"]

    PaginatedResponse:
      summary: Paginated list response
      value:
        isSuccess: true
        message: "Data retrieved successfully"
        errors: null
        result:
          page: 1
          totalCount: 100
          datas: []

    SelectOptions:
      summary: Select options list
      value:
        isSuccess: true
        message: "Options retrieved successfully"
        errors: null
        element:
          - id: "0"
            text: "Active"
            disabled: false
          - id: "1"
            text: "Inactive"
            disabled: false
# 共用契約範例:平台 API v1

**文件目的**:此契約定義了電商平台 API v1 的通用規範與核心端點 (`Product`) 的互動方式,作為前端、後端、測試與產品團隊的共同協作依據。

---

### 1. 通訊協定約定 (Communication Protocol)

- **協定**: 所有 API 均透過 `HTTPS` 提供服務。
- **基礎路徑 (Base URL)**: `https://api.your-ecommerce.com/v1`
- **認證 (Authentication)**: 所有需要授權的請求,都必須在 HTTP Header 中帶上 `Authorization` 欄位,其值為 `Bearer <YOUR_API_TOKEN>`。
- **請求與回應格式**: 所有請求與回應的 `body` 均使用 `application/json` 格式。

---

### 2. 標準回應格式與錯誤處理約定 (Standard Response & Error Handling)

為了讓所有客戶端(前端、APP)能用統一的方式處理 API 回應,我們定義一個標準的回應包裝 (Response Wrapper)。

#### 2.1. 標準回應結構 (Standard Response Schema)

所有 API 回應都必須遵循以下結構。我們可以使用 TypeScript Interface 來清晰地定義它:

/\*\*

- 標準 API 回應的共用契約
  \*/
  interface ApiResponse<T> {
  /\*\*
  - 請求是否成功
    \*/
    success: boolean;

/\*\*

- 成功時的回應資料 (泛型 T)
- 若請求失敗,此欄位為 null
  \*/
  data: T | null;

/\*\*

- 失敗時的錯誤資訊物件
- 若請求成功,此欄位為 null
  \*/
  error: ApiError | null;
  }

/\*\*

- 標準錯誤物件結構
  \*/
  interface ApiError {
  /\*\*
  - 內部定義的錯誤代碼,方便前端進行邏輯判斷
    \*/
    code: number;

/\*\*

- 人類可讀的錯誤訊息
  \*/
  message: string;
  }

#### 2.2. 通用錯誤代碼 (Common Error Codes)

| HTTP 狀態碼 | 內部代碼 (`code`) | 說明                                   |
| :---------- | :---------------- | :------------------------------------- |
| `400`       | `40001`           | 請求參數驗證失敗 (Invalid Parameters)  |
| `401`       | `40101`           | 未經授權 (Unauthorized)                |
| `403`       | `40301`           | 權限不足 (Forbidden)                   |
| `404`       | `40401`           | 請求的資源不存在 (Resource Not Found)  |
| `500`       | `50000`           | 伺服器內部錯誤 (Internal Server Error) |

---

### 3. 具體端點契約:取得商品資訊

現在,我們將上述通用約定應用到一個具體的端點上。

**端點**: `GET /products/{productId}`

**描述**: 根據提供的 `productId` 取得單一商品的詳細資訊。

#### 3.1. 請求 (Request)

- **路徑參數 (Path Parameter)**:
  - `productId` (string, format: uuid): 商品的唯一識別碼。

#### 3.2. 回應 (Responses)

- **資料格式約定 (Data Schema)**: 首先定義 `Product` 物件的契約。

  // 商品物件的共用契約
  interface Product {
  id: string; // UUID
  name: string;
  description: string;
  price: number;
  currency: 'TWD' | 'USD';
  stock: number;
  imageUrl: string;
  createdAt: string; // ISO 8601 format date string
  }

- **成功回應 (200 OK)**:
  當商品成功找到時,HTTP 狀態碼為 `200`,回應 `body` 遵循 `ApiResponse<Product>` 結構。

json
// Response Body (200 OK)
{
"success": true,
"data": {
"id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"name": "高效能無線機械鍵盤",
"description": "提供極致的打字體驗與 RGB 燈效。",
"price": 3200,
"currency": "TWD",
"stock": 150,
"imageUrl": "https://cdn.your-ecommerce.com/images/keyboard.jpg",
"createdAt": "2025-09-18T10:00:00Z"
},
"error": null
}

- **失敗回應 (404 Not Found)**:
  當 `productId` 對應的商品不存在時,HTTP 狀態碼為 `404`,回應 `body` 遵循 `ApiResponse<null>` 結構。

json
// Response Body (404 Not Found)
{
"success": false,
"data": null,
"error": {
"code": 40401,
"message": "商品不存在"
}
}

- **失敗回應 (400 Bad Request)**:
  當 `productId` 格式不正確(不是有效的 UUID)時,HTTP 狀態碼為 `400`。

json
// Response Body (400 Bad Request)
{
"success": false,
"data": null,
"error": {
"code": 40001,
"message": "請求參數驗證失敗: productId 必須是有效的 UUID 格式"
}
}

---

### 如何使用這份契約

- **後端團隊**:以此為規格書,實作 `GET /products/{productId}` 端點,確保回傳的 JSON 結構完全符合契約。
- **前端團隊**:在後端還在開發時,就可以根據這份契約建立 `Product` 的 TypeScript 型別,並使用 Mock Server 模擬成功和失敗的回應來開發商品詳情頁面。
- **測試團隊**:撰寫自動化測試案例,分別驗證 200, 404, 400 等情境下的回應是否與契約一致。

上一篇
Day 13-0 | 跨團隊協作設計:技術文件、OpenAPI、共用契約 : API 文檔化與團隊協作標準建立 - 前言: 商業邏輯的素描
系列文
AWS架構師的自我修養:30天雲端系統思維實戰指南26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言