HTTP & API Automation — fetch + Bun.env

Lesson 0005 · Glossary · 1 · 2 · 3 · 4 · Next: globs + scheduling

The win, in one line: by the end of this lesson you'll have turned a curl ... | 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.

1. fetch is global and Promise-based

fetch(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)

2. Bun.env / process.env and .env files

Bun 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}` },
});
Keep .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.

3. Compose with file I/O from Lesson 2

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)

The bash → Bun map (this layer)

bashBun
curl -s URLawait 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: ..." URLfetch(URL, { headers: { Authorization } })
secrets in shell env / a sourced file.env loaded into process.env
curl -o fileawait Bun.write("file", res)

Practice

Recall check (from memory, no peeking)

1. What signals a 404 in fetch?

fetch only rejects on network failures. An HTTP error status is still a successful promise — check res.ok / res.status.

2. How do you read the JSON body?

await res.json() returns the parsed object. It’s the jq replacement — typed and in-progress.

3. Where do secrets live in Bun?

In process.env (same as Bun.env), loaded automatically from a gitignored .env file. Never commit or log it.

Hands-on: a release checker

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.

  1. 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.
  2. Parse await res.json(), log the tag_name (unauthenticated GitHub API is fine for a public repo).
  3. Extend: accept an optional first argument that is the download URL, use Bun.write to save the release tarball_url to disk if a second arg (a path) is given.
  4. Refactor so the API token can come from 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.

Read this next

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.