API 命名工作流程

API Request & Response Case Converter

應用程式內部維持 camelCase,只在 request/response 邊界與 snake_case JSON 互轉。可直接測試兩個方向,並複製 Fetch 或 Axios 的實作模式。

回應:snake_case → camelCase · 請求:camelCase → snake_case

API 邊界方向

snake_case API → camelCase 應用程式
應用程式 camelCase JSON
{
  "userProfile": {
    "firstName": "Ada",
    "createdAt": "2026-09-12"
  }
}
已重新命名的鍵:3payload 轉換會在瀏覽器本機執行。

產生 API 邊界程式碼

選擇 JSON HTTP client 模式。產生的範例會把送出的應用程式物件轉成 snake_case,並把收到的 JSON 回應轉成 camelCase。

type JsonRecord = Record<string, unknown>

function isJsonObject(value: unknown): value is JsonRecord {
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
    return false
  }
  return Object.getPrototypeOf(value) === Object.prototype
}

function splitPrefix(key: string) {
  const prefix = key.match(/^[_$]+/u)?.[0] ?? ""
  return { prefix, value: key.slice(prefix.length) }
}

function splitWords(value: string) {
  return value
    .replace(/([A-Z]+)([A-Z][a-z])/gu, "$1 $2")
    .replace(/([a-z0-9])([A-Z])/gu, "$1 $2")
    .split(/[^A-Za-z0-9]+/u)
    .filter(Boolean)
}

function toCamelKey(key: string) {
  const { prefix, value } = splitPrefix(key)
  const words = splitWords(value).map((word) => word.toLowerCase())
  const [firstWord, ...restWords] = words
  if (!firstWord) return key
  return prefix + firstWord + restWords
    .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
    .join("")
}

function toSnakeKey(key: string) {
  const { prefix, value } = splitPrefix(key)
  const words = splitWords(value).map((word) => word.toLowerCase())
  return words.length ? prefix + words.join("_") : key
}

function mapJsonKeys(
  value: unknown,
  convertKey: (key: string) => string,
): unknown {
  if (Array.isArray(value)) {
    return value.map((item) => mapJsonKeys(item, convertKey))
  }

  if (!isJsonObject(value)) {
    return value
  }

  const output: JsonRecord = {}
  for (const [key, child] of Object.entries(value)) {
    const nextKey = convertKey(key)
    if (Object.hasOwn(output, nextKey)) {
      throw new Error(`Key collision after case conversion: ${nextKey}`)
    }
    output[nextKey] = mapJsonKeys(child, convertKey)
  }
  return output
}

export function keysToCamel(value: unknown) {
  return mapJsonKeys(value, toCamelKey)
}

export function keysToSnake(value: unknown) {
  return mapJsonKeys(value, toSnakeKey)
}

export async function apiJson<T>(
  url: string,
  options: { method?: string; body?: unknown } = {},
): Promise<T> {
  const response = await fetch(url, {
    method: options.method ?? (options.body === undefined ? "GET" : "POST"),
    headers: { "content-type": "application/json" },
    body:
      options.body === undefined
        ? undefined
        : JSON.stringify(keysToSnake(options.body)),
  })

  if (!response.ok) {
    throw new Error(`API request failed: ${response.status}`)
  }

  const wireData: unknown = await response.json()
  return keysToCamel(wireData) as T
}

產生的邊界專門處理 JSON payload。FormData、Blob、stream、檔案及其他非 JSON body 請保持在 mapper 之外。

把命名轉換集中在 API 邊界

component、hook、store 與 domain code 維持單一內部命名慣例,只有外部 JSON contract 在邊界轉換。

1. 正規化傳入回應

解析伺服器 JSON 後,在應用程式使用前把 snake_case 鍵遞迴轉為 camelCase。

2. 正規化送出請求

在 JSON 序列化前,把應用程式的 camelCase 鍵轉為 API 預期的 snake_case。

3. 非 JSON body 保持在 mapper 之外

不要把 FormData、Blob、stream、檔案或任意 class instance 當作 JSON 物件轉換。

需要 stop paths、regex 排除、diff 或本機 JSON 檔案?

使用 JSON Key Case Converter 測試進階物件規則,再把相同邊界政策帶入應用程式程式碼。

開啟 JSON Key Case Converter

API 工作流程實作指南

依 HTTP client、query function、serializer 或 model alias 層選擇對應實作指南。