API ネーミングワークフロー

API Request & Response Case Converter

アプリ内部では camelCase を維持し、request/response 境界で snake_case JSON と相互変換します。両方向をテストし、Fetch または Axios の実装例をコピーできます。

Response: snake_case → camelCase · Request: camelCase → snake_case

API 境界の方向

snake_case API → camelCase アプリ
アプリ用 camelCase JSON
{
  "userProfile": {
    "firstName": "Ada",
    "createdAt": "2026-09-12"
  }
}
変更されたキー: 3ペイロード変換はブラウザ内で実行されます。

API 境界コードを生成

JSON HTTP クライアントのパターンを選択します。生成コードは送信データを 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 ペイロード専用です。FormData、Blob、ストリーム、ファイルなどの非 JSON ボディはこのマッパーの対象外にしてください。

命名変換を API 境界に集約

コンポーネント、hook、store、ドメインコードは1つの内部命名規則を保ち、外部 JSON だけを境界で変換します。

1. 受信レスポンスを正規化

サーバー JSON を解析後、アプリが使う前に snake_case キーを camelCase へ再帰変換します。

2. 送信リクエストを正規化

JSON にシリアライズする前に、アプリの camelCase キーを API が期待する snake_case へ変換します。

3. 非 JSON ボディは変換しない

FormData、Blob、ストリーム、ファイル、任意のクラスインスタンスを JSON キーマッパーに通さないでください。

stop path、regex 除外、diff、ローカル JSON が必要ですか?

高度なオブジェクト単位ルールは JSON Key Case Converter で確認し、同じ境界ルールをアプリへ実装できます。

JSON Key Case Converter を開く

API ワークフロー実装ガイド

HTTP クライアント、query function、serializer、model alias に合わせた実装ガイドを選べます。