NNicheStackTutorials
← All tutorials
Frontend Tools

caseflip: Fix camelCase ↔ snake_case Without Leaving the Browser

Freelance API integration needed batch field renames. Built a 90-line case converter that handles edge cases like HTTPHeader and iOS-style acronyms.

·10 min read·Frontend Tools
caseflip: Fix camelCase ↔ snake_case Without Leaving the Browser cover

Context: Client's REST API returned user_id but frontend types used userId. Needed a quick converter during a call — not another npm package.

Full tool

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Case Converter</title>
  <style>
    body { font-family: system-ui; max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
    textarea { width: 100%; min-height: 140px; font-family: monospace; }
    .btns { display: flex; flex-wrap: wrap; gap: .5rem; margin: .75rem 0; }
    button { padding: .4rem .8rem; cursor: pointer; }
  </style>
</head>
<body>
  <h1>Case Converter</h1>
  <textarea id="in" placeholder="user_id or userId"></textarea>
  <div class="btns">
    <button data-mode="camel">camelCase</button>
    <button data-mode="pascal">PascalCase</button>
    <button data-mode="snake">snake_case</button>
    <button data-mode="kebab">kebab-case</button>
    <button data-mode="constant">CONSTANT_CASE</button>
    <button data-mode="title">Title Case</button>
  </div>
  <textarea id="out" readonly></textarea>
  <script>
    const words = (s) =>
      s.replace(/([a-z])([A-Z])/g, '$1 $2')
       .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
       .split(/[\s_\-.]+/)
       .filter(Boolean)
       .map(w => w.toLowerCase());

    const convert = {
      camel: (w) => w.map((x, i) => i ? x[0].toUpperCase() + x.slice(1) : x).join(''),
      pascal: (w) => w.map(x => x[0].toUpperCase() + x.slice(1)).join(''),
      snake: (w) => w.join('_'),
      kebab: (w) => w.join('-'),
      constant: (w) => w.join('_').toUpperCase(),
      title: (w) => w.map(x => x[0].toUpperCase() + x.slice(1)).join(' '),
    };

    const input = document.getElementById('in');
    const output = document.getElementById('out');

    document.querySelectorAll('[data-mode]').forEach(btn => {
      btn.addEventListener('click', () => {
        const mode = btn.dataset.mode;
        const lines = input.value.split('\n');
        output.value = lines.map(line => {
          const t = line.trim();
          if (!t) return '';
          return convert[mode](words(t));
        }).join('\n');
      });
    });
  </script>
</body>
</html>

Bug I shipped first

HTTPResponse became h t t p response because I split on every capital letter.

Fix: the regex ([A-Z]+)([A-Z][a-z]) keeps acronyms together before splitting.

Batch mode

One identifier per line. Pasting 200 GraphQL field names → click snake_case → copy for OpenAPI spec.

Real output

Input:  getUserProfileURL
snake:  get_user_profile_url
kebab:  get-user-profile-url

Verify

  • user_id → camel → userId
  • XMLHttpRequest → snake → xml_http_request (acceptable tradeoff)
  • Empty lines preserved in batch output

Host as static HTML. No dependencies. Works offline after first load.

Tags: String ToolsVanilla JScamelCasesnake_case