cover

前言:API 是前後端之間的契約

流程概覽

flowchart LR
    A[資源建模<br/>Resource Modeling] --> B[URL 設計<br/>RESTful 命名]
    B --> C[Request / Response<br/>格式定義]
    C --> D[錯誤處理<br/>統一格式]
    D --> E[認證機制<br/>JWT / OAuth]
    E --> F[API 文件化<br/>OpenAPI]

    style A fill:#4CAF50,color:#fff
    style B fill:#2196F3,color:#fff
    style C fill:#FF9800,color:#fff
    style D fill:#F44336,color:#fff
    style E fill:#9C27B0,color:#fff
    style F fill:#009688,color:#fff

在現代軟體架構中,API(Application Programming Interface)扮演著前端與後端之間的契約角色。一份設計良好的 API,就像一份清晰的合約——雙方都能依據它各自獨立開發、測試與部署,而不需要時刻溝通實作細節。

反之,一份設計混亂的 API 會導致前端工程師不斷猜測回傳格式、後端工程師頻繁處理相容性問題,最終整個團隊陷入無止盡的聯調地獄。

好的 API 設計應該具備以下特質:

  • 一致性(Consistency):命名風格、回傳格式、錯誤處理全部統一
  • 可預測性(Predictability):看到 URL 就能推測行為
  • 安全性(Security):適當的認證與授權機制
  • 可文件化(Documentability):能自動產生清楚的文件

本文將從 RESTful 設計原則、認證機制到 API 文件化三大面向,完整介紹 API 設計的最佳實踐。


API 請求生命週期

一個典型的 API 請求會經過多層處理。以下是從客戶端發出請求到取得資料庫回應的完整流程:

sequenceDiagram
    participant C as Client (Browser/App)
    participant A as Auth Layer
    participant G as API Gateway
    participant CT as Controller
    participant S as Service Layer
    participant D as Database

    C->>A: 1. 發送請求 + Token
    A->>A: 2. 驗證 Token / API Key
    alt 認證失敗
        A-->>C: 401 Unauthorized
    end
    A->>G: 3. 轉發已驗證的請求
    G->>G: 4. Rate Limiting / 路由
    alt 超過速率限制
        G-->>C: 429 Too Many Requests
    end
    G->>CT: 5. 路由到對應 Controller
    CT->>CT: 6. 參數驗證 / 反序列化
    alt 參數無效
        CT-->>C: 400 Bad Request
    end
    CT->>S: 7. 呼叫 Service 處理業務邏輯
    S->>D: 8. 查詢 / 寫入資料庫
    D-->>S: 9. 回傳資料
    S-->>CT: 10. 封裝業務結果
    CT-->>G: 11. 序列化回應
    G-->>C: 12. 回傳 HTTP Response

每一層都有其職責:Auth Layer 負責身份驗證、API Gateway 負責流量控制與路由、Controller 負責參數校驗、Service 負責業務邏輯、Database 負責資料持久化。這種分層架構讓每一層都可以獨立測試與替換。


核心概念一:RESTful 設計原則

URL 命名慣例

RESTful API 的核心思想是將一切視為「資源(Resource)」,URL 代表資源的位置,HTTP 方法代表對資源的操作。

模式說明範例
/resources資源集合GET /users 取得所有使用者
/resources/:id單一資源GET /users/42 取得 ID 為 42 的使用者
/resources/:id/sub-resources巢狀資源GET /users/42/orders 取得該使用者的訂單
/resources/:id/sub-resources/:subId巢狀單一資源GET /users/42/orders/7 取得特定訂單

命名規範:

  • 使用名詞而非動詞:/users 而非 /getUsers
  • 使用複數形式:/users 而非 /user
  • 使用 kebab-case:/user-profiles 而非 /userProfiles
  • 避免過深的巢狀:最多兩層,超過的用查詢參數或獨立端點處理

HTTP 方法語義

方法語義冪等性安全性典型用途
GET讀取資源查詢列表或單筆資料
POST建立資源新增使用者、提交訂單
PUT完整取代資源更新整個使用者資料
PATCH部分更新資源只更新使用者的 email
DELETE刪除資源刪除一筆訂單

冪等性(Idempotency) 意味著同一個請求執行一次或多次,結果都一樣。GETPUTDELETE 都是冪等的,但 POST 不是——連續兩次 POST 可能會建立兩筆資料。

HTTP 狀態碼

狀態碼是 API 與客戶端溝通的重要語言,必須正確使用:

2xx 成功回應:

狀態碼含義使用時機
200 OK請求成功GET 成功取得資料、PUT/PATCH 更新成功
201 Created資源已建立POST 成功建立新資源
204 No Content成功但無內容DELETE 成功刪除資源

4xx 客戶端錯誤:

狀態碼含義使用時機
400 Bad Request請求格式錯誤缺少必填欄位、JSON 格式不正確
401 Unauthorized未認證沒有提供 Token 或 Token 過期
403 Forbidden無權限Token 有效但權限不足
404 Not Found資源不存在查詢的 ID 不存在
409 Conflict資源衝突重複建立已存在的資源
422 Unprocessable Entity語義錯誤格式正確但業務邏輯不允許
429 Too Many Requests請求過多超過速率限制

5xx 伺服器錯誤:

狀態碼含義使用時機
500 Internal Server Error伺服器內部錯誤未預期的程式例外
502 Bad Gateway上游伺服器錯誤後端服務無回應
503 Service Unavailable服務暫時不可用維護中或過載

分頁、過濾與排序

當資源集合可能包含大量資料時,必須實作分頁機制:

Offset-based 分頁(傳統方式):

GET /users?page=2&limit=20
  • 優點:簡單直觀、可跳頁
  • 缺點:資料異動時可能產生重複或遺漏

Cursor-based 分頁(推薦用於即時資料流):

GET /users?cursor=eyJpZCI6MTAwfQ&limit=20
  • 優點:效能更好、不受資料異動影響
  • 缺點:無法跳到任意頁

過濾與排序:

GET /users?status=active&role=admin&sort=-created_at,name
  • 過濾:使用查詢參數 ?status=active
  • 排序:sort=field 升序,sort=-field 降序
  • 多重排序:逗號分隔 sort=-created_at,name

API 版本管理策略

API 演進是不可避免的,版本管理策略決定了如何處理向後相容性:

策略範例優點缺點
URL 路徑/v1/users直觀、容易路由URL 變得冗長
HeaderAccept: application/vnd.api+json;version=1URL 乾淨不易除錯、CDN 快取困難
Query 參數/users?version=1彈性高容易忘記傳遞

實務建議: URL 路徑版本 (/v1/) 是最常見且最推薦的做法,因為它最直觀、最容易在 API Gateway 層做路由分流。

統一錯誤回應格式

所有 API 錯誤都應該回傳一致的格式,讓前端能用統一的方式處理:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "輸入資料驗證失敗",
    "details": [
      {
        "field": "email",
        "message": "email 格式不正確"
      },
      {
        "field": "password",
        "message": "密碼長度至少 8 個字元"
      }
    ],
    "requestId": "req_abc123",
    "timestamp": "2024-09-15T10:30:00Z"
  }
}

requestId 對於除錯非常重要——前端回報問題時,後端可以透過這個 ID 快速追蹤到對應的日誌。

HATEOAS

HATEOAS(Hypermedia as the Engine of Application State)是 REST 的最高成熟度等級,回應中包含相關操作的連結:

{
  "id": 42,
  "name": "Alice",
  "links": {
    "self": "/users/42",
    "orders": "/users/42/orders",
    "update": "/users/42",
    "delete": "/users/42"
  }
}

理論上 HATEOAS 讓 API 更具自描述性,但在實務中,大多數前端應用不會動態解析這些連結——它們通常在編譯時就已經確定了 API 路徑。因此 HATEOAS 在多數專案中屬於「知道就好」的層級,除非你在設計一個需要高度動態探索的公開 API。


核心概念二:認證機制

Session-based vs Token-based

比較面向Session-basedToken-based
狀態有狀態(伺服器保存 Session)無狀態(客戶端持有 Token)
儲存位置伺服器記憶體 / Redis客戶端(Cookie / Header)
擴展性需要共享 Session Store天然支援水平擴展
跨域需處理 CORS + CookieBearer Token 較容易跨域
適用場景傳統 Server-rendered 應用SPA、行動應用、微服務

現代架構中,Token-based 認證已成為主流,尤其是 JWT(JSON Web Token)。

JWT(JSON Web Token)

JWT 由三段以 . 分隔的 Base64 編碼字串組成:

header.payload.signature
  • Header:演算法與 Token 類型,例如 {"alg": "HS256", "typ": "JWT"}
  • Payload:承載的資料(Claims),例如 {"sub": "user_42", "role": "admin", "exp": 1694780000}
  • Signature:使用密鑰對 Header + Payload 簽名,確保內容未被竄改

Access Token vs Refresh Token:

類型用途有效期儲存位置
Access Token存取 API 資源短(15 分鐘 ~ 1 小時)記憶體或 HttpOnly Cookie
Refresh Token換發新的 Access Token長(7 天 ~ 30 天)HttpOnly Secure Cookie

短效的 Access Token 降低了 Token 洩漏的風險——即使被盜也只有短暫的有效時間。Refresh Token 則讓使用者不需要頻繁重新登入。

OAuth 2.0

OAuth 2.0 是一個授權框架,常見兩種流程:

Authorization Code Flow(適用於有後端的 Web 應用):

  1. 使用者點擊「透過 Google 登入」
  2. 瀏覽器跳轉到 Google 授權頁面
  3. 使用者同意授權
  4. Google 回傳 Authorization Code 到後端
  5. 後端用 Code 向 Google 換取 Access Token
  6. 後端用 Access Token 取得使用者資訊

Client Credentials Flow(適用於服務對服務通訊):

  1. 服務 A 使用 Client ID + Client Secret 向授權伺服器請求 Token
  2. 授權伺服器回傳 Access Token
  3. 服務 A 用 Token 存取服務 B 的 API

API Key

API Key 是最簡單的認證方式,適用於服務對服務的通訊:

GET /api/data HTTP/1.1
X-API-Key: sk_live_abc123def456
  • 優點:實作簡單
  • 缺點:無法攜帶使用者身份資訊、洩漏風險較高
  • 適用場景:第三方整合、內部微服務、公開但需計量的 API

速率限制(Rate Limiting)

速率限制是防止 API 被濫用的關鍵機制,常見演算法:

  • Fixed Window:固定時間視窗(例如每分鐘 100 次)
  • Sliding Window:滑動時間視窗,更平滑
  • Token Bucket:令牌桶演算法,允許突發流量

回應 Header 應包含速率限制資訊:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 67
X-RateLimit-Reset: 1694780060

核心概念三:API 文件化

OpenAPI / Swagger 規範

OpenAPI Specification(前身為 Swagger)是描述 RESTful API 的標準格式。它讓你用 YAML 或 JSON 定義 API 的結構,並自動產生互動式文件。

自動產生 API 文件

常見的工具鏈:

  • Swagger UI:基於 OpenAPI spec 產生互動式文件頁面
  • Redoc:更美觀的文件呈現方式
  • swagger-jsdoc:從程式碼中的 JSDoc 註解自動產生 OpenAPI spec
  • tsoa / NestJS Swagger:從 TypeScript 裝飾器自動產生

API 測試工具

  • Postman:最廣泛使用的 API 測試工具,支援集合、環境變數、自動化測試
  • Insomnia:更輕量的替代方案,介面簡潔
  • Thunder Client:VS Code 擴充套件,不離開編輯器就能測試 API
  • curl / httpie:命令列工具,適合 CI/CD 整合

實務範例

範例一:Express.js RESTful Router

// routes/users.js
import { Router } from 'express';
import { authenticate } from '../middleware/auth.js';
import { validate } from '../middleware/validate.js';
import { createUserSchema, updateUserSchema } from '../schemas/user.js';
 
const router = Router();
 
// GET /v1/users - 取得使用者列表(支援分頁與過濾)
router.get('/', authenticate, async (req, res) => {
  const { page = 1, limit = 20, status, sort = '-created_at' } = req.query;
  const offset = (page - 1) * limit;
 
  const filters = {};
  if (status) filters.status = status;
 
  const [users, total] = await Promise.all([
    UserService.findAll({ filters, sort, limit, offset }),
    UserService.count(filters),
  ]);
 
  res.json({
    data: users,
    pagination: {
      page: Number(page),
      limit: Number(limit),
      total,
      totalPages: Math.ceil(total / limit),
    },
  });
});
 
// GET /v1/users/:id - 取得單一使用者
router.get('/:id', authenticate, async (req, res) => {
  const user = await UserService.findById(req.params.id);
  if (!user) {
    return res.status(404).json({
      error: {
        code: 'USER_NOT_FOUND',
        message: `使用者 ${req.params.id} 不存在`,
      },
    });
  }
  res.json({ data: user });
});
 
// POST /v1/users - 建立新使用者
router.post('/', authenticate, validate(createUserSchema), async (req, res) => {
  const user = await UserService.create(req.body);
  res.status(201).json({ data: user });
});
 
// PATCH /v1/users/:id - 部分更新使用者
router.patch('/:id', authenticate, validate(updateUserSchema), async (req, res) => {
  const user = await UserService.update(req.params.id, req.body);
  if (!user) {
    return res.status(404).json({
      error: {
        code: 'USER_NOT_FOUND',
        message: `使用者 ${req.params.id} 不存在`,
      },
    });
  }
  res.json({ data: user });
});
 
// DELETE /v1/users/:id - 刪除使用者
router.delete('/:id', authenticate, async (req, res) => {
  const deleted = await UserService.delete(req.params.id);
  if (!deleted) {
    return res.status(404).json({
      error: {
        code: 'USER_NOT_FOUND',
        message: `使用者 ${req.params.id} 不存在`,
      },
    });
  }
  res.status(204).send();
});
 
// GET /v1/users/:id/orders - 取得使用者的訂單
router.get('/:id/orders', authenticate, async (req, res) => {
  const { cursor, limit = 20 } = req.query;
  const orders = await OrderService.findByUserId(req.params.id, { cursor, limit });
 
  res.json({
    data: orders.items,
    pagination: {
      nextCursor: orders.nextCursor,
      hasMore: orders.hasMore,
    },
  });
});
 
export default router;

範例二:JWT 認證 Middleware

// middleware/auth.js
import jwt from 'jsonwebtoken';
 
const ACCESS_TOKEN_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_TOKEN_SECRET = process.env.JWT_REFRESH_SECRET;
 
// 產生 Token 對
export function generateTokens(user) {
  const accessToken = jwt.sign(
    {
      sub: user.id,
      email: user.email,
      role: user.role,
    },
    ACCESS_TOKEN_SECRET,
    { expiresIn: '15m' }
  );
 
  const refreshToken = jwt.sign(
    { sub: user.id },
    REFRESH_TOKEN_SECRET,
    { expiresIn: '7d' }
  );
 
  return { accessToken, refreshToken };
}
 
// 認證 Middleware
export function authenticate(req, res, next) {
  // 從 Authorization Header 取得 Token
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({
      error: {
        code: 'MISSING_TOKEN',
        message: '未提供認證 Token',
      },
    });
  }
 
  const token = authHeader.split(' ')[1];
 
  try {
    const decoded = jwt.verify(token, ACCESS_TOKEN_SECRET);
    req.user = {
      id: decoded.sub,
      email: decoded.email,
      role: decoded.role,
    };
    next();
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({
        error: {
          code: 'TOKEN_EXPIRED',
          message: 'Token 已過期,請使用 Refresh Token 換發新 Token',
        },
      });
    }
    return res.status(401).json({
      error: {
        code: 'INVALID_TOKEN',
        message: 'Token 無效',
      },
    });
  }
}
 
// 授權 Middleware(角色檢查)
export function authorize(...allowedRoles) {
  return (req, res, next) => {
    if (!req.user || !allowedRoles.includes(req.user.role)) {
      return res.status(403).json({
        error: {
          code: 'FORBIDDEN',
          message: '權限不足,無法執行此操作',
        },
      });
    }
    next();
  };
}
 
// Refresh Token 端點
export async function refreshAccessToken(req, res) {
  const { refreshToken } = req.body;
  if (!refreshToken) {
    return res.status(400).json({
      error: {
        code: 'MISSING_REFRESH_TOKEN',
        message: '未提供 Refresh Token',
      },
    });
  }
 
  try {
    const decoded = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET);
    const user = await UserService.findById(decoded.sub);
 
    if (!user) {
      return res.status(401).json({
        error: { code: 'USER_NOT_FOUND', message: '使用者不存在' },
      });
    }
 
    const tokens = generateTokens(user);
    res.json({ data: tokens });
  } catch (err) {
    return res.status(401).json({
      error: { code: 'INVALID_REFRESH_TOKEN', message: 'Refresh Token 無效或已過期' },
    });
  }
}

範例三:OpenAPI YAML 規格定義

# openapi.yaml
openapi: 3.0.3
info:
  title: User Management API
  description: 使用者管理 RESTful API
  version: 1.0.0
  contact:
    name: API Support
    email: api@example.com
 
servers:
  - url: https://api.example.com/v1
    description: Production
  - url: https://staging-api.example.com/v1
    description: Staging
 
paths:
  /users:
    get:
      summary: 取得使用者列表
      tags: [Users]
      security:
        - bearerAuth: []
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: status
          in: query
          schema:
            type: string
            enum: [active, inactive, suspended]
        - name: sort
          in: query
          schema:
            type: string
            default: '-created_at'
      responses:
        '200':
          description: 成功取得使用者列表
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '401':
          $ref: '#/components/responses/Unauthorized'
 
    post:
      summary: 建立新使用者
      tags: [Users]
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserInput'
      responses:
        '201':
          description: 使用者建立成功
        '400':
          $ref: '#/components/responses/BadRequest'
        '409':
          description: 使用者已存在
 
  /users/{id}:
    get:
      summary: 取得單一使用者
      tags: [Users]
      security:
        - bearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: 成功
        '404':
          $ref: '#/components/responses/NotFound'
 
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
 
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
          format: email
        name:
          type: string
        role:
          type: string
          enum: [user, admin]
        status:
          type: string
          enum: [active, inactive, suspended]
        createdAt:
          type: string
          format: date-time
 
    CreateUserInput:
      type: object
      required: [email, name, password]
      properties:
        email:
          type: string
          format: email
        name:
          type: string
          minLength: 2
        password:
          type: string
          minLength: 8
 
    Pagination:
      type: object
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
        totalPages:
          type: integer
 
  responses:
    BadRequest:
      description: 請求格式錯誤
    Unauthorized:
      description: 未認證或 Token 無效
    NotFound:
      description: 資源不存在

範例四:Rate Limiting Middleware

// middleware/rateLimit.js
 
// 簡易的記憶體型速率限制(生產環境建議使用 Redis)
const requestCounts = new Map();
 
/**
 * 建立速率限制 Middleware
 * @param {Object} options
 * @param {number} options.windowMs - 時間視窗(毫秒)
 * @param {number} options.max - 時間視窗內最大請求數
 * @param {string} options.message - 超限時的錯誤訊息
 * @param {Function} options.keyGenerator - 產生識別 key 的函式
 */
export function rateLimit({
  windowMs = 60 * 1000,
  max = 100,
  message = '請求過於頻繁,請稍後再試',
  keyGenerator = (req) => req.ip,
} = {}) {
  // 定期清理過期記錄
  setInterval(() => {
    const now = Date.now();
    for (const [key, record] of requestCounts.entries()) {
      if (now - record.windowStart > windowMs) {
        requestCounts.delete(key);
      }
    }
  }, windowMs);
 
  return (req, res, next) => {
    const key = keyGenerator(req);
    const now = Date.now();
    let record = requestCounts.get(key);
 
    // 如果沒有記錄或時間視窗已過期,重新開始計數
    if (!record || now - record.windowStart > windowMs) {
      record = { count: 0, windowStart: now };
      requestCounts.set(key, record);
    }
 
    record.count++;
 
    // 設定速率限制相關 Header
    const remaining = Math.max(0, max - record.count);
    const resetTime = Math.ceil((record.windowStart + windowMs) / 1000);
 
    res.set('X-RateLimit-Limit', String(max));
    res.set('X-RateLimit-Remaining', String(remaining));
    res.set('X-RateLimit-Reset', String(resetTime));
 
    if (record.count > max) {
      res.set('Retry-After', String(Math.ceil(windowMs / 1000)));
      return res.status(429).json({
        error: {
          code: 'RATE_LIMIT_EXCEEDED',
          message,
          retryAfter: Math.ceil((record.windowStart + windowMs - now) / 1000),
        },
      });
    }
 
    next();
  };
}
 
// 使用範例
// app.js
import express from 'express';
import { rateLimit } from './middleware/rateLimit.js';
 
const app = express();
 
// 全域速率限制:每分鐘 100 次
app.use(rateLimit({ windowMs: 60 * 1000, max: 100 }));
 
// 登入端點更嚴格:每 15 分鐘 5 次
app.use('/v1/auth/login', rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: '登入嘗試過於頻繁,請 15 分鐘後再試',
  keyGenerator: (req) => `login:${req.ip}`,
}));

常見問題與風險

1. API 命名不一致

問題: 團隊內不同工程師用不同風格命名 API,例如 /getUsers/user-list/fetch_orders

風險: 前端工程師無法預測 API 路徑,增加聯調成本。

解法:

  • 制定並文件化 API 命名規範(API Style Guide)
  • 在 Code Review 中嚴格把關
  • 使用 Linter 工具(如 spectral)檢查 OpenAPI spec 是否符合規範

2. JWT 儲存在 localStorage

問題: 許多教學文章將 JWT 存在 localStorage,但這極度不安全。

風險: 任何 XSS(跨站腳本攻擊)漏洞都能輕易竊取 localStorage 中的 Token。攻擊者只需注入一段 document.cookielocalStorage.getItem('token') 就能取得使用者的身份憑證。

解法:

  • Access Token 存在記憶體中(JavaScript 變數)
  • Refresh Token 存在 HttpOnly + Secure + SameSite=Strict 的 Cookie 中
  • 搭配 CSRF Token 防護

3. 沒有實作速率限制

問題: API 完全沒有速率限制,任何人都能無限制地呼叫。

風險:

  • DDoS 攻擊造成服務癱瘓
  • 暴力破解密碼
  • 爬蟲大量抓取資料
  • API 使用成本失控

解法:

  • 全域速率限制 + 針對敏感端點的更嚴格限制
  • 使用 API Gateway 層級的限制(如 AWS API Gateway、Nginx)
  • 對認證相關端點實作指數退避(Exponential Backoff)

4. 缺少 API 版本管理

問題: API 變更直接影響所有客戶端,沒有版本隔離。

風險: 後端的一次 breaking change 可能導致所有前端應用同時崩潰。

解法:

  • 從第一天就引入版本前綴 /v1/
  • 新版本上線後,舊版本維持至少一個 deprecation 週期
  • 在回應 Header 中加入 DeprecationSunset 標頭通知客戶端

5. 錯誤回應格式不統一

問題: 不同端點回傳不同格式的錯誤,有的是 { message: "..." },有的是 { error: "..." },有的直接回傳字串。

風險: 前端無法用統一的攔截器處理錯誤,每個 API 呼叫都要寫特殊的錯誤處理邏輯。

解法:

  • 定義全域的錯誤格式規範
  • 實作全域錯誤處理 Middleware
  • 所有錯誤都經過同一個格式化層再回傳

總結

API 設計不只是技術選擇,更是團隊協作的基礎。一份好的 API 設計能讓前後端並行開發、降低溝通成本、減少整合問題。記住以下原則:

  • 一致性優先:命名、格式、錯誤處理都要統一
  • 安全性不能妥協:正確使用 JWT、不把 Token 存在 localStorage、實作速率限制
  • 文件即契約:用 OpenAPI 規範定義 API,自動產生文件
  • 版本管理從第一天開始:未來的自己會感謝現在的決定

Proto 實踐對照

在 Django Proto 中,RESTful API 設計遵循本文的原則:使用 DRF ViewSet 實現資源路由、drf-spectacular 自動產生 Swagger 文件、統一的 { success, data, error } 回應格式、以及 Cursor-based 分頁。前端的 Vue 3 Proto 則用 Axios Interceptors 處理 Token 注入和 401 重試。詳見 Proto 規劃方法論


延伸閱讀