DEV Community

SEN LLC
SEN LLC

Posted on

Converting curl to fetch, axios, Python, Go, Ruby, and 5 More — All in the Browser

Converting curl to fetch, axios, Python, Go, Ruby, and 5 More — All in the Browser

"Copy as cURL" is the fastest way to share an HTTP request. But once someone sends you the curl command, you usually want it in your actual language. This tool parses real-world curl (from browser DevTools, from Stack Overflow, from docs) and outputs equivalent code in 10 different HTTP clients.

The challenge isn't the code generation — each target language is about 30 lines of string templating. The challenge is parsing curl correctly. Shell quoting, line continuations, multi-flag combinations, -d vs --data-raw, implicit Content-Type detection — all the edge cases that make one-off regex parsers break.

🔗 Live demo: https://sen.ltd/portfolio/curl-converter/
📦 GitHub: https://github.com/sen-ltd/curl-converter

Screenshot

Features:

  • 10 target languages: fetch, axios, XHR, HTTPie, Python requests, Python http.client, Node http, PHP curl, Go net/http, Ruby net/http
  • Shell-aware tokenizer
  • Real curl flag parsing (-X, -H, -d, -u, -F, --cookie, etc.)
  • Backslash line continuations
  • Auto-detect JSON vs form-encoded bodies
  • Basic auth conversion
  • Japanese / English UI
  • Zero dependencies, 124 tests

Shell-aware tokenization

You can't just split on spaces. curl -H "Content-Type: application/json" has a space inside the quoted header value. Line continuations with \ break tokens across lines:

export function tokenize(command) {
  const tokens = [];
  let current = '';
  let i = 0;
  let quote = null; // '"' or "'" or null

  while (i < command.length) {
    const c = command[i];

    if (quote) {
      if (c === quote) {
        quote = null;
        i++;
      } else if (c === '\\' && quote === '"') {
        // Escape inside double quotes
        current += command[i + 1];
        i += 2;
      } else {
        current += c;
        i++;
      }
    } else {
      if (c === '"' || c === "'") {
        quote = c;
        i++;
      } else if (c === '\\' && (command[i + 1] === '\n' || command[i + 1] === '\r')) {
        // Line continuation
        i += 2;
      } else if (/\s/.test(c)) {
        if (current) { tokens.push(current); current = ''; }
        i++;
      } else {
        current += c;
        i++;
      }
    }
  }
  if (current) tokens.push(current);
  return tokens;
}
Enter fullscreen mode Exit fullscreen mode

Single quotes don't process escapes (like POSIX). Double quotes do. Backslash followed by newline eats both characters. Whitespace outside quotes terminates a token.

Parsing the flags

Once you have tokens, walk the array and match known curl flags:

export function parseCurl(command) {
  const tokens = tokenize(command).slice(1); // drop "curl"
  const result = { url: '', method: 'GET', headers: {}, body: null, auth: null };

  for (let i = 0; i < tokens.length; i++) {
    const t = tokens[i];
    if (t === '-X' || t === '--request') {
      result.method = tokens[++i];
    } else if (t === '-H' || t === '--header') {
      const [name, ...rest] = tokens[++i].split(':');
      result.headers[name.trim()] = rest.join(':').trim();
    } else if (t === '-d' || t === '--data' || t === '--data-raw' || t === '--data-binary') {
      result.body = tokens[++i];
      if (result.method === 'GET') result.method = 'POST';
    } else if (t === '-u' || t === '--user') {
      const [user, pass] = tokens[++i].split(':');
      result.auth = { user, pass };
      // Auto-inject Basic auth header
      result.headers['Authorization'] = 'Basic ' + btoa(`${user}:${pass}`);
    } else if (!t.startsWith('-')) {
      result.url = t;
    }
  }
  return result;
}
Enter fullscreen mode Exit fullscreen mode

Note the side effects: -d implicitly sets POST, -u injects an Authorization header. These match curl's actual behavior.

The fetch generator

Once you have the parsed model, each generator is a template:

export function toFetch(p) {
  const opts = [];
  opts.push(`  method: '${p.method}',`);
  if (Object.keys(p.headers).length > 0) {
    opts.push('  headers: {');
    for (const [k, v] of Object.entries(p.headers)) {
      opts.push(`    '${k}': ${JSON.stringify(v)},`);
    }
    opts.push('  },');
  }
  if (p.body) {
    const formatted = tryFormatJSON(p.body);
    opts.push(`  body: ${formatted},`);
  }
  return `fetch('${p.url}', {\n${opts.join('\n')}\n})\n  .then(r => r.json())\n  .then(data => console.log(data));`;
}
Enter fullscreen mode Exit fullscreen mode

The tryFormatJSON helper attempts to pretty-print if the body parses as JSON; otherwise returns the raw string wrapped in quotes. This produces readable output for API calls while falling back gracefully for form data.

Different languages, same model

Each generator reads from the same parsed model and produces idiomatic code:

  • Python requests: requests.post(url, headers=..., json=...)
  • Go: http.NewRequest, req.Header.Add, client.Do
  • Ruby: Net::HTTP::Post.new, then set headers and body

10 generators × ~30 lines each = 300 lines of code. Most of the complexity lives in the parser; the generators are just string templates.

Series

This is entry #61 in my 100+ public portfolio series.

Top comments (0)