cover

先講結論

OpenAPI(前身是 Swagger)解決的問題只有一個:前後端對 API 的理解應該有單一來源的真相(Single Source of Truth)

有了 OpenAPI spec:

  • 前端不用猜 API 格式,直接看文件(或跑 mock server)
  • QA 可以用 spec 做 contract testing
  • 可以自動生成各語言的 SDK
  • Swagger UI 直接變成可互動的 API 文件

沒有 OpenAPI:每個人對 API 的理解都略有差異,靠口頭或 Postman collection 傳遞,版本更新容易漏同步。


OpenAPI 3.0 結構

一個完整的 OpenAPI 文件由以下幾個部分組成:

openapi: 3.0.3          ← OpenAPI 版本(不是 API 版本)
info:                   ← API 基本資訊
servers:                ← 環境列表(prod / staging / local)
tags:                   ← 分類標籤
components:             ← 共用元件(schemas / parameters / security)
paths:                  ← API 端點定義(核心)

完整範本

以下是一個可直接使用的完整範本,涵蓋認證、使用者管理、訂單三個模組。

openapi: 3.0.3
 
info:
  title: "[系統名稱] API"
  description: |
    [系統名稱] 的 RESTful API 規格文件。
 
    ## 認證方式
    需要認證的 API 在 Header 帶入 Bearer Token:
    ```
    Authorization: Bearer {token}
    ```
 
    ## 錯誤格式
    所有錯誤回應統一格式:
    ```json
    {
      "code": "ERROR_CODE",
      "message": "人類可讀的錯誤說明",
      "details": {}
    }
    ```
 
    ## 版本策略
    版本號放在 URL path:`/api/v1/...`
  version: "1.0.0"
  contact:
    name: API Support
    email: api@example.com
 
servers:
  - url: https://api.example.com/api/v1
    description: 正式環境
  - url: https://staging-api.example.com/api/v1
    description: 測試環境
  - url: http://localhost:3000/api/v1
    description: 本機開發
 
tags:
  - name: Auth
    description: 認證相關
  - name: Users
    description: 使用者管理
  - name: Orders
    description: 訂單管理
 
# ─── 共用元件 ──────────────────────────────────────────
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
 
  schemas:
    Error:
      type: object
      properties:
        code:
          type: string
          example: "VALIDATION_ERROR"
        message:
          type: string
          example: "Email 格式不正確"
        details:
          type: object
 
    Pagination:
      type: object
      properties:
        page:
          type: integer
          example: 1
        per_page:
          type: integer
          example: 20
        total:
          type: integer
          example: 150
        total_pages:
          type: integer
          example: 8
 
    User:
      type: object
      properties:
        id:
          type: integer
          example: 1
        email:
          type: string
          format: email
          example: "user@example.com"
        name:
          type: string
          example: "王小明"
        status:
          type: string
          enum: [active, inactive, banned]
          example: "active"
        created_at:
          type: string
          format: date-time
          example: "2026-01-01T00:00:00Z"
 
    Order:
      type: object
      properties:
        id:
          type: integer
          example: 1001
        user_id:
          type: integer
          example: 1
        status:
          type: string
          enum: [pending, confirmed, shipped, delivered, cancelled]
        total:
          type: number
          format: decimal
          example: 1299.00
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderItem'
        created_at:
          type: string
          format: date-time
 
    OrderItem:
      type: object
      properties:
        product_id:
          type: integer
        product_name:
          type: string
        quantity:
          type: integer
        unit_price:
          type: number
          format: decimal
 
  parameters:
    PageParam:
      name: page
      in: query
      schema:
        type: integer
        default: 1
    PerPageParam:
      name: per_page
      in: query
      schema:
        type: integer
        default: 20
        maximum: 100
 
# ─── API 路徑 ──────────────────────────────────────────
paths:
 
  # ── Auth ──────────────────────────────────────────────
  /auth/login:
    post:
      tags: [Auth]
      summary: 使用者登入
      description: 使用 Email + 密碼登入,取得 Access Token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                  example: "user@example.com"
                password:
                  type: string
                  format: password
                  minLength: 8
                  example: "Password123"
      responses:
        "200":
          description: 登入成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                    example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
                  expires_in:
                    type: integer
                    description: Token 有效秒數
                    example: 1800
                  user:
                    $ref: '#/components/schemas/User'
        "401":
          description: 帳號或密碼錯誤
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: "INVALID_CREDENTIALS"
                message: "帳號或密碼錯誤"
        "429":
          description: 登入嘗試次數過多
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: "TOO_MANY_ATTEMPTS"
                message: "登入嘗試次數過多,請 30 分鐘後再試"
 
  /auth/logout:
    post:
      tags: [Auth]
      summary: 登出
      security:
        - BearerAuth: []
      responses:
        "200":
          description: 登出成功
        "401":
          description: 未認證
 
  /auth/refresh:
    post:
      tags: [Auth]
      summary: 刷新 Token
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refresh_token]
              properties:
                refresh_token:
                  type: string
      responses:
        "200":
          description: 刷新成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                  refresh_token:
                    type: string
                  expires_in:
                    type: integer
        "401":
          description: Refresh Token 無效或已過期
 
  # ── Users ──────────────────────────────────────────────
  /users/me:
    get:
      tags: [Users]
      summary: 取得當前使用者資料
      security:
        - BearerAuth: []
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        "401":
          description: 未認證
 
    patch:
      tags: [Users]
      summary: 更新當前使用者資料
      security:
        - BearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                  minLength: 2
                  maxLength: 50
      responses:
        "200":
          description: 更新成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        "422":
          description: 資料驗證失敗
 
  # ── Orders ─────────────────────────────────────────────
  /orders:
    get:
      tags: [Orders]
      summary: 取得訂單列表
      security:
        - BearerAuth: []
      parameters:
        - $ref: '#/components/parameters/PageParam'
        - $ref: '#/components/parameters/PerPageParam'
        - name: status
          in: query
          description: 篩選訂單狀態
          schema:
            type: string
            enum: [pending, confirmed, shipped, delivered, cancelled]
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Order'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
 
    post:
      tags: [Orders]
      summary: 建立訂單
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  type: array
                  minItems: 1
                  items:
                    type: object
                    required: [product_id, quantity]
                    properties:
                      product_id:
                        type: integer
                      quantity:
                        type: integer
                        minimum: 1
                note:
                  type: string
                  maxLength: 500
      responses:
        "201":
          description: 訂單建立成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        "400":
          description: 庫存不足
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: "INSUFFICIENT_STOCK"
                message: "部分商品庫存不足"
                details:
                  out_of_stock_products: [101, 205]
 
  /orders/{id}:
    get:
      tags: [Orders]
      summary: 取得單一訂單
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        "404":
          description: 訂單不存在
 
    patch:
      tags: [Orders]
      summary: 取消訂單
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [action]
              properties:
                action:
                  type: string
                  enum: [cancel]
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        "400":
          description: 訂單狀態不允許此操作
        "404":
          description: 訂單不存在

常用語法速查

Schema 型別

type: string          # 字串
type: integer         # 整數
type: number          # 數字(含小數)
type: boolean         # 布林
type: array           # 陣列
type: object          # 物件
 
format: email         # Email 格式驗證
format: date-time     # ISO 8601 日期時間
format: password      # 密碼(Swagger UI 會遮罩)
format: decimal       # 用於金額欄位

驗證規則

minLength: 2
maxLength: 50
minimum: 1
maximum: 999
enum: [pending, confirmed, shipped]
required: [email, password]   # 在 object 層級標注必填欄位
nullable: true                # 允許 null 值

$ref 引用

# 引用 components 內的 schema
$ref: '#/components/schemas/User'
 
# 引用 components 內的 parameter
$ref: '#/components/parameters/PageParam'
 
# 跨文件引用(適合大型專案拆分)
$ref: './schemas/user.yaml#/User'

Code First vs Design First

方式說明優缺點
Design First先寫 OpenAPI spec,再實作強制前後端對齊 / 需要維護 spec 同步
Code First先寫程式,再從程式碼生成 spec開發快 / spec 可能落後實作

現代框架的 Code First 工具:

  • FastAPI — 從 Python type hints 自動生成 OpenAPI
  • NestJS@nestjs/swagger 裝飾器生成
  • Spring Boot — springdoc-openapi
  • Django — drf-spectacular

推薦:API 對外或跨團隊時用 Design First;內部快速迭代可以 Code First,但要確保 spec 跟得上。


工具生態

工具用途
Swagger UI互動式 API 文件瀏覽
Redoc更漂亮的文件介面,適合對外
Stoplight / ScalarAPI 設計 IDE
Postman支援從 OpenAPI 匯入
SpectralOpenAPI spec lint,CI 整合
openapi-generator從 spec 自動生成各語言 SDK
Prism / msw從 spec 跑 mock server

相關文章