Courses Learn Blog Contact
Get Started →
Lesson 8

Day 8: APIs and HTTP

Methods, status codes, fetch and the response.ok trap, JSON both ways, CORS explained properly, and why an API key in frontend code is public.

Webbo3 Level 2: JavaScript Development 21 min read Free

Webbo3 Level 2 · JavaScript Development · Day 8 of 20

APIs and HTTP

How your JavaScript asks a computer in another country for data, and how to read what comes back.

By the end of today you can

  • Explain what an API is and what happens between fetch() and the data arriving
  • Read a status code and know whose problem it is
  • Send GET and POST requests correctly, with the right headers and body
  • Turn JSON into JavaScript objects and back again
  • Recognise a CORS error on sight, and know why it is not your code
  • Keep an API key out of your repository

An API is a door another company leaves open on purpose. Behind it is data you could never collect yourself: exchange rates, weather, football fixtures, a payment system, another team's database.

Today you learn to knock on that door properly, and to understand the answer that comes back. Yesterday gave you the tools for waiting; today is what you wait for.

About the Examples Today

Most browser examples call a small practice API at https://api.webbo3.test. It always answers the same way, so what you see in each box is exactly what the browser did with that answer. One example calls the real GitHub API, and in Do This Now you will call real public APIs yourself.

1. What an API Actually Is

Definition

An API (Application Programming Interface) is an agreed list of things one program may ask another program for, and the shape of the answer it will get back. A web API does this over HTTP: you send a request to a URL, and a response comes back, almost always as JSON.

Real-Life Example

You know the restaurant. You sit at a table, the waiter takes your order to a kitchen you never see, and brings food back. The API is the waiter and the menu: a fixed list of what you may order, and what arrives when you do.

You never see the kitchen, and you do not need to. The other team can rebuild their entire kitchen and your order still works, because the menu did not change.

Picture This

Picture the waiter with a notepad. You can only order what is on the menu, you must order it the way the menu says, and what comes back is always served on the same kind of plate.

2. The Parts of a Request

Definition

Every HTTP request is made of the same few parts. The method says what you want done, the URL says which thing, headers carry information about the request, and the body carries data you are sending.

Real-Life Example

A letter to a company: the envelope says who it is for (the URL), a note on the front says "Urgent, reply in writing" (headers), the first word inside says what you want, "Please cancel" (the method), and the pages inside are the details (the body).

Types: The Parts

PartWhat it isExample
MethodWhat you want doneGET, POST, PUT, PATCH, DELETE
URLWhich thing you wanthttps://api.webbo3.test/courses/2
HeadersInformation about the requestContent-Type: application/json
BodyData you are sendingOnly on POST, PUT and PATCH
Query stringOptions on a GET?page=2&city=Lagos

Types: The Methods

MethodMeansHas a body
GETGive me this. Never changes anything.No
POSTCreate a new oneYes
PUTReplace this one entirelyYes
PATCHChange these fields onlyYes
DELETERemove this oneUsually not
GET Must Be Safe, and This Is Not a Style Rule

A GET must never change anything on the server. Browsers, search engines and proxies all assume this and will re-send a GET whenever they like. Build a "delete" that works over GET and something will eventually crawl your links and empty your database.

Building a Query String

URLSearchParams builds the part after the ? for you, and handles spaces and special characters correctly.

What this code doesWe want to build a GET address with search options, letting JavaScript handle the spaces and symbols safely.
app.js
1
2
3
4
5
6
7
// The options for this search
const params = new URLSearchParams({ city: 'Lagos', page: 2, q: 'data analysis' });

// Join them onto the address
const url = `https://api.webbo3.test/students?${params}`;

console.log(url);
OUTPUT   TERMINAL
➜ webbo3 node app.js
https://api.webbo3.test/students?city=Lagos&page=2&q=data+analysis
Picture This

Picture the letter again: the method is the first word, the URL is the address on the envelope, the headers are the stamps and notes on the front, and the body is what is folded inside.

3. Status Codes: Whose Problem Is It?

Definition

Every response carries a three-digit status code saying how the request went. The first digit tells you the family, and the family tells you whose problem it is.

Real-Life Example

Sending a parcel. "Delivered" is a 2xx. "Wrong address, returned to sender" is a 4xx: you made the mistake. "Depot on fire, delayed" is a 5xx: the courier has the problem, and there is nothing wrong with your parcel.

Types

CodeMeansWhose problem
200OKNobody, it worked
201Created, after a POSTNobody
204Done, and there is nothing to send backNobody
301, 302It movedNobody, follow it
400Bad request: your JSON or fields are wrongYours
401Unauthenticated: no valid key or tokenYours
403Authenticated, but not allowedYours
404Nothing at that URLYours
429Too many requests, slow downYours
500The server brokeTheirs
503Server overloaded or downTheirs
The Whole Trick

4xx means you got it wrong. 5xx means they did. That one sentence tells you who to go and talk to, and it will save you hours.

Picture This

Picture the parcel tracker: 2xx delivered, 4xx returned because of your label, 5xx stuck because of the courier.

4. JSON: The Format in the Middle

Definition

JSON (JavaScript Object Notation) is text that looks like a JavaScript object. APIs send and receive it because text travels over a network and objects do not. JSON.stringify() turns an object into JSON text; JSON.parse() turns JSON text back into an object.

Real-Life Example

Sending furniture across the country. You flat-pack the wardrobe into a box (stringify), ship the box, and the other side assembles it again from the instructions (parse). What travels is the flat box, never the wardrobe.

Types: The Two Directions

What this code doesWe want to turn an object into JSON text and back, and see what type each one is.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
// A real JavaScript object
const student = { name: 'Chidinma', score: 88, tags: ['frontend'], active: true };

// Object to text, ready to send
const asText = JSON.stringify(student);
console.log(typeof asText);
console.log(asText);

// Text back to an object, ready to use
const backToObject = JSON.parse(asText);
console.log(typeof backToObject);
console.log(backToObject.name);
OUTPUT   TERMINAL
➜ webbo3 node app.js
string
{"name":"Chidinma","score":88,"tags":["frontend"],"active":true}
object
Chidinma

A readable version, for logging, uses two extra arguments: no filter, and indent by two spaces.

What this code doesWe want JSON printed in a readable, indented shape for inspecting in the console.
app.js
1
2
3
4
const student = { name: 'Chidinma', score: 88 };

// null = keep every property, 2 = indent by two spaces
console.log(JSON.stringify(student, null, 2));
OUTPUT   TERMINAL
➜ webbo3 node app.js
{
  "name": "Chidinma",
  "score": 88
}
What JSON Quietly Loses

JSON has no functions, no undefined and no dates. A date becomes a string, a property whose value is undefined disappears, and a function is dropped.

What this code doesWe want to see which parts of an object survive a round trip through JSON.
app.js
1
2
3
4
5
6
7
8
9
10
// An object with a date, an undefined value and a method
const record = {
  name: 'Chidinma',
  joined: new Date('2026-09-12T08:00:00Z'),
  nickname: undefined,
  greet() { return 'hi'; }
};

// Send it through JSON and back
console.log(JSON.parse(JSON.stringify(record)));
OUTPUT   TERMINAL
➜ webbo3 node app.js
{ name: 'Chidinma', joined: '2026-09-12T08:00:00.000Z' }

That is why an API date arrives as a string like "2026-09-12T08:00:00.000Z", and you build a Date from it yourself with new Date(text).

Picture This

Picture the flat-pack box. Only what fits in the box travels. Anything that cannot be written on the instructions, like a function, is left behind.

5. fetch: Asking for Data

Definition

fetch(url) sends an HTTP request and gives back a Promise (Day 7) of a response. The body of the response arrives separately, so you read it with response.json(), which is another Promise.

Real-Life Example

Ordering by phone. First the shop picks up and says whether they have it (the response and its status). Then they read out the details (the body). Two separate moments, so two separate waits.

Example 1: A GET Request

response.ok is true for any status from 200 to 299.

What this code doesWe want to fetch the list of courses from the practice API, check the status, and put the courses on the page.
index.html
1
2
<p id="status"></p>
<ul id="courses"></ul>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function loadCourses() {
  // Wait 1: the response arrives, with its status
  const response = await fetch('https://api.webbo3.test/courses');
  console.log('status:', response.status, response.ok);

  // Wait 2: the body is read and parsed from JSON
  const courses = await response.json();

  // Render them the Day 4 way
  document.querySelector('#courses').innerHTML = courses
    .map((course) => `<li>${course.name} (${course.hours}h)</li>`)
    .join('');
  document.querySelector('#status').textContent = `${courses.length} courses`;
}

loadCourses();
BROWSERindex.html · after the request finished
CONSOLE   BROWSER DEVTOOLS
status: 200 true

Common Mistake: fetch Does Not Fail on a 404

fetch only rejects when the request could not be made at all: no network, or blocked by the browser. A 404 or a 500 is a request that worked and brought back an error, so your catch never runs. Predict what this prints:

What this code doesWe want to see what happens when the server answers "not found" and the code does not check for it.
index.html
1
<p>Loading course 99...</p>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
async function loadCourse() {
  try {
    // Ask for a course that does not exist
    const response = await fetch('https://api.webbo3.test/courses/99');
    console.log('the request worked; status:', response.status);
    const course = await response.json();
    console.log('course name:', course.name);
  } catch (error) {
    console.log('catch ran:', error.message);
  }
}

loadCourse();
BROWSERindex.html · after the request finished
CONSOLE   BROWSER DEVTOOLS
Failed to load resource: the server responded with a status of 404 (Not Found)
the request worked; status: 404
course name: undefined
Why This Happens

The catch never ran. The code read the error body as if it were a course, and got undefined. You must check response.ok yourself, every single time.

The Fix: Check response.ok and Throw

What this code doesWe want a 404 to land in the same catch block as a network failure, by checking response.ok and throwing when it is false.
index.html
1
<p id="message">Loading...</p>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function loadCourse(id) {
  const message = document.querySelector('#message');
  try {
    const response = await fetch(`https://api.webbo3.test/courses/${id}`);

    // fetch will NOT do this for you
    if (!response.ok) {
      throw new Error(`Request failed: ${response.status}`);
    }

    const course = await response.json();
    message.textContent = course.name;
  } catch (error) {
    message.textContent = `Sorry: ${error.message}`;
  }
}

loadCourse(99);
BROWSERindex.html · after asking for course 99
CONSOLE   BROWSER DEVTOOLS
Failed to load resource: the server responded with a status of 404 (Not Found)

Example 2: No Network at All

What this code doesWe want to see what fetch does when the request cannot be made at all, as when the student is offline.
index.html
1
<p id="message">Loading...</p>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
async function loadCourses() {
  const message = document.querySelector('#message');
  try {
    const response = await fetch('https://api.webbo3.test/courses');
    message.textContent = `status ${response.status}`;
  } catch (error) {
    // This time fetch itself rejects
    console.log(error.name, error.message);
    message.textContent = 'Could not reach the server. Check your connection.';
  }
}

loadCourses();
BROWSERindex.html · with the network switched off
CONSOLE   BROWSER DEVTOOLS
Failed to load resource: net::ERR_INTERNET_DISCONNECTED
TypeError Failed to fetch

A network failure arrives as a TypeError. That is how the complete pattern below tells "your connection" apart from "their server".

Overall Example: The Complete Pattern

This is the function you will copy into every project for the rest of the course. Every line is there because of something that goes wrong without it.

instanceof asks whether a value is a particular kind of object: here, whether the error is a TypeError.

What this code doesWe want one reusable function that handles success, an error status and a dead network the same careful way, and to call it all three ways.
index.html
1
<ul id="log"></ul>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// The pattern for every GET in this course
async function apiGet(url) {
  try {
    const response = await fetch(url, { headers: { Accept: 'application/json' } });

    // A 4xx or 5xx: turn it into an error
    if (!response.ok) {
      throw new Error(`${response.status} from the server`);
    }

    return await response.json();
  } catch (error) {
    // A TypeError here almost always means the network, not your code
    if (error instanceof TypeError) {
      throw new Error('Could not reach the server. Check your connection.');
    }
    throw error;
  }
}

// Try it three ways and write down what happened
const log = document.querySelector('#log');
async function tryIt(label, url) {
  try {
    const data = await apiGet(url);
    log.innerHTML += `<li>${label}: got ${data.length} courses</li>`;
  } catch (error) {
    log.innerHTML += `<li>${label}: ${error.message}</li>`;
  }
}

async function run() {
  await tryIt('good URL', 'https://api.webbo3.test/courses');
  await tryIt('wrong URL', 'https://api.webbo3.test/cursus');
  await tryIt('offline', 'https://api.webbo3.test/offline');
}

run();
BROWSERindex.html · after all three calls
CONSOLE   BROWSER DEVTOOLS
Failed to load resource: the server responded with a status of 404 (Not Found)
Failed to load resource: net::ERR_INTERNET_DISCONNECTED

Example 3: A Real API

The same fetch works outside the browser too. This one calls GitHub's real, public API and prints fields that do not change.

What this code doesWe want to make a real request to a real API on the internet and read fields out of the JSON it sends back.
app.js
1
2
3
4
5
6
7
8
9
10
11
12
// A real public API: GitHub's profile for its mascot account
async function showProfile() {
  const response = await fetch('https://api.github.com/users/octocat');
  if (!response.ok) throw new Error(`GitHub said ${response.status}`);

  const user = await response.json();
  console.log(user.login);
  console.log(user.name);
  console.log(user.html_url);
}

showProfile();
OUTPUT   TERMINAL
➜ webbo3 node app.js
octocat
The Octocat
https://github.com/octocat
Picture This

Picture phoning a shop: first they pick up and say whether they have it (status), then they read out the details (body). A shop that picks up and says "we do not stock that" still answered the phone, which is why fetch calls a 404 a success.

6. Sending Data

Definition

To create something, send a POST. You pass fetch a second argument: the method, a Content-Type header saying the body is JSON, and the body itself as a JSON string.

Real-Life Example

Filling in a registration form and posting it. The envelope says "registration" (the method and URL), a note says "form enclosed, in English" (the Content-Type), and the filled form is inside (the body).

What this code doesWe want to create a new student on the practice API and show the record the server sends back.
index.html
1
<p id="result">Saving...</p>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async function createStudent(student) {
  const response = await fetch('https://api.webbo3.test/students', {
    method: 'POST',
    // Tell the server the body is JSON
    headers: { 'Content-Type': 'application/json' },
    // The body must be a string
    body: JSON.stringify(student)
  });

  if (!response.ok) throw new Error(`Failed: ${response.status}`);
  console.log('status:', response.status);
  return response.json();
}

async function save() {
  const created = await createStudent({ name: 'Chidinma', city: 'Lagos' });
  document.querySelector('#result').textContent = `Saved as student #${created.id}`;
}

save();
BROWSERindex.html · after the POST finished
CONSOLE   BROWSER DEVTOOLS
status: 201

The practice API replies 201, which means Created, with the new record and the id it was given.

Common Mistake: Sending the Object Itself

The body must be a string. Pass the object directly and JavaScript turns it into text the only way it knows:

What this code doesWe want to see the text an object turns into when it is sent without JSON.stringify.
app.js
1
2
3
4
5
6
7
const student = { name: 'Chidinma' };

// What fetch would send if you wrote body: student
console.log(String(student));

// What it sends with JSON.stringify
console.log(JSON.stringify(student));
OUTPUT   TERMINAL
➜ webbo3 node app.js
[object Object]
{"name":"Chidinma"}

"[object Object]" is not JSON, so the server replies 400. Leave out the Content-Type header and many servers will not read the body at all, which also ends in a 400.

Picture This

Picture posting a registration: the form must be filled in on paper (a string), and the envelope must say what is inside, or the office cannot process it.

7. CORS: The Error That Is Not Your Fault

Definition

CORS (Cross-Origin Resource Sharing) is a browser rule. A page from one origin (its protocol, domain and port, such as http://localhost:5500) may only read a response from another origin if that server sends an Access-Control-Allow-Origin header saying it is allowed.

Real-Life Example

A security guard at an office will only hand you a letter addressed to you if the sender wrote "visitors may collect". The letter arrived, the guard has it, but without that note the guard will not give it to you.

What this code doesWe want to see the real error the browser gives when a page asks a server that does not allow other origins to read its answers.
index.html
1
<p id="message">Loading...</p>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
async function load() {
  const message = document.querySelector('#message');
  try {
    // A different origin: port 8765, not 5500
    const response = await fetch('http://localhost:8765/courses');
    message.textContent = `status ${response.status}`;
  } catch (error) {
    message.textContent = `Blocked: ${error.message}`;
  }
}

load();
BROWSERlocalhost:5500/index.html · with a server that sends no CORS header
CONSOLE   BROWSER DEVTOOLS
Access to fetch at 'http://localhost:8765/courses' from origin 'http://localhost:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Failed to load resource: net::ERR_FAILED

What That Error Really Means

  1. The request usually went out, and the server usually answered. The browser blocked your page from reading the answer.
  2. It is a browser rule, not a JavaScript bug. The same request from a terminal or a server works fine, which is why it works in Node but not on your page.
  3. You cannot fix it from the frontend. No header you set and no fetch option changes it.
  4. The real fixes: use an API that allows browser requests, or put a small backend of your own in the middle, which is what you will do in Level 3.
Do Not Reach for a Public CORS Proxy

They are advertised everywhere as the one-line fix. You would be routing your users' requests, and any key or token in them, through a stranger's server. Fine for a throwaway experiment on public data, never for anything real.

Picture This

Picture the guard holding your letter: it arrived, but without the sender's permission note you cannot take it. Only the sender can add that note.

8. API Keys, and the Mistake That Costs Money

Definition

An API key is a secret string that identifies you to an API. Anything in your frontend JavaScript is visible to anyone who opens DevTools, so a key in the browser is effectively public, whatever the documentation calls it.

Real-Life Example

A key to your house. Taped to the front door, it is not a secret any more, however well you hid it among the other things on the door.

This Has a Real Bill Attached

A key committed to a public GitHub repository is found by automated scanners within minutes. People have woken up to four-figure invoices from cloud providers because of one commit. If you ever commit one, rotate it (ask for a new key) immediately: deleting the commit does not help, because it is already in the history.

How to Keep a Key Out of Git

  1. Use APIs that need no key wherever you can.
  2. When a key is unavoidable, keep it in a file called .env.
  3. Add .env to .gitignore before your first commit.
  4. Commit a .env.example that lists the names with no values, so others know what to set.
  5. A key that must stay secret has to live on a server, not in the browser. That is Level 3.
What this code doesWe want Git to ignore the file that holds our secret keys, from the very first commit.
.gitignore
1
2
3
# Files Git must never track
.env
node_modules/
NOTE   
Git reads this file and never adds the listed files, so your .env can never be pushed.
What this code doesWe want to tell other developers which keys the project needs, without sharing ours.
.env.example
1
2
# Copy this file to .env and fill in your own values
WEATHER_API_KEY=
NOTE   
This one IS committed. It has the names and no values.
Picture This

Picture your house key again: it lives in your pocket (the server or .env), never taped to the front door (your JavaScript or your repository).

Do This Now

Use a real public API that needs no key. Two that work well: https://api.github.com/users/NAME (put in any GitHub username) and https://restcountries.com/v3.1/region/africa.

  1. Fetch the data and log it. Look at the real shape in the console before you write one line of rendering code.
  2. Add the response.ok check, and prove it by requesting a URL you know is a 404. Confirm your catch runs.
  3. Render the data onto the page with map and join, the Day 4 way.
  4. Open the Network tab in DevTools, refresh, and find your request. Read its status, its response headers and its JSON. This is where you will debug every API problem from now on.
  5. Go offline (DevTools, Network tab, choose Offline) and reload. Whatever your page does now, make it say something useful.

Checkpoint

Q1. A request returns 404. Does your catch block run?

No. fetch only rejects when the request could not be made at all. A 404 is a request that worked and brought back an error, so you check response.ok and throw yourself.

Q2. What is the difference between 401 and 403?

401 means the server does not know who you are: no key, or an invalid one. 403 means it knows exactly who you are, and you are still not allowed.

Q3. Why does body: student fail where body: JSON.stringify(student) works?

The body must be a string. An object turns into the text "[object Object]", which is not JSON, so the server rejects it with a 400.

Q4. Your fetch works in Node but is blocked in the browser. What is it?

CORS. The server is not sending an Access-Control-Allow-Origin header that allows your page's origin, so the browser will not let the page read the answer. It cannot be fixed from the frontend.

Tick Before You Move On

  • ☐ I can explain what an API is using the restaurant
  • ☐ I know 4xx is mine and 5xx is theirs
  • ☐ I fetched real data and looked at its shape before rendering it
  • ☐ My fetch checks response.ok, and I proved it with a deliberate 404
  • ☐ I sent a POST with a JSON string body and a Content-Type header
  • ☐ I found my request in the Network tab and read its status and headers
  • ☐ I know what my page does with no connection, because I tried it
  • ☐ .env is in my .gitignore

Quick Recap

An API is a menu of requests another program accepts, and the shape of its answers. A request has a method, a URL, headers and sometimes a body; GET never changes anything. 4xx is your mistake, 5xx is theirs. fetch does not throw on 404 or 500: check response.ok yourself, every time. JSON.stringify on the way out, response.json() on the way in; JSON drops dates, undefined and functions. CORS is a browser rule enforced by the server's headers, not a bug you can patch. Any key in frontend JavaScript is public; .env goes in .gitignore before the first commit.

Final Scenario

Your course dashboard needs the list of courses from the Webbo3 API. The student is on a train, the signal comes and goes, and one day the course they bookmarked is deleted.

Your apiGet function already handles all of it: the list when the network is good, a clear "check your connection" when it is not, and a readable message for the missing course, instead of a blank page.

An app that handles bad news clearly feels more trustworthy than one that only works on good wifi. Every request from now on goes through that one careful function.

Tomorrow, Day 9: working with real data. Real data is messy, slow and sometimes empty: searching, filtering and paging it without the interface lying to the user.

Learn this with a real instructor

Free lessons take you a long way on your own. The full programmes add live classes, assignments, Skill Points and a certificate — built for African students.