cURL to Fetch Converter
Paste a cURL command and get working fetch, async/await, Node.js or axios code, with the flags that have no JavaScript equivalent flagged explicitly. All processing happens locally in your browser.
Convert a cURL command into fetch or axios code
The command is tokenised the way a shell would read it, so single quotes, escaped characters and line continuations survive. Flags that have no JavaScript equivalent are called out rather than dropped.
fetch("https://api.example.com/v1/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer TOKEN_PLACEHOLDER",
},
body: JSON.stringify({
"sku": "AB-100",
"quantity": 2
}),
})
.then((response) => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then((data) => console.log(data))
.catch((error) => console.error(error));Credentials stay in the page. Only the request shape — method, header count, content type — is recorded for usage analytics; never URLs, tokens or body content.
| Part | Value |
|---|---|
| URL | https://api.example.com/v1/orders |
| Method | POST |
| Header: Content-Type | application/json |
| Header: Authorization | Bearer •••••••••••• |
What is the difference between cURL and fetch?
Both send HTTP requests, but they live in different worlds and that shapes what each one can express.
cURL is a command-line program. It has around 250 flags covering proxies, client certificates, retries, timeouts, output files, cookie jars and transfer resumption. It talks to the operating system's network stack directly.
fetch is a browser API, also built into Node.js since version 18. It is deliberately narrow, and some of what cURL does is not merely missing but forbidden: a browser will not let JavaScript skip certificate validation, set the Host header, or read a response the server has not permitted through CORS.
So conversion is a translation, not a mapping. The parts that transfer cleanly are the method, URL, headers, body and basic auth. The parts that do not are called out on the page rather than quietly dropped, because a silently missing --insecure or --max-time is how a converted request ends up behaving differently in production.
How to convert a command
- Paste the whole command Include every continuation line. Backslash continuations (Bash), caret continuations (cmd) and backtick continuations (PowerShell) are all joined automatically.
- Pick an output style A promise chain, async/await, Node.js, or axios. All four are generated from the same parsed request, so switching does not change the semantics.
- Read the parsed table It shows the URL, method, headers and form fields exactly as understood. If something is missing there, it is missing from the output too — that is where to look first when the result is not what you expected.
-
Check the warnings
Anything cURL does that fetch cannot is listed with the JavaScript alternative where one exists, such as
AbortSignal.timeout()in place of--max-time.
How the method is decided
cURL infers the method rather than requiring it, and the rules matter because they explain requests that appear to have no method at all.
| Command contains | Method used |
|---|---|
| Nothing special | GET |
-d, --data, -F or --data-urlencode |
POST |
-T / --upload-file |
PUT |
-I / --head |
HEAD |
-G with -d |
GET, with the data moved into the query string |
-X / --request |
Whatever you specify, overriding all of the above |
The -G case is the one people miss. curl -G https://api/search -d "q=coffee" is a GET with ?q=coffee appended, not a POST with a body. The generated code reflects that: the query lands in the URL and there is no body option.
Examples: how bodies are translated
JSON
curl -X POST https://api.example.com/v1/orders \
-H "Content-Type: application/json" \
-d '{"sku":"AB-100","quantity":2}'
becomes:
fetch("https://api.example.com/v1/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
"sku": "AB-100",
"quantity": 2
}),
})
Note that the body is reconstructed as an object inside JSON.stringify() rather than pasted as a string. That way the payload is editable and any syntax error shows up in your editor rather than at runtime.
Form-encoded
curl https://api.example.com/session -d "username=demo" -d "password=x"
Two -d flags are joined with &, exactly as cURL does, and the result is wrapped in URLSearchParams:
body: new URLSearchParams("username=demo&password=x"),
There is a useful detail here: when you pass a URLSearchParams object as the body, fetch sets Content-Type: application/x-www-form-urlencoded for you.
Multipart form
curl -X POST https://api.example.com/v1/upload \
-F "title=Quarterly report" \
-F "document=@report.pdf"
becomes a FormData object. The @report.pdf value cannot be translated literally, because a browser has no access to your filesystem by path:
const form = new FormData();
form.append("title", "Quarterly report");
form.append("document", /* File from an <input type="file"> */ file); // was @report.pdf
Do not set Content-Type when sending FormData. The multipart boundary is generated by the browser and must appear in the header. If you set the header yourself the boundary is missing and the server cannot parse the body. That is why the generated code omits it for form requests.
Basic auth
-u user:pass becomes an explicit header, because fetch has no auth option:
"Authorization": `Basic ${btoa("user:pass")}`,
In Node.js, btoa exists but Buffer.from("user:pass").toString("base64") is the more idiomatic choice.
Shell quoting is where conversion usually goes wrong
The command has to be read the way a shell reads it, and shells treat the two quote characters very differently.
| Quoting | Behaviour | Consequence |
|---|---|---|
'single' |
Everything is literal. No expansion, no escapes. | You cannot put a single quote inside, at all. |
"double" |
$VAR expands, and \" \\ \$ are escapes. |
A JSON body needs every internal " escaped. |
This is why JSON bodies are conventionally wrapped in single quotes — no escaping needed. And it explains the strangest-looking construct in cURL commands:
--data-raw '{"name":"O'"'"'Brien"}'
Reading that from the left: open single quote, then ' closes it, "'" is a double-quoted single quote, then ' reopens. The shell concatenates the adjacent pieces into one argument containing {"name":"O'Brien"}. Tokenising the command properly handles this; a regex-based parser does not.
Windows shells differ
cmd.exe has no single-quote quoting at all, so a command copied from a Linux tutorial fails there with confusing errors. PowerShell has its own rules and needs --% or careful escaping. If you copied a command on Windows and the JSON body looks mangled, that is usually the cause rather than the converter.
Flags that cannot be translated
| Flag | Why not | Closest equivalent |
|---|---|---|
-k / --insecure |
Browsers never allow skipping certificate checks | Fix the certificate, or use a trusted local CA |
-m / --max-time |
fetch has no timeout option | signal: AbortSignal.timeout(5000) |
--retry |
No retry support in fetch | A loop with backoff in your own code |
-x / --proxy |
Not configurable from page JavaScript | Environment or agent configuration in Node |
--compressed |
Not needed | Browsers and Node negotiate and decompress automatically |
-o file |
No filesystem access from the browser | Handle the response body in JavaScript |
--cert / --key |
Client certificates are not exposed to fetch | Handled by the OS or a Node agent |
There is also a category that converts fine but then fails at runtime for a different reason. A cURL command that works from your terminal can be blocked in the browser by CORS, because cURL is not subject to the same-origin policy. A request that returns 200 in a terminal and fails in the console with a CORS message is not a conversion problem — the server needs to send the right Access-Control-Allow-Origin response headers.
Cookies are similar. -b becomes a Cookie header, but browsers forbid setting that header from JavaScript. The generated code therefore also sets credentials: "include", which is how a browser is actually asked to send cookies.
Use cases
- Turning a documentation example into working code. Most API docs give cURL. This gets you to a request you can paste into a component.
- Reproducing a browser request. DevTools "Copy as cURL" produces a long command with many headers; converting it back gives you a clean starting point that keeps the ones that matter.
- Sharing a repro without a terminal. A colleague who works in the browser console can run the fetch version of a bug report directly.
- Migrating a shell script to Node. The Node.js output uses the built-in fetch available since Node 18, with no dependency to add.
- Checking what a long command actually sends. The parsed table is often more useful than the generated code, especially for a command with fifteen headers.
- Moving between fetch and axios. Both are generated from the same parse, so you can compare them side by side.
Inspecting the URL itself is a separate job — the URL Parser breaks down query parameters, and the JWT Debugger decodes a bearer token.
Privacy and credentials
Parsing and code generation happen in your browser. The command is not uploaded, and no request is sent to the URL in it — nothing is executed, only read.
What is recorded: usage analytics capture only the shape of the request — HTTP method, number of headers, content type, and whether a body was present. URLs, header values, tokens, credentials and body content are never included. Credential headers are also masked in the parsed table on screen so a token is not left visible in a screenshot or a screen share.
Even so, treat a real production token as compromised once it has been pasted into any browser tab. Rotating it afterwards costs very little.
Frequently Asked Questions
Why does my converted request fail with a CORS error?
Because cURL ignores the same-origin policy and browsers do not. The conversion is correct; the server simply has not sent Access-Control-Allow-Origin for your page's origin. Either the API needs to allow it, or the call belongs on your own server rather than in the browser.
Should I set Content-Type when sending FormData?
No. The multipart boundary is generated by the browser and must be part of the header value. If you set Content-Type yourself the boundary is missing and the server cannot parse the body, which usually surfaces as an empty or malformed request. The generated code deliberately omits it.
How do I add a timeout, since fetch has no option for it?
Pass an abort signal: signal: AbortSignal.timeout(5000). That is the modern equivalent of --max-time 5. Older code uses an AbortController with a setTimeout that calls controller.abort(). When a timeout flag is present in your command, the tool suggests the exact value.
Why is my single-quoted JSON body being mangled?
Almost always because the command was copied on Windows. cmd.exe has no single-quote quoting, so a Linux-style command with '{"a":1}' arrives with the quotes as literal characters. Rewrite the body with double quotes and escaped internal quotes, or run it in WSL or Git Bash.
Does it handle multiple -d flags?
Yes, joined with & exactly as cURL does. So -d "a=1" -d "b=2" produces the body a=1&b=2. If -G is also present, that combined string is appended to the URL as a query string instead of being sent as a body.
What happens to -F "file=@document.pdf"?
A FormData append is generated with a comment marking where the File goes, because a browser cannot read a file by path. Supply a File or Blob from an element, or from a drag-and-drop handler. In Node 18+ you can use fs.openAsBlob().
Is anything about my command sent to your servers?
No. Parsing and generation run entirely in the page. Usage analytics record only the request shape — method, header count, content type, whether a body was present — and never the URL, headers, tokens or body. Credential headers are masked on screen too.