1. Insight
Insight
The problem this article addresses and why it matters.
Every API doc page ends the same way
Whether you're reading Stripe's API reference, GitHub's REST docs, the OpenAI Platform, or any internal company API, the canonical example is a curl one-liner. It works in any terminal, it's copy-pasteable, it shows the headers and body shape in obvious form. The reader's next step is always the same: translate that curl into a fetch call in their actual code.
The translation looks trivial. It isn't. The curl examples in real API docs use -H "Authorization: Bearer sk_live_..." with a literal key inline — paste that into your TypeScript and you've just committed a production credential to git history. They use multiline JSON body strings that don't always survive the copy-paste through Slack and back. They omit error handling because the docs aren't trying to show you how to ship; they're trying to show you the wire format. And they're written for one HTTP client — the moment you swap fetch for axios or got, you're rewriting from scratch.
Why a translator and not just a code snippet
A textbook curl-to-fetch translator would handle the URL, the method, the headers, and the body. That gets you to a working call. The translator in this article goes further on three specific axes that matter once the generated code lands in a real codebase.
First, it detects credentials in the curl input and hoists them to process.env references — the Authorization header value gets replaced with process.env.STRIPE_SECRET_KEY and a manifest of what was extracted comes back with the result. Second, it produces output for five client libraries (fetch, axios, node-fetch, got, ky), so a developer can paste the same curl into the tool and get the call shape their codebase actually uses. Third, it can wrap the output in a retry loop with configurable backoff, which is the boilerplate everyone writes anyway when the generated code goes into a real service.
What this article delivers
The three-axis translation walked end-to-end: a real curl from the Stripe docs converted to fetch with env extraction, the same input converted to axios with retry-wrapping, and the annotated mode that adds inline comments explaining every option. We cover the edge cases (multipart bodies, file uploads, custom auth schemes) and the cases where the generated code needs human refinement.
2. Intent
Intent
What you will be able to do after reading.
By the end of this article you will be able to:
- Translate any curl command into idiomatic code for fetch, axios, node-fetch, got, or ky
- Extract API keys and bearer tokens automatically into
process.envreferences with a manifest of what was hoisted - Wrap the generated call in a retry loop with exponential or linear backoff and a configurable set of retry-eligible status codes
- Add inline comments to the output that document each option (annotated mode) — useful when the consumer is unfamiliar with the target client library
- Recognise the curl flags that don't translate cleanly (
--resolve,--unix-socket,--anyauth) and what to do about them
The Examples section walks through three real translations: a Stripe API call, a GitHub webhook test, and a multipart file upload.
3. Examples
Examples
Annotated code and worked scenarios.
Before / after: a Stripe API call with secret extraction
A canonical curl from the Stripe docs (sk_live_ key inlined as the docs show it):
Before:
curl https://api.stripe.com/v1/customers \
-H "Authorization: Bearer sk_live_51HxQz9KZvBT6abc...xyz" \
-d "email=alice@example.com" \
-d "name=Alice"After:
curlToFetch({
curl,
target: 'fetch',
typescript: true,
extractEnvVars: true,
});const response = await fetch('https://api.stripe.com/v1/customers', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
email: 'alice@example.com',
name: 'Alice',
}).toString(),
});Plus a manifest of what was extracted:
// envVars: [
// {
// name: 'STRIPE_SECRET_KEY',
// originalValue: 'sk_live_51Hxxxxxxxxxxxxxxxxxxx', // masked in output
// placeholder: 'process.env.STRIPE_SECRET_KEY',
// },
// ]The original key is masked in the manifest so a developer reviewing the tool's output doesn't have to handle the plaintext credential a second time. The placeholder is what shipped in the code.
Before / after: retry-wrapped output
Same input, with retryConfig added:
curlToFetch({
curl,
target: 'fetch',
typescript: true,
extractEnvVars: true,
retryConfig: {
maxRetries: 3,
strategy: 'exponential',
retryOn: [429, 502, 503, 504],
},
});Output:
async function callStripeCreateCustomer(): Promise<Response> {
let lastError: unknown;
for (let attempt = 0; attempt <= 3; attempt++) {
try {
const response = await fetch('https://api.stripe.com/v1/customers', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
email: 'alice@example.com',
name: 'Alice',
}).toString(),
});
if ([429, 502, 503, 504].includes(response.status) && attempt < 3) {
await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
continue;
}
return response;
} catch (err) {
lastError = err;
if (attempt < 3) {
await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
continue;
}
}
}
throw lastError;
}The retry loop is the boilerplate every team writes around fetch calls. With this option the generated code is ready to drop into a production service without further wrapping.
Before / after: annotated mode
annotate: true adds inline comments explaining each option — useful for developers unfamiliar with the target client's idioms:
const response = await fetch(
// curl positional URL argument
'https://api.stripe.com/v1/customers',
{
// -X POST (POST is implicit when -d is present)
method: 'POST',
// -H "Authorization: Bearer ..."
// Secret extracted from header value — see envVars manifest below
headers: {
'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`,
// -d (URL-encoded form data) implies this Content-Type
'Content-Type': 'application/x-www-form-urlencoded',
},
// Two -d flags joined with '&'; URLSearchParams.toString() handles encoding
body: new URLSearchParams({
email: 'alice@example.com',
name: 'Alice',
}).toString(),
},
);Before / after: multipart file upload
A multipart upload — curl uses -F for form fields and file references:
Before:
curl https://api.example.com/upload \
-H "Authorization: Bearer abc123" \
-F "file=@./report.pdf" \
-F "description=Q4 results"After (target: fetch):
const formData = new FormData();
formData.append('file', new Blob([await fs.promises.readFile('./report.pdf')]), 'report.pdf');
formData.append('description', 'Q4 results');
const response = await fetch('https://api.example.com/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.UPLOAD_API_KEY}`,
},
body: formData,
});Notice the absence of an explicit Content-Type header — fetch sets the multipart boundary automatically when the body is FormData. The translator emits this idiom rather than the brittle "manually set boundary" pattern.
When humans use this
A developer reading a new API's documentation runs the example curl through the tool to get a working call in their codebase's idiom. With extractEnvVars: true the workflow stays safe — they paste the example with the literal key from the docs, the tool hoists it, they update .env.example with the new variable name. Annotated mode is the teaching tool — junior developers learn fetch's quirks by seeing the curl-to-fetch correspondence inline.
When agents use this
Three production patterns:
- Integration scaffolding. An agent asked to "integrate with the Foo API" reads the API docs, finds the curl examples, runs each through
curl_to_fetchwith the target client matching the codebase (most codebases prefer fetch or axios), and writes a typed wrapper around each call. The env-extraction is critical here — the agent must not commit credentials, and the tool's behaviour gives a deterministic guarantee rather than asking the LLM to "remember not to inline the key." - Library porting. A team migrating from axios to fetch (or vice versa) runs every existing call through the tool to produce a deterministic conversion. The retry-wrapping option keeps the resilience semantics consistent across the migration.
- Webhook test harness. A multi-step agent generating webhook tests pulls curl from the provider's docs (GitHub, Stripe, Slack), converts each to fetch, wraps with assertions, runs against a local mock.
Edge cases
Cookie-based auth
curl's -b "session=abc123" and --cookie-jar are translated to a Cookie header (set per request) or a credentials: 'include' option for browser-targeted fetch. The translator does NOT generate a cookie-jar manager — that's a stateful concern outside the per-call translation surface.
--anyauth, --digest, --ntlm
Translated to a warning. JavaScript's standard fetch has no built-in support for digest or NTLM auth; the translator points the developer at node-fetch with a custom auth interceptor or a dedicated library (http-auth-utils). The generated call still includes the URL and body — only the auth is left as a // TODO comment.
Binary file downloads (-o, --output)
The output behaviour -o filename.bin doesn't have a fetch equivalent — fetch returns a Response and the developer chooses what to do with the body. The translator emits a comment showing the equivalent pattern (const buffer = await response.arrayBuffer(); await fs.promises.writeFile('filename.bin', Buffer.from(buffer));).
Custom curl resolvers
--resolve example.com:443:127.0.0.1 and --unix-socket /var/run/x.sock have no portable fetch translation. Both return warnings; the generated code uses the original URL and the developer handles the networking concern via an HTTP agent at the runtime layer.
4. Documentation
Documentation
Reference signatures, edge cases, and lookup tables.
Input parameters
Field | Type | Required | Default | Description |
|---|---|---|---|---|
|
| ✓ | — | Curl command string. Multi-line with |
|
| ✓ | — | Output client library |
|
| ✗ |
| Generate TypeScript with response typing. Otherwise plain JavaScript. |
|
| ✗ |
| Hoist credential-looking values into |
|
| ✗ |
| Add inline comments explaining each generated option |
|
| ✗ | — | Wrap output in a retry loop |
Output shape
{
code: string; // The generated call
imports: string; // Required imports (for axios, got, ky targets)
warnings: string[]; // Flags / features that didn't translate cleanly
envVars?: Array<{ // when extractEnvVars: true
name: string; // suggested env variable name
originalValue: string; // masked
placeholder: string; // what appears in the code
}>;
}Env-var extraction heuristic
A header value or query parameter is extracted to process.env when it matches one of:
Pattern | Suggested env name |
|---|---|
|
|
|
|
|
|
URL contains |
|
Known prefixes — | Mapped to the relevant service ( |
The original value is masked in the output (first 4 chars + ...). The placeholder (process.env.X) is what appears in the generated code.
Error codes
Code | When it fires | Recovery |
|---|---|---|
|
| Provide a non-empty curl string |
| Curl parser failed (unbalanced quotes, unknown flag) | Verify the curl command runs in a real terminal first |
| Curl uses TLS client cert flags ( | Use a runtime HTTP agent for the cert; the call shape is still in the output |
When NOT to use this tool
Don't use it to translate complex multi-step curl pipelines (curl A → jq B → curl C with the output). Those are shell pipelines, not HTTP calls; the equivalent in code is multiple await statements with explicit data passing, which the translator doesn't emit.
Don't use the retry wrapper as a substitute for idempotency analysis. The retry loop will re-issue any failed request matching the retry status codes — if the underlying API isn't idempotent (a POST that creates a resource), retrying creates duplicates. Use idempotency keys (Idempotency-Key header) for mutating calls.
For very-fine-grained HTTP control (streaming response bodies, custom DNS resolution, connection pooling), the generated code is a starting point. Layer your own HTTP-client config on top.
Performance notes
Typical execution: under 10ms for inputs below 2KB. The curl parser is the dominant cost; output emission is fast. The tool is deterministic — same input + same parameters always produce byte-identical output — so REST responses are Edge-Cache eligible. The env-extraction pattern recognition is a fixed regex set; matching is O(n) in the input length.