fetch + Bun.envcurl ... | jq chore into a typed Bun script that fetches an API, checks res.ok, parses JSON, and writes the result to disk — with secrets loaded from a .env file.
A large slice of daily scripts is just "hit an endpoint, pull a field, save it." Bun ships the browser fetch natively, and loads .env files for you — so API automation needs no dependencies.
fetch is global and Promise-basedfetch(url) returns a Response. Await it, check res.ok, then read the body as JSON, text, or bytes. Always check res.ok — a 404 or 500 still comes back as a resolved promise.
const res = await fetch("https://api.github.com/repos/oven-sh/bun");
if (!res.ok) { // covers 4xx and 5xx
console.error(`API error ${res.status}: ${await res.text()}`);
process.exit(1);
}
const data = await res.json(); // typed object — the jq twin
console.log(data.stargazers_count);
This is the exact structure you’d have written as curl -s URL | jq '.stargazers_count' — but here the shape is a typed value, and a failure aborts the script like set -e. (Bun docs: HTTP — fetch)
Bun.env / process.env and .env filesBun auto-loads a .env file from your project directory into process.env (which is the same object as Bun.env). No dotenv dependency, no config call:
# .env (your local secret — gitignore this file)
API_TOKEN=ghp_abcd1234
BASE_URL=https://api.github.com
// your script:
const token = process.env.API_TOKEN; // loaded automatically
const res = await fetch(`${process.env.BASE_URL}/repos/oven-sh/bun/releases/latest`, {
headers: { Authorization: `Bearer ${token}` },
});
.env out of git. Add it to .gitignore; commit a .env.example with placeholder names instead. process.env is where secrets live, so never log it.
Official sources for the two URL shapes above: Bun docs: utils — Bun.env, and Bun docs: environment variables.
API automation usually ends with "save it somewhere." Combine fetch with what you already know:
const res = await fetch("https://example.com/api/data");
await Bun.write("data.json", res); // Response → file, in one call
// or parse then transform then write:
const data = await (await fetch(url)).json();
const cleaned = (data.items ?? []).filter((i) => i.active);
await Bun.write("report.csv", cleaned.map((i) => `${i.name},${i.count}`).join("\n"));
Bun.write accepts a Response directly, so streaming an API body to disk is one line. (Bun docs: file I/O)
| bash | Bun |
|---|---|
curl -s URL | await fetch(URL) |
curl -sS -f URL (fail on error) | check res.ok yourself, process.exit(1) on failure |
curl URL | jq '.field' | (await res.json()).field |
curl -H "Authorization: ..." URL | fetch(URL, { headers: { Authorization } }) |
| secrets in shell env / a sourced file | .env loaded into process.env |
curl -o file | await Bun.write("file", res) |
fetch?fetch only rejects on network failures. An HTTP error status is still a successful promise — check res.ok / res.status.await res.json() returns the parsed object. It’s the jq replacement — typed and in-progress.process.env (same as Bun.env), loaded automatically from a gitignored .env file. Never commit or log it.Build check-bun-release.ts that fetches GitHub’s latest Bun release and prints/saves the tag. This is a real, portable, curl-replacing script.
const res = await fetch("https://api.github.com/repos/oven-sh/bun/releases/latest"); guard on res.ok; exit 1 with the status on failure.await res.json(), log the tag_name (unauthenticated GitHub API is fine for a public repo).Bun.write to save the release tarball_url to disk if a second arg (a path) is given.process.env if set, and add a header only then.Check your work (immediate feedback):
bun run check-bun-release.ts
# prints the latest release tag, e.g. "v1.4.1", exit 0
bun run check-bun-release.ts <invalid> /tmp/x.tar.gz
# if the URL arg were real, writes the tarball; with an invalid arg, exits 1
echo $? # 1 on the failure branch — your guard-on-res.ok worked
If the bad input path exited nonzero with a clear message, you’ve built the curl-with--f behavior — plus actual types.
Primary sources for this lesson: Bun Docs: HTTP (the fetch overview) and Bun Docs: environment variables (the .env auto-load, including precedence). Together they cover both halves: making the request and keeping credentials out of your source.
After here, the natural next weekend pair is globs (Bun.Glob, batch file matching) and scheduling (Bun.Cron.schedule) — the final slice of the mission.