# Convert cURL to fetch or Axios Without Breaking the Request

> Map cURL flags to fetch and Axios options, avoid quoting and content-type mistakes, and verify the converted request against the original.

**Quick answer:** A cURL command is a set of request options: method, URL, headers, body, authentication, and redirect behavior. Convert it by mapping each flag to the matching fetch or Axios option, then compare the actual request headers and body rather than assuming the conversion is correct.

## Key takeaways

- curl -d sends a body and defaults to application/x-www-form-urlencoded; JSON needs an explicit Content-Type header.
- fetch does not reject on HTTP 4xx or 5xx; check response.ok explicitly.
- Browser fetch enforces CORS, while curl and server-side fetch do not.
- Axios serializes JSON automatically when you pass an object as data.
- Always compare the converted request with the original using a echo or request inspector.

---

Copying a cURL command from API documentation is easy. Turning it into a correct `fetch` or Axios call is where small details matter: content types, body encoding, authentication, redirects, and error handling.

This guide maps the common flags, shows the equivalent code, and explains how to verify the conversion instead of guessing.

## Read the cURL command as options

A cURL command is not a single opaque string. It is a sequence of flags:

```bash
curl -X POST 'https://api.example.com/orders' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer TOKEN' \
  -d '{"item":"keyboard","quantity":2}'
```

The equivalent request is:

- Method: `POST`
- URL: `https://api.example.com/orders`
- Headers: `Content-Type: application/json`, `Authorization: Bearer TOKEN`
- Body: `{"item":"keyboard","quantity":2}`

Once you list those four things, the JavaScript version is straightforward.

The four-line summary omits information that changes the request. `-d` implies `POST` when `-X` is absent, `-I` implies `HEAD`, and `-G` moves the data into the query string instead of the body. Other flags do not map to browser fetch at all: `--resolve`, `--cacert`, `--cert`, `--proxy`, `--http1.1`, and `--connect-timeout` configure the transport, not the HTTP request. They belong to a server-side client or a proxy; dropping them silently is how "it worked with curl" becomes a different request in the browser.

Shell quoting is part of the request too. Single quotes protect `$` and `!`; double quotes allow expansion, so a command containing `$TOKEN` may have been substituted in the documentation's environment or may expand to an empty string in yours. Multiline JSON passed to `-d '...'` keeps its newlines only because of the quotes; retyping it without them changes the body.

## The flag-to-option map

| cURL flag | Meaning | fetch | Axios |
| --- | --- | --- | --- |
| `-X POST` | HTTP method | `method: "POST"` | `method: "post"` |
| `-H 'Name: value'` | Request header | `headers: { Name: "value" }` | `headers: { Name: "value" }` |
| `-d '...'` | Request body | `body: "..."` | `data: "..."` |
| `--data-raw` | Body without file interpretation | `body: "..."` | `data: "..."` |
| `--data-binary @file` | Binary body from a file | `body: fileBlob` | `data: fileBlob` |
| `-F 'name=value'` | Multipart form field | `FormData` | `FormData` |
| `-u user:pass` | Basic authentication | `Authorization` header | `auth: { username, password }` |
| `-b 'name=value'` | Cookie | `credentials` or `Cookie` header | `withCredentials` or header |
| `-L` | Follow redirects | `redirect: "follow"` (default) | `maxRedirects` (default follows) |
| `-k` | Skip TLS verification | Not available in browsers | `httpsAgent` option (Node only) |
| `--json '{...}'` | JSON body plus JSON content type (curl 7.82+) | `body: JSON.stringify(...)` plus header | `data: {...}` |
| `-G` | Move data into the query string | Build with `URLSearchParams` | `params` |
| `--data-urlencode` | URL-encode a form field | `URLSearchParams` | `params` |
| `-I` | HEAD request | `method: "HEAD"` | `method: "head"` |
| `-m 10` | Whole-transfer timeout | `signal: AbortSignal.timeout(10000)` | `timeout: 10000` or signal |
| `--retry 3` | Retry failed transfers | Implement a loop with backoff | Implement an interceptor |
| `--compressed` | Request compressed response | Handled automatically | Handled automatically |

Two mappings deserve a warning. `--data-urlencode` encodes each field as form data; fetch has no equivalent flag, so build a `URLSearchParams` instead of appending the string to the URL by hand. `-m 10` limits the whole transfer, while Axios `timeout` is interpreted differently across adapters; for a precise deadline, use `AbortSignal.timeout` with both fetch and Axios. `--retry` has no direct equivalent at all, and a retry policy must know whether the request is idempotent. Retrying a POST can create duplicate orders, so add retries only for safe methods or when the API supports idempotency keys, with jittered backoff.

## GET with headers

```bash
curl 'https://api.example.com/orders?status=open' \
  -H 'Accept: application/json' \
  -H 'Authorization: Bearer TOKEN'
```

```js
const response = await fetch("https://api.example.com/orders?status=open", {
  method: "GET",
  headers: {
    Accept: "application/json",
    Authorization: "Bearer TOKEN"
  }
});

if (!response.ok) {
  throw new Error(`Request failed: ${response.status} ${await response.text()}`);
}

const data = await response.json();
```

Axios:

```js
const { data } = await axios.get("https://api.example.com/orders", {
  params: { status: "open" },
  headers: { Authorization: "Bearer TOKEN" }
});
```

Axios serializes `params` into the query string. With fetch, build the query string with `URLSearchParams`.

The query-string details cause quiet mismatches. `URLSearchParams` encodes spaces as `+`, which is correct for form encoding but may surprise an API that expects `%20`. Repeated keys require `append`: an object literal cannot represent `status=open&status=paid`, because the second value overwrites the first. Axios serializes arrays with brackets by default (`status[]=open`), so an API that expects repeated bare keys needs a custom `paramsSerializer`.

fetch also refuses a body on GET or HEAD. `new Request(url, { method: "GET", body: "..." })` throws `TypeError: Request with GET/HEAD method cannot have body`. curl will happily send `-X GET -d '...'`; the browser API will not, so that request needs a different design rather than a different option.

## POST JSON

```bash
curl -X POST 'https://api.example.com/orders' \
  -H 'Content-Type: application/json' \
  -d '{"item":"keyboard","quantity":2}'
```

```js
const response = await fetch("https://api.example.com/orders", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ item: "keyboard", quantity: 2 })
});
```

Axios:

```js
const { data } = await axios.post(
  "https://api.example.com/orders",
  { item: "keyboard", quantity: 2 },
  { headers: { "Content-Type": "application/json" } }
);
```

Axios sets the JSON content type automatically when `data` is an object. With fetch, you must set it and stringify the body yourself.

`JSON.stringify` is not a transparent copy of the cURL body. Properties with `undefined` values disappear, `Date` objects become ISO strings through `toJSON`, `BigInt` values throw `TypeError: Do not know how to serialize a BigInt`, and circular references throw `Converting circular structure to JSON`. If the original body was hand-written, the stringified version may also differ in whitespace, key order after object reconstruction, and escaping of non-ASCII characters. Most servers do not care; signature schemes do.

Axios adds the JSON content type only when it serializes an object itself. If you pass an already-stringified body, set the header explicitly, or the request may go out as `text/plain` and fail content negotiation.

## The content-type trap

`curl -d` defaults to `application/x-www-form-urlencoded`, not JSON. If the command includes `-H 'Content-Type: application/json'`, the header wins. If it does not, the server receives form-encoded data.

For form-encoded requests, use `URLSearchParams`:

```js
const body = new URLSearchParams({
  grant_type: "client_credentials",
  client_id: "abc",
  client_secret: "secret"
});

const response = await fetch("https://auth.example.com/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body
});
```

The [form URL encoded converter](/tools/form-urlencoded) helps inspect the encoded value.

curl's `-d @file` reads the file and strips newlines and carriage returns; `--data-binary @file` preserves the exact bytes. A pretty-printed JSON fixture sent with `-d @payload.json` arrives as one line, which is usually harmless, but a body that depends on newlines — NDJSON, a signature, a PEM block — is corrupted. Use `--data-binary` when the file's bytes matter.

`URLSearchParams` used as a fetch body sets `Content-Type: application/x-www-form-urlencoded;charset=UTF-8` automatically. Setting a bare `application/x-www-form-urlencoded` header manually overrides that value, and some servers are strict about the charset parameter. Prefer letting the object set the header, or include the parameter explicitly. If both a proxy and the application add a content type, the duplicate header is another source of 415 responses.

## Basic authentication

```bash
curl -u 'user:password' 'https://api.example.com/me'
```

```js
const credentials = btoa("user:password");
const response = await fetch("https://api.example.com/me", {
  headers: { Authorization: `Basic ${credentials}` }
});
```

Axios:

```js
const { data } = await axios.get("https://api.example.com/me", {
  auth: { username: "user", password: "password" }
});
```

In a browser, `btoa` fails on non-Latin characters. Encode the credentials as UTF-8 bytes first. The [Basic auth header generator](/tools/basic-auth) handles this for you.

RFC 7617 splits the decoded credential at the first colon: the user-id cannot contain a colon, while the password can. A username containing a colon produces a credential the server parses incorrectly, usually as a different user or as an authentication failure. Non-ASCII credentials have no universally agreed charset; the standard allows a `charset` parameter, and without it some servers assume UTF-8 while others assume Latin-1. If the API documents one, encode those bytes deliberately instead of assuming `btoa` did the right thing.

Basic authentication in a browser is visible to the user and to anything that can read the page; it is not a secret-storage mechanism. The password is also sent on every request. Use it only over TLS, and prefer a short-lived token when the API offers one.

## File uploads

```bash
curl -X POST 'https://api.example.com/upload' \
  -F 'file=@report.pdf' \
  -F 'description=Monthly report'
```

```js
const form = new FormData();
form.append("file", fileInput.files[0]);
form.append("description", "Monthly report");

const response = await fetch("https://api.example.com/upload", {
  method: "POST",
  body: form
});
```

Do not set `Content-Type` manually. The browser adds `multipart/form-data` with the correct boundary. Setting the header yourself usually produces a request the server cannot parse.

`-F` carries more than a field name. `-F 'file=@report.pdf;type=application/pdf'` sets the part's content type, and `-F 'file=@photo.jpg;filename=renamed.jpg'` overrides the filename the server sees. In fetch, `form.append("file", blob, "renamed.jpg")` supplies the filename, and a `File` already carries its type; a plain `Blob` may need an explicit `type` when constructed. Multiple files under one field use repeated `append` calls with the same field name.

Fetch cannot report upload progress. If the UI needs a progress bar, use `XMLHttpRequest` or Axios's `onUploadProgress`, which is backed by XHR in browsers. Axios in Node may need the `form-data` package or a runtime with a compatible global `FormData`; test the runtime instead of assuming browser code works server-side.

## Redirects and cookies

`fetch` follows redirects by default, but the behavior differs for cross-origin redirects and credentialed requests. If the original cURL command relied on `-L` and cookies, check both:

- Browser fetch: use `credentials: "include"` for cross-origin cookies.
- Node fetch: cookies are not managed automatically; pass the `Cookie` header.
- Axios browser: `withCredentials: true`.
- Axios Node: manage cookies with a cookie jar or pass the header.

`fetch`'s `redirect` option has three values: `follow` (the default), `error`, and `manual`. In browsers, `manual` returns an opaque-redirect response with status 0, so the `Location` header cannot be read; in Node's fetch implementation, `manual` exposes the 3xx response. A conversion that reads redirect targets may therefore work in Node and fail in the browser. Fetch also caps redirect chains and rejects with a `TypeError` when the limit is exceeded, rather than returning the last response.

Cookies are not part of fetch's API. In the browser, `credentials: "include"` lets the browser attach and store cookies according to its cookie policy, but JavaScript still cannot read `Set-Cookie`. In Node, fetch has no cookie jar at all, so a login flow needs explicit header handling or a client library with a jar. Axios in Node has the same limitation; `withCredentials` only affects browsers.

## Verify the conversion

The most reliable check is to compare the actual requests. Use the [HTTP request workbench](/tools/http-request) or the [cURL converter](/tools/curl-converter) to inspect both forms.

Check:

1. Method and URL, including query parameters.
2. Every header, especially `Content-Type` and `Authorization`.
3. The body bytes. JSON key order usually does not matter, but encoding and whitespace can.
4. Redirect behavior.
5. Error handling. A fetch call that resolves with a 401 is not a success.

A conversion is correct when the server receives the same request, not when the code looks similar.

If the request is signed, byte fidelity is mandatory. AWS Signature Version 4 and webhook HMAC schemes sign a canonical request that includes a hash of the exact body. Rebuilding the body with `JSON.stringify` can reorder keys or change escaping, and the server rejects the signature even though the JSON is semantically identical. In that case, keep the original body as a string and sign those bytes.

Chrome and Firefox can export a request as cURL from the network panel ("Copy as cURL"), which is the fastest way to compare the browser's version with the original command. curl's `--trace-ascii` prints the bytes sent and received, headers included, which is more precise than `-v` when whitespace or encoding is in question.

## A note on browser vs server

A cURL command often runs outside the browser. If you convert it to client-side fetch, the browser adds constraints:

- CORS preflight for non-simple requests.
- No access to forbidden headers.
- Credentials require explicit configuration.
- Mixed content is blocked.

The forbidden header list explains several failed conversions. Browser fetch silently drops attempts to set `Host`, `Cookie`, `Content-Length`, `Origin`, and `Referer`. A server that requires a manually computed `Content-Length` or a specific `Cookie` header cannot be called from browser JavaScript; move that request server-side.

The final constraint is secrecy. A cURL command in documentation contains an API key because the reader is expected to run it on a trusted machine. Moving the same key into browser code publishes it to every visitor. If the API uses a secret key, the browser should call your backend and the backend should call the API. A conversion that works is still the wrong architecture if it ships a secret.

If the API is not designed for browser access, keep the request on your server and expose a narrow endpoint to the client. Do not disable browser security to make a copied command work.

## Frequently asked questions

### Why does my converted fetch request return a different response?

The most common causes are a missing Content-Type header, a body serialized differently, a missing Authorization header, or a redirect that curl followed but fetch did not. Compare the request headers and body byte-for-byte with the original command.

### Does fetch throw an error for a 500 response?

No. fetch resolves successfully for any HTTP status, including 4xx and 5xx. Check response.ok or response.status and handle the error body explicitly.

### How do I convert curl -u user:password to fetch?

Build a Basic Authorization header by base64-encoding the username and password with a colon between them, then send it as Authorization: Basic <value>. Do not put credentials in a URL.

### Why does curl work but the browser fetch fails with CORS?

curl does not enforce the browser same-origin policy. The browser sends a preflight for many cross-origin requests and blocks the response if the server does not return the required CORS headers. The server needs a CORS configuration, not a different fetch call.

### How do I send a file with fetch?

Use FormData and append the file and any other fields. Do not set the Content-Type header manually; the browser adds the multipart boundary. Axios accepts the same FormData object.

## References

- [curl - Manual](https://curl.se/docs/manpage.html)
- [MDN - Using the Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)
- [MDN - FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
- [Axios - Request config](https://axios-http.com/docs/req_config)

---

- Canonical: https://www.jsonmind.com/guides/convert-curl-to-fetch-axios
- Locale: en
- Published: 2026-11-09
- Updated: 2026-11-09
- Tools: curl-converter, http-request, form-urlencoded, basic-auth