A curl command converts cleanly to code once you read it as four separate parts — method, headers, body, and auth — because every HTTP client expresses those same four things, just with different syntax. The mistakes people make almost never come from the target language; they come from misreading what curl was doing implicitly. This article walks through the anatomy of a typical command, maps each flag to fetch, axios, Python requests, and Go, and then covers the pitfalls that silently change behaviour: the implicit POST, the disappearing content type, and shell quoting.
What are the parts of a curl command?
Read a curl command as a small set of flags, and the rest is the URL. The flags you meet most often are these:
| Flag | Long form | Meaning |
|---|---|---|
-X |
--request |
HTTP method (GET, POST, PUT, DELETE…) |
-H |
--header |
One request header; repeatable |
-d |
--data |
Request body; also flips the method to POST |
-u |
--user |
HTTP Basic auth as user:password |
-F |
--form |
multipart/form-data field or file upload |
Take a representative command:
curl -X POST https://api.example.com/v1/orders
-H "Content-Type: application/json"
-H "Authorization: Bearer TOKEN"
-d '{"sku":"AB-12","qty":3}'
That is one method (POST), two headers, and a JSON body. There is no magic beyond that. Once you can name the four parts, the translation is mechanical.
How does -d map to fetch, axios, requests, and Go?
The body maps to whichever field the client uses for the request payload. The important detail is that curl sends the body as bytes and lets you set the content type by hand, so your code must do the same. Here is the command above in four languages.
JavaScript fetch:
await fetch("https://api.example.com/v1/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer TOKEN"
},
body: JSON.stringify({ sku: "AB-12", qty: 3 })
});
axios:
await axios.post(
"https://api.example.com/v1/orders",
{ sku: "AB-12", qty: 3 },
{ headers: { Authorization: "Bearer TOKEN" } }
);
Note the asymmetry: with fetch you serialise the body yourself and set the content type yourself. With axios, passing a plain object as the data argument makes it serialise to JSON and set Content-Type: application/json automatically. That convenience is also a trap — if you hand axios a pre-stringified body it will not double-encode, but it also will not set the header, so behaviour diverges from what the object form would do.
Python requests:
import requests
requests.post(
"https://api.example.com/v1/orders",
json={"sku": "AB-12", "qty": 3},
headers={"Authorization": "Bearer TOKEN"},
)
Using json= tells requests to serialise the dict and set the JSON content type. If you instead use data= with a dict, requests form-encodes it and sets application/x-www-form-urlencoded — the same default curl uses with -d. Choosing the wrong keyword is the single most common requests bug.
Go:
body := strings.NewReader(`{"sku":"AB-12","qty":3}`)
req, _ := http.NewRequest("POST", "https://api.example.com/v1/orders", body)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer TOKEN")
resp, err := http.DefaultClient.Do(req)
Go is the most explicit of the four: nothing is inferred. You build the request, set every header, and send it. That verbosity is useful when you are trying to understand exactly what a command does, because there is no hidden default to trip over.
Why does -d quietly make a POST?
Because curl treats the presence of a body as intent to POST. The moment you add -d, curl changes the method from the default GET to POST and, unless you say otherwise, sets Content-Type: application/x-www-form-urlencoded. This is the source of two classic surprises.
First, people copy a command that has -d but no -X POST and assume it is a GET because they never see the word POST. It is a POST. If you translate it to code as a GET with a query string, the server sees a completely different request.
Second, people send JSON with -d but forget the content-type header. curl then labels a JSON payload as form-encoded, and a strict API rejects it or, worse, silently parses garbage. When you convert such a command, decide what the body actually is: if it is JSON, set application/json explicitly in both the command and the code. The tool cannot guess your intent from the bytes alone, and neither can the server.
There is a related subtlety with -d and files. A value beginning with @, such as -d @payload.json, tells curl to read the body from that file, and plain -d also strips newlines from the data. If you are pasting multi-line JSON and want it sent verbatim, --data-raw is the honest choice because it disables both the @ interpretation and the newline stripping.
How do -H and -u become headers in code?
Every -H is one header, and -u is a shortcut for one specific header. Repeated -H flags become entries in your headers object or map, in order. There is nothing special about Authorization, Accept, or a custom X-Request-Id — they are all just strings of the form Name: value split on the first colon.
-u user:pass is worth understanding because it looks like a separate feature but is not. It performs HTTP Basic authentication, which is defined as base64-encoding the string user:password and sending it as Authorization: Basic <encoded>. So -u alice:s3cret is exactly equivalent to computing the base64 of alice:s3cret and setting that Authorization header yourself. Knowing this means you never have to hunt for a special “basic auth” method in your HTTP client — you can always fall back to setting the header directly, and every client also offers a first-class helper (an auth= tuple in requests, an auth object in axios, req.SetBasicAuth in Go).
What quoting and escaping pitfalls bite when copying commands?
The quotes in a curl command belong to your shell, not to curl, and that is where copied commands break. Before curl ever runs, the shell processes the quotes: single quotes preserve everything literally, double quotes still allow variable and backtick expansion, and an unquoted { or & can be interpreted by the shell in ways you did not intend. When you paste a command from documentation into a different shell — or into a Windows cmd prompt, where single quotes are not string delimiters at all — the body can arrive mangled.
Three habits prevent most of this. Wrap JSON bodies in single quotes so the shell leaves them alone. When the JSON itself must contain a single quote, either escape it or switch the whole body to --data-raw read from a file. And when you convert to code, rebuild the body from its meaning — the object you intended to send — rather than trying to preserve the exact quoting of the shell string, because the code has no shell to satisfy.
A converter helps precisely here: it parses the command once, resolves the implicit method and content type, and emits a body your language serialises on its own terms. Our cURL-to-code converter runs entirely in your browser, so the command — and any token inside it — never leaves your machine. That matters, because a real command often carries a live bearer token or a Basic-auth password. Strip or rotate those before sharing a command, and prefer client-side tooling over anything that would post your request to a remote server to “convert” it.
A quick checklist for a faithful conversion
Confirm the method: if you see -d, --data, or -F and no -X, the method is POST, not GET. Confirm the content type: JSON needs an explicit application/json header, or the receiving code will treat -d as form data. Carry every -H across in order, and turn -u into either a Basic-auth helper or an Authorization: Basic header. Finally, reconstruct the body from intent, not from shell quoting. Do those five things and the same request goes out whether it is typed as curl, fetch, axios, requests, or Go.