
先講結論
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 / Scalar | API 設計 IDE |
| Postman | 支援從 OpenAPI 匯入 |
| Spectral | OpenAPI spec lint,CI 整合 |
| openapi-generator | 從 spec 自動生成各語言 SDK |
| Prism / msw | 從 spec 跑 mock server |
