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 패턴을 선택하세요. 생성 예제는 앱의 outgoing object를 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, file 등 비 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, file 또는 임의 class instance를 JSON 키 mapper로 변환하지 마세요.

stop path, regex 제외, diff, 로컬 JSON이 필요한가요?

고급 객체 규칙은 JSON Key Case Converter에서 테스트한 뒤 동일한 경계 정책을 앱 코드에 적용하세요.

JSON Key Case Converter 열기

API 워크플로 구현 가이드

HTTP client, query function, serializer 또는 model alias 계층에 맞는 구현 가이드를 선택하세요.