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
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;
}
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;
}
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));`;
}
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.
- 📦 Repo: https://github.com/sen-ltd/curl-converter
- 🌐 Live: https://sen.ltd/portfolio/curl-converter/
- 🏢 Company: https://sen.ltd/

Top comments (0)