Blog
Oct 12, 2025 - 12 MIN READ
Programming in the Small 3: Objects, JSON, and APIs in JavaScript

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

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

  1. Add PUT /api/todos/:id to update a todo’s done status.
  2. Validate input: return 400 if title is missing or empty.
  3. Persist data: save db to a JSON file and reload on startup.
  4. Client retry: write a helper that retries fetch up to 3 times on network errors.

8) References


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.

Built by DOGUNFX using Nuxt UI • © 2025