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
- Describe what happens between
fetch()and the data arriving - Read a status code and know whose problem it is
- Send GET, POST, PUT, PATCH and DELETE requests correctly
- Turn a JSON response 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.
1. What an API actually is
You already 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. It is an agreed list of things you are allowed to ask for and the shape of the answer you will get. You never see the kitchen, you do not need to, and that is the point: the other team can rebuild their entire kitchen and your order still works.
A web API is that idea over HTTP. You send a request to a URL, and a response comes back, almost always as JSON.
2. The parts of a request
| Part | What it is | Example |
|---|---|---|
| Method | What you want done | GET, POST, PUT, PATCH, DELETE |
| URL | Which thing you want | https://api.example.com/students/12 |
| Headers | Information about the request | Content-Type: application/json |
| Body | Data you are sending | Only on POST, PUT and PATCH |
| Query string | Options on a GET | ?page=2&city=Lagos |
| Method | Means | Has a body |
|---|---|---|
GET | Give me this. Never changes anything. | No |
POST | Create a new one | Yes |
PUT | Replace this one entirely | Yes |
PATCH | Change these fields only | Yes |
DELETE | Remove this one | Usually 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 feel like it. Build a "delete" that works over GET and something will eventually crawl your links and empty your database.
3. Status codes
| Code | Family | Means | Whose problem |
|---|---|---|---|
200 | 2xx | OK | Nobody, it worked |
201 | 2xx | Created, after a POST | Nobody |
204 | 2xx | Done, and there is no content to send back | Nobody |
301 302 | 3xx | It moved | Nobody, follow it |
400 | 4xx | Bad request: your JSON or fields are wrong | Yours |
401 | 4xx | Unauthenticated: no valid key or token | Yours |
403 | 4xx | Authenticated, but not allowed | Yours |
404 | 4xx | No such thing at that URL | Yours |
429 | 4xx | Too many requests, slow down | Yours |
500 | 5xx | The server broke | Theirs |
503 | 5xx | Server overloaded or down | Theirs |
The families are 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.
4. fetch, and the trap in it
const response = await fetch('https://api.example.com/students');
const students = await response.json();
console.log(students);Two awaits, because there are two waits: one for the response to arrive, and one for the body to be read and parsed. Now the trap, and it catches everybody:
fetch does not throw on 404 or 500
fetch only rejects when the request could not be made at all: no network, bad DNS, blocked by CORS. A 404 or a 500 is a successful request that returned an error page, so your catch never runs and you cheerfully try to read JSON out of an error. You must check response.ok yourself, every single time.
async function getStudents() {
const response = await fetch('https://api.example.com/students');
// fetch will NOT do this for you
if (!response.ok) {
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
}
return response.json();
}response.ok is simply true for any status in the 200s. Throwing when it is false is what puts a real HTTP error into the same catch block as a network failure, which is where you wanted it all along.
5. The complete pattern
This is the function you will copy into every project for the rest of the course. Read it slowly: every line is there because of something that goes wrong without it.
async function apiGet(url) {
try {
const response = await fetch(url, {
headers: { 'Accept': 'application/json' }
});
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
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;
}
}Sending data
async function createStudent(student) {
const response = await fetch('https://api.example.com/students', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(student) // an object will NOT do
});
if (!response.ok) throw new Error(`Failed: ${response.status}`);
return response.json();
}
await createStudent({ name: 'Chidinma', city: 'Lagos' });bodymust be a string. Passing the object directly sends[object Object], and the server replies 400.- Without
Content-Type: application/jsonmany servers will not parse the body and you get a 400 with an empty-looking payload.
6. JSON: the format in the middle
JSON looks like a JavaScript object and is not one. It is text. Two functions move between them, and they are the only two you need.
const student = { name: 'Chidinma', score: 88, tags: ['frontend'], active: true };
const asText = JSON.stringify(student);
console.log(typeof asText, asText);
const backToObject = JSON.parse(asText);
console.log(typeof backToObject, backToObject.name);
// readable version, for logging
console.log(JSON.stringify(student, null, 2));What JSON quietly loses
JSON has no functions, no undefined and no dates. A Date becomes a string, a key whose value is undefined disappears entirely, and a function is dropped. That is why an API date arrives as "2026-09-12T08:00:00Z" and you have to build a Date from it yourself.
const record = {
name: 'Chidinma',
joined: new Date('2026-09-12T08:00:00Z'),
nickname: undefined,
greet() { return 'hi'; }
};
console.log(JSON.parse(JSON.stringify(record)));7. CORS: the error that is not your fault
You will meet this in your first hour with a real API, and without a name for it you will lose an afternoon.
Access to fetch at 'https://api.example.com/data' from origin
'http://localhost:5500' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.The browser refuses to let a page from one origin read a response from another origin unless the server sends a header saying it is allowed. Note what that means: the request usually went out and the server usually answered. The browser blocked you from reading the answer.
- It is a browser rule, not a JavaScript bug. The same request from a terminal or from a server works fine, which is why
curlsucceeds while your page fails. - You cannot fix it from the frontend. No header you set, no option you pass to
fetch, changes it. - The fixes are real ones: use an API that permits 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 are 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.
8. API keys, and the mistake that costs money
Many APIs identify you with a key. Anything in your JavaScript is visible to anyone who opens DevTools, so a key in your frontend is a public key, whatever the documentation calls it.
This has a real bill attached
A key committed to a public GitHub repository is found by automated scanners within minutes, not days. People have woken up to four-figure invoices from cloud providers because of one commit. Treat every key as if it were your bank card, because for some services it effectively is.
- For this course, use APIs that need no key at all wherever you can.
- When a key is unavoidable, keep it in a
.envfile, add.envto.gitignorebefore your first commit, and commit a.env.examplewith the names and no values. - A key that must stay secret has to live on a server, not in a browser. That is Level 3, and it is the honest answer.
- If you ever commit one: rotate it immediately. Deleting the commit does not help, because it is already in the history and probably already scraped.
# .gitignore - add this BEFORE your first commit
.env
node_modules/Do this now
Use a public, keyless API. Good ones for today: https://restcountries.com/v3.1/all, https://api.github.com/users/<name>, or https://jsonplaceholder.typicode.com/posts.
- Fetch a list and log it. Look at the real shape in the console before you write one line of rendering code.
- Add the
response.okcheck and prove it works by requesting a URL you know is a 404. Confirm yourcatchruns. - Render the list onto the page with
mapandjoin, the Day 4 way. - Open the Network tab, refresh, and find your request. Read its status, its response headers and its JSON. This is where you will debug every API problem for the rest of your life.
- Deliberately go offline and reload. What does your page do? Whatever it 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 successful request with an error status, so you have to 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. Sending credentials fixes the first and never fixes the second.
Q3. Why does `body: student` fail where `body: JSON.stringify(student)` works?
body must be a string. An object gets converted to the text [object Object], which is not JSON, so the server rejects it with a 400.
Q4. Your fetch works in the terminal but is blocked in the browser. What is it?
CORS. The server is not sending an Access-Control-Allow-Origin header that permits your origin, and the browser will not let the page read the response. It cannot be fixed from the frontend.
Tick before you move on
- ☐ I fetched real data and looked at its shape before rendering it
- ☐ My fetch checks
response.okand I proved it with a deliberate 404 - ☐ 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
- ☐
.envis in my.gitignore
Quick recap
4xx is your mistake, 5xx is theirs · fetch does not throw on 404 or 500: check response.ok yourself · JSON.stringify on the way out, response.json() on the way in · 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 in .gitignore before commit one
Tomorrow, Day 9: real data is messy, slow and sometimes empty. Searching, filtering and paging it without the interface lying to the user.