An API is the service contract, an SDK is code that wraps that contract for a programming language, and a CLI is a finished command tool. Choose the API when you need direct protocol control, the SDK when you are building an application, and the CLI when a command is enough.
One service, three interfaces
Save this complete example as demo.mjs:
import { spawn } from "node:child_process";
import { createServer } from "node:http";
import { fileURLToPath } from "node:url";
async function requestBuckets(baseUrl) {
const response = await fetch(`${baseUrl}/buckets`);
if (!response.ok) throw new Error(`Bucket API returned ${response.status}`);
return response.json();
}
class BucketSdk {
constructor(baseUrl) { this.baseUrl = baseUrl; }
async listBuckets() { return (await requestBuckets(this.baseUrl)).buckets; }
}
async function runCli(baseUrl) {
for (const bucket of await new BucketSdk(baseUrl).listBuckets()) console.log(bucket);
}
if (process.argv[2] === "--cli") {
await runCli(process.argv[3]);
} else {
const server = createServer((request, response) => {
if (request.method !== "GET" || request.url !== "/buckets") {
response.writeHead(404).end("Not found");
return;
}
response.writeHead(200, { "content-type": "application/json" });
response.end(JSON.stringify({ buckets: ["photos", "reports"] }));
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
if (!address || typeof address === "string") throw new Error("Local service did not bind");
const baseUrl = `http://127.0.0.1:${address.port}`;
try {
console.log(`API: ${JSON.stringify(await requestBuckets(baseUrl))}`);
console.log(`SDK: ${(await new BucketSdk(baseUrl).listBuckets()).join(", ")}`);
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), "--cli", baseUrl], {
stdio: ["ignore", "pipe", "inherit"]
});
let output = "";
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk) => { output += chunk; });
const exitCode = await new Promise((resolve) => child.once("close", resolve));
if (exitCode !== 0) throw new Error(`CLI exited with ${exitCode}`);
console.log(`CLI:\n${output.trim()}`);
} finally {
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
}
}Run node demo.mjs. Expected output:
API: {"buckets":["photos","reports"]}
SDK: photos, reports
CLI:
photos
reportsA real failure and correction
A request to /bucket gets 404 Not found because the contract defines /buckets. Correct the path or the service contract. Switching from API to SDK or CLI does not remove authentication, permissions, throttling, retries, pagination, or service limits. Each interface must handle the same underlying rules.
See the idea visually
Watch the original BetterMeir EP008 video.
Tested with Node.js 26.0.0 on 13 Sep 2026.
