
Programming in the Small 3: Objects, JSON, and APIs in JavaScript
A practical guide to JavaScript objects, working with JSON, and calling APIs (Node.js/Bun) — how data flows between frontend, backend, and a database.
dogunfx
In Part 2, we explored functions, loops, and arrays. Now we’ll connect those skills to real-world data using objects, JSON, and APIs. By the end, you’ll understand how data moves between a frontend, a backend, and a database — and you’ll have runnable examples for Node.js or Bun.
This post is runtime-agnostic: examples work in Node 18+ (native
fetch) or Bun. No browser DOM required.
1) Objects — Your Data’s Shape
Objects represent related data and behavior in a single structure.
const user = {
id: "u1",
name: "Dogunfx",
roles: ["author", "admin"],
active: true,
greet() {
return `Hello, ${this.name}!`;
}
};
console.log(user.greet()); // "Hello, Dogunfx!"
Access & Update
console.log(user.name); // dot notation
console.log(user["active"]); // bracket notation
user.active = false;
user["roles"].push("editor");
Nested Objects
const post = {
id: "p1",
author: { id: "u1", name: "Dogunfx" },
meta: { likes: 3, tags: ["js", "api"] }
};
console.log(post.author.name); // "Dogunfx"
console.log(post.meta.tags.join(",")); // "js,api"
Copying Objects (Important!)
const original = { a: 1, nested: { n: 2 } };
const copy1 = original; // same reference (❌ changes leak)
const copy2 = { ...original }; // shallow copy (nested refs stay)
const copy3 = structuredClone(original); // deep copy (Node 17+/Bun)
copy2.nested.n = 99; // also changes original.nested.n (shallow)
copy3.nested.n = 100; // original unaffected (deep)
2) JSON — Portable Data for the Web
JSON (JavaScript Object Notation) is a text format for exchanging data between systems.
Convert JS ⇄ JSON
const data = { id: "1", title: "Learn JSON", done: false };
const json = JSON.stringify(data); // JS object -> JSON string
// '{"id":"1","title":"Learn JSON","done":false}'
const parsed = JSON.parse(json); // JSON string -> JS object
// { id: '1', title: 'Learn JSON', done: false }
Common JSON Pitfalls
- No functions/undefined/symbols — they’re dropped in JSON.
- Dates become strings. You must rehydrate:
new Date(obj.createdAt). - Numbers can lose precision beyond 2^53−1 — send as strings if needed.
3) Calling APIs with fetch (Node/Bun)
APIs return JSON for your app to consume. In Node 18+ and Bun, fetch is built-in.
GET: Read Data
// run: node get.js (Node 18+) or bun run get.js
const url = "https://jsonplaceholder.typicode.com/todos/1";
const run = async () => {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const todo = await res.json();
console.log(todo);
};
run().catch(console.error);
POST: Send Data
const createPost = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Hello", body: "World", userId: 1 })
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const created = await res.json();
console.log(created);
};
createPost().catch(console.error);
Robust Error Handling
async function getJson(url) {
try {
const res = await fetch(url, { timeout: 8000 }); // Bun supports timeout; Node ignores
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
console.error("Request failed:", err.message);
return null;
}
}
4) Mini Backend (Mock DB) + Client — Data Flow End-to-End
Let’s simulate a tiny backend with an in-memory “database,” then call it from a client script.
Backend (Node Express or Bun’s serve)
Option A — Node.js + Express (install once: npm i express)
// server.js
import express from "express";
const app = express();
app.use(express.json());
const db = [{ id: "1", title: "Write post", done: false }];
app.get("/api/todos", (req, res) => res.json(db));
app.post("/api/todos", (req, res) => {
const todo = { id: Date.now().toString(), ...req.body, done: false };
db.push(todo);
res.status(201).json(todo);
});
app.listen(3001, () => console.log("API on http://localhost:3001"));
Run:
node server.js
Option B — Bun (bun --watch server.ts)
// server.ts (Bun)
const db = [{ id: "1", title: "Write post", done: false }];
export default {
port: 3001,
fetch(req: Request) {
const url = new URL(req.url);
if (req.method === "GET" && url.pathname === "/api/todos") {
return Response.json(db);
}
if (req.method === "POST" && url.pathname === "/api/todos") {
return req.json().then((body) => {
const todo = { id: Date.now().toString(), ...body, done: false };
db.push(todo);
return new Response(JSON.stringify(todo), {
status: 201,
headers: { "Content-Type": "application/json" },
});
});
}
return new Response("Not found", { status: 404 });
},
};
Run:
bun run server.ts
Client (Frontend/CLI Caller)
// client.js
const API = "http://localhost:3001/api/todos";
const main = async () => {
// Create
let res = await fetch(API, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Ship Part 3" })
});
const created = await res.json();
console.log("Created:", created);
// Read
res = await fetch(API);
const all = await res.json();
console.log("All todos:", all);
};
main().catch(console.error);
Run:
node client.js # or bun run client.js
That’s the data path: Client → HTTP request → Backend → (DB) → Response → Client.
5) CORS (When Frontend & Backend Are on Different Origins)
If your frontend runs on http://localhost:3000 and your API on http://localhost:3001, the browser enforces CORS. For local dev with Express:
// npm i cors
import cors from "cors";
app.use(cors({ origin: "http://localhost:3000" }));
Bun/Cloudflare-style servers must set headers manually:
return new Response(JSON.stringify(data), {
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "http://localhost:3000"
}
});
In Node/Bun CLI (no browser), CORS doesn’t apply — it’s a browser security policy.
6) Bonus: Shape Your Data with Types (JSDoc or TypeScript)
Even in plain JS, you can document shapes using JSDoc for editor help:
/**
* @typedef {{ id: string; title: string; done: boolean }} Todo
* @type {Todo[]}
*/
const db = [];
Or use TypeScript:
type Todo = { id: string; title: string; done: boolean };
const db: Todo[] = [];
7) Practice Challenges
- Add PUT /api/todos/:id to update a todo’s
donestatus. - Validate input: return
400iftitleis missing or empty. - Persist data: save
dbto a JSON file and reload on startup. - Client retry: write a helper that retries
fetchup to 3 times on network errors.
8) References
- MDN — Objects: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects
- MDN — JSON: https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/JSON
- MDN — Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
- Node fetch (Node 18+): https://nodejs.org/api/globals.html#fetch
- Bun server & fetch: https://bun.sh/docs/runtime/overview
With objects as your data model, JSON as the transport format, and APIs as the bridge, you now have everything needed to move data through your app — from input to database to UI. In Part 4, we’ll layer in asynchronous patterns (promises, async/await) and add file/database persistence for a tiny full-stack project.
Understanding package.json in a JavaScript or TypeScript App
Learn what package.json does, how it defines your JS/TS project’s dependencies, scripts, metadata, and configuration for Node.js, Bun, npm, and TypeScript.
Programming in the Small 2: Working with Functions, Loops, and Arrays in JavaScript
A hands-on continuation of “Getting Started with JavaScript Using Node.js or Bun,” diving deeper into functions, loops, and arrays — the real building blocks of programming logic.