Vibe Coding Fundamentals · Season 1 · Lesson 4
Language: English · 한국어
An API call is a contract: a client sends a request, a server responds, and your app decides whether the response is usable. When a button fails to show data, first separate a connection problem from an HTTP error and a malformed response. They demand different fixes.
In Lesson 3, the server—not a login badge—was the authority for authentication. This week’s question is “The API responded. Why did my screen still fail?” We’ll diagnose it with a local, fictional seat-count API.
1. API, HTTP, and JSON are different layers
- API: An interface contract between programs. Its URL matters, but so do inputs, outputs, failures, and the meaning of the operation.
- HTTP request: The method (
GET, for example), URL, headers, and sometimes a body. OurGET /api/seatsrequest has no body. - HTTP response: A status code, headers, and usually a body. A
200response does not guarantee that the body matches the app’s expectations. - JSON: A data format, such as
{"available":2}. It isn’t a database or a transport protocol. Reading it withresponse.json()can fail. - fetch(): An HTTP
404or503normally fulfills the fetch promise with aResponse. Checkresponse.okand validate the data separately.
A practical decision rule: Did a response arrive? Was its HTTP status acceptable? Could we parse its body? Did the parsed data match the contract? This sequence makes AI-generated API code much easier to review.
2. A mental model: four boundaries between a button and a result
- UI: A click starts the action and the page shows
Loading.... - Browser → server: A
GETrequest goes to/api/seats?case=ok. - Server → browser: The server replies with an HTTP status, headers, and a body. Status and valid JSON are not the same test.
- Browser → UI: Only after
response.ok, JSON parsing, and integer validation does the page show a seat count.
| Where it fails | What to inspect |
|---|---|
| Before a response | Is the request in DevTools Network? URL, connectivity, or CORS? |
| At the HTTP boundary | Is the status 404, 401, or 503? |
| While reading the body | Is the response actually parseable JSON, with the expected Content-Type? |
| After parsing | Does available exist and contain an integer? |
An API 404 is not necessarily the same problem as a page that breaks only on refresh. Compare this with the SPA refresh 404 Clinic before changing routing rules.
3. A free, local lab with four deliberately different responses
The seat count 2 is fictional test data. This is a read-only learning server: it books nothing and stores nothing. No paid service, subscription, credit card, domain, account, or real data is needed.
- Install the free Node.js runtime and confirm
node -vin your terminal. - Copy the entire example below into one file called
server.js, notserver.js.txt. - From that folder, run
node server.js. Openhttp://127.0.0.1:3200in a browser. If port 3200 is already in use, identify the other process first. - Choose each scenario, click Check seats, and compare the page with the
/api/seatsentry in DevTools Network.
// Synthetic read-only API lab. Local learning use only.
const http = require('node:http');
const page = `<!doctype html>
<html lang="en"><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>API response lab</title>
<main style="font:1rem/1.6 system-ui;max-width:30rem;margin:3rem auto;padding:0 1rem">
<h1>Fictional seat availability</h1>
<label for="scenario">Server scenario</label>
<select id="scenario">
<option value="ok">200 / valid JSON</option>
<option value="busy">503 / server unavailable</option>
<option value="broken">200 / invalid JSON</option>
<option value="slow">Slow response / timeout</option>
</select>
<button id="load">Check seats</button>
<p id="result" role="status" aria-live="polite">Not requested yet.</p>
</main>
<script>
const button = document.querySelector('#load');
const result = document.querySelector('#result');
button.addEventListener('click', async () => {
button.disabled = true;
result.textContent = 'Loading...';
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 1500);
try {
const mode = document.querySelector('#scenario').value;
const response = await fetch('/api/seats?case=' + mode, {
signal: controller.signal
});
if (!response.ok) {
result.textContent = 'HTTP ' + response.status + ': no seats shown';
return;
}
const data = await response.json();
if (!Number.isInteger(data.available)) {
throw new Error('Unexpected response shape');
}
result.textContent = 'Available: ' + data.available;
} catch (error) {
result.textContent = error.name === 'AbortError'
? 'Timed out. The server outcome is unknown.'
: error instanceof SyntaxError
? 'Invalid JSON from server.'
: 'Network or data error: ' + error.message;
} finally {
clearTimeout(timer);
button.disabled = false;
}
});
</script></html>`;
http.createServer((req, res) => {
const url = new URL(req.url, 'http://127.0.0.1:3200');
if (req.method === 'GET' && url.pathname === '/') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(page);
}
if (req.method === 'GET' && url.pathname === '/api/seats') {
const mode = url.searchParams.get('case');
const headers = { 'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'no-store' };
if (mode === 'busy') {
res.writeHead(503, { ...headers, 'Retry-After': '5' });
return res.end(JSON.stringify({ error: 'temporarily_unavailable' }));
}
if (mode === 'broken') {
res.writeHead(200, headers);
return res.end('{"available":'); // Intentionally malformed JSON.
}
const send = () => {
res.writeHead(200, headers);
res.end(JSON.stringify({ available: 2 }));
};
if (mode === 'slow') return setTimeout(send, 2200);
return send();
}
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'not_found' }));
}).listen(3200, '127.0.0.1', () => {
console.log('Open http://127.0.0.1:3200');
});
The server listens only on 127.0.0.1, and the page calls its own origin. The busy option intentionally returns 503; broken returns invalid JSON with status 200; slow responds after 2.2 seconds. The browser cancels waiting after 1.5 seconds. Aborting a browser request does not prove the server stopped its work.
4. What counts as success, failure, or an edge case?
| Scenario | Network observation → page result |
|---|---|
ok | 200 with valid JSONPage: Available: 2 |
busy | 503 with an error bodyPage: HTTP 503: no seats shown |
broken | 200 with invalid JSONPage: Invalid JSON from server. |
slow | Server waits about 2.2 s; client aborts at 1.5 s Page: Timed out. The server outcome is unknown. |
| Stop the server, then click in the still-open page | Connection error, usually no HTTP response Page: Network or data error |
Open /api/unknown in the address bar | 404Page: JSON not_found |
Loading... is a UI state, not an HTTP status. 503 indicates temporary unavailability. This lab sends Retry-After: 5, but that header does not automatically make the browser retry. Stop the server with Ctrl+C.
5. Five ways an AI-built API integration goes wrong
- ① Treating a fulfilled fetch as business success: A
404or503still has a response. Without checkingresponse.ok, the UI can report success for an error. - ② Assuming every body is JSON: A gateway might return an HTML error page, empty body, or malformed JSON. Parsing fails even when the request reached a server.
- ③ Ignoring response shape: The UI expects
available, but the API starts returningremaining. A parseable body can still violate the contract. - ④ Calling a timeout a definite failure: The client stopped waiting; the server may already have performed the action. Retrying a charge or create operation could duplicate it.
- ⑤ Treating CORS, HTTP status, and routing as one bug: This same-origin lab needs no CORS configuration. Cross-origin APIs need server permission;
mode: 'no-cors'does not make their responses readable.
6. Ask AI for observable states, not just a working-looking button
Weak prompt: “Make this button fetch data from the API. Keep retrying if it fails.”
Better prompt: “Inspect the existing API’s method, URL, request, and response JSON contract before coding. Distinguish loading, success, HTTP 4xx/5xx, network failure, invalid JSON, wrong response shape, and timeout. Check response.ok, clean up aborted requests, and give me reproducible DevTools Network checks. Do not add automatic retries until the operation is confirmed read-only and idempotent, and duplicate side effects are ruled out. Use synthetic data, never real customer records or tokens.”
Review questions: “Does a 503 appear as success? What happens with 200 and broken JSON? Can we tell whether the server executed a request after a timeout?”
7. When is retrying safe?
GET is defined by HTTP as safe (read-only) and idempotent. If a seat-checking endpoint really follows those semantics, checking again does not create another booking. But an unanswered POST that creates a reservation or charges a card may already have succeeded. Don’t automatically repeat a write request without a server-side duplicate-prevention design, such as idempotency keys and result lookup. Uncontrolled retries can also amplify an outage.
8. Undo and recover
- Reset the lab: Stop it with
Ctrl+Cand restartnode server.js. There is no database or durable state to restore. - Undo an AI edit: Keep a copy of the original
server.jsbefore changing it. Compare the diff and restore the known-working version if needed. A later lesson introduces Git for this. - On a real service: Capture the request URL, status, response body, and time before changing code. Make the smallest justified fix or roll back, and independently verify whether any write operation already took effect.
9. When this approach is the wrong one
Don’t build an API server just to display values already available in a static page. And don’t deploy this teaching server as a real booking, payment, or login backend: it lacks authentication, permissions, persistence, input controls, and operational safeguards. A mock is useful for a UI prototype, but passing a mock test is not proof that the network integration works.
10. Quick review, with answers
- Q: If
fetch()returns, did the API succeed? Not necessarily. Check HTTP status, JSON parsing, and the response shape. - Q: Can a
200response still break the UI? Yes. Trybrokenin the lab. - Q: Does timeout mean the server did nothing? No. You know the client did not obtain a timely answer, not whether the operation executed.
- Q: Is retrying every
GETalways fine? No. Confirm the server actually honors safe semantics and consider traffic, load, and policy.
11. What’s next?
Lesson 2 located the owners of data. Lesson 3 located the authority for login. Now that we can evaluate an API response, Lesson 5 will cover databases, schemas, IDs, and migrations: what must happen for a seat count to survive a restart? For a related diagnostic, see the Clinic on data disappearing after refresh.