---
title: "11 — Consuming APIs and Styling CLI Output"
uid: consuming-apis
tags: ["got", "ky", "http-client", "nodejs", "roadmap:nodejs", "fetch", "axios", "chalk"]
excerpt: "Built-in fetch covers most calls; Axios, Got, Ky add the ergonomics — retries, interceptors, JSON parsing. And every pretty terminal library is ANSI escape codes to stdout."
date: 2026-08-13T03:27:55+0000
source: https://www.aveshina.my.id/en/blog/consuming-apis
---

"Just fetch it and console.log the result" was my API-consuming strategy, and it worked until the ergonomics mattered. The model that splits cleanly: **there is a built-in fetch good enough for most calls, and the libraries (Axios, Got, Ky) exist to add the ergonomics — retries, interceptors, automatic JSON parsing — that raw fetch leaves to me.** On the output side, every "pretty terminal" library is just writing ANSI escape codes to stdout [1].

The framing that landed for me is the layers on both sides: fetch on the input, stdout sugar on the output.

## fetch: the built-in baseline

Since Node 18, **fetch is a global**, the same API browsers use [2][3]. It is promise-based, returns a Response object, and covers the common cases — GET, POST with JSON, custom headers — without any dependency.

```
const res = await fetch('https://api.example.com/users/42');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const user = await res.json();
```

Two things about fetch that I had to internalize. First, **fetch does not throw on HTTP errors** — a 404 or 500 is a resolved promise with res.ok === false. I check res.ok (or res.status) myself and throw. Second, **the body is a stream** — res.json() parses it as JSON, res.text() reads it as a string, but the body is consumed once. These two traits are why the libraries exist: they add the "throw on bad status" and "parse the body" steps that fetch leaves explicit.

For a one-off call, fetch is enough. The libraries earn their keep when the calls multiply.

## Axios: the full-featured client

**Axios** is the most popular HTTP client for both browser and Node, and its appeal over raw fetch is the ergonomics pile [4]: it throws on non-2xx statuses, automatically parses JSON, supports request/response interceptors (for adding auth tokens globally), and handles timeouts cleanly.

```
import axios from 'axios';

const client = axios.create({ baseURL: 'https://api.example.com', timeout: 5000 });
client.interceptors.request.use(config => {
  config.headers.Authorization = `Bearer ${getToken()}`;
  return config;
});

const { data } = await client.get('/users/42');   // data is already parsed JSON
```

The interceptor pattern is the part I use most — one place to attach auth headers, log requests, or retry on failure, applied to every call through that client instance. For an app talking to one API with auth, that centralization is the reason to pick Axios over fetch.

## Got: the Node-native, human-friendly client

**Got** is designed specifically for Node.js (it does not run in browsers) and is more tuned to Node's strengths — it supports pagination, RFC-compliant caching, retries with backoff, and streaming out of the box [5]. Where Axios aims for "familiar everywhere," Got aims for "powerful on the server."

```
import got from 'got';

const data = await got('https://api.example.com/users/42', {
  timeout: { request: 5000 },
  retry: { limit: 2 }
}).json();
```

The .json() method both fetches and parses in one call, and the retry option means transient failures get retried automatically. Got is what I reach for in server-side scripts and APIs where retry and streaming matter.

## Ky: the tiny Fetch-based client

**Ky** is a small, modern client built on top of the Fetch API itself [6]. It is essentially "fetch with the ergonomics added" — it throws on errors, parses JSON, supports retries, and weighs about 1KB. Because it is built on fetch, the same code runs in modern browsers, Deno, and Node. Ky is the choice when I want the fetch ergonomics but not Axios's weight or Got's Node-only scope.

## Picking between them

The decision is mostly about how much I want on top of fetch:

- **fetch** (built-in) — one-off calls, no dependencies, I am willing to handle errors and parsing myself.
- **Axios** — many calls to one API, interceptors for auth/logging, familiar API across teams.
- **Got** — server-side work that needs retries, streaming, pagination, or caching.
- **Ky** — fetch ergonomics in a tiny package, multi-runtime.

The model I keep is "fetch is the baseline; the libraries are conveniences." Knowing exactly which convenience I am paying for keeps the choice deliberate.

## Styling CLI output: chalk, figlet, cli-progress

On the output side, three libraries cover the everyday CLI presentation needs, and all of them work by writing ANSI escape codes to stdout that the terminal interprets as styling [7].

**chalk** is the color-and-style library — red for errors, green for success, bold for emphasis, underline for links. It works across different terminals and degrades gracefully where a style is unsupported.

```
import chalk from 'chalk';
console.log(chalk.green('✓ build succeeded'));
console.log(chalk.red.bold('✗ tests failed'));
```

**figlet** renders text as large ASCII art — the stylized banner a CLI prints on startup. **cli-progress** draws progress bars for long-running operations like downloads, migrations, or batch processing, where seeing completion percentage matters.

The discipline with all three: **styling is decoration on the streams, never a substitute for correct exit codes and good stderr messages.** A red error message still needs a non-zero exit code, because CI and shell scripts do not read colors — they read exit codes. I keep styling for the human-facing layer and make sure the machine-facing layer (exit codes, structured stderr) is correct regardless.

## How I use this

The model I keep is "fetch is the baseline; libraries add ergonomics; styling is decoration." For outbound HTTP, I start with fetch and reach for Axios (or Got) the moment I find myself repeating interceptor-style logic or retry logic by hand. For CLI output, I reach for chalk the moment I want errors to read differently from normal output, but I never let styling carry information that the exit code does not also carry. Keeping the two layers separate — the data-fetching layer and the presentation layer — is what keeps both legible.

## References

[1] "chalk," GitHub. [Online]. Available: [https://github.com/chalk/chalk#readme](https://github.com/chalk/chalk#readme)

[2] Mozilla, "Using the Fetch API," MDN Web Docs. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)

[3] OpenJS Foundation, "Globals: fetch," Node.js API Docs. [Online]. Available: [https://nodejs.org/api/globals.html#fetch](https://nodejs.org/api/globals.html#fetch)

[4] "Axios Documentation," axios-http.com. [Online]. Available: [https://axios-http.com/docs/intro](https://axios-http.com/docs/intro)

[5] "Got," npm. [Online]. Available: [https://www.npmjs.com/package/got](https://www.npmjs.com/package/got)

[6] "Ky," GitHub. [Online]. Available: [https://github.com/sindresorhus/ky](https://github.com/sindresorhus/ky)

[7] "figlet," GitHub. [Online]. Available: [https://github.com/patorjk/figlet.js](https://github.com/patorjk/figlet.js)

```quiz
Q: Raw fetch does NOT throw on which of these?
- Network errors (connection refused)
- HTTP error statuses like 404 or 500
correct: 1
explain: fetch only rejects on network failure. A 404 or 500 is a resolved promise with res.ok === false. You must check res.ok yourself and throw if you want HTTP errors to behave like exceptions.

Q: What does Axios's interceptor pattern let you do that raw fetch does not?
- Run code on the server
- Attach logic (auth headers, logging, retries) once, applied to every request through that client instance
correct: 1
explain: Interceptors run for every request or response on a given Axios instance. That centralizes cross-cutting concerns like adding an Authorization header without repeating it at every call site.

Q: Which HTTP client is Node-only and built around retries, streaming, and pagination?
- Ky
- Got
correct: 1
explain: Got targets Node.js specifically and ships server-oriented features (retries with backoff, RFC-compliant caching, streaming, pagination). Ky is fetch-based and multi-runtime; Axios is browser+Node.

Q: chalk, figlet, and cli-progress all ultimately work by…
- modifying the process exit code
- writing ANSI escape codes to stdout that the terminal interprets as styling
correct: 1
explain: These libraries emit ANSI escape sequences mixed into the output text. The terminal interprets them as colors, styles, or drawing instructions. They do not change behavior — they decorate the streams.

Q: Why must a styled red error message also carry a non-zero exit code?
- Because ANSI codes can crash some terminals
- Because CI and shell scripts branch on exit codes, not colors — styling is for humans, exit codes are for machines
correct: 1
explain: Pipes and CI read exit codes, not colors. A red message with exit 0 looks like success to a shell script. Styling is decoration for the human layer; the exit code is the contract for the machine layer.
```
