APIs feel scary at first.
They are not.
An API is just a way for two sides to talk.
You ask for something.
It replies.
That is it.
If you can hold a conversation, you can work with APIs.
Let me show you.
The shape of a conversation
Human talk:
You say hello.
You ask a question.
You get an answer.
API talk:
You open a connection.
You send a request.
You get a response.
That is all we are doing.
A real request and response
Here is a tiny example that asks a public API for a user.
GET /users/42 HTTP/1.1
Host: api.example.com
Accept: application/json
A normal reply looks like this:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 42,
"name": "Tara",
"email": "tara@example.com"
}
Read it like a chat.
You asked for user 42.
The server said OK and sent you a JSON object.
Do it in the browser with fetch
<script>
async function getUser(id) {
const res = await fetch(`https://api.example.com/users/${id}`, {
headers: { "Accept": "application/json" }
});
if (!res.ok) {
throw new Error(`Request failed with ${res.status}`);
}
const data = await res.json();
console.log(data);
return data;
}
getUser(42).catch(err => console.error(err.message));
</script>
Do it in Node.js
import fetch from "node-fetch";
async function createUser(payload) {
const res = await fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},
body: JSON.stringify(payload)
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Failed ${res.status}: ${text}`);
}
return res.json();
}
await createUser({ name: "Tara", email: "tara@example.com" });
Do it in Python
import requests
def get_posts(page=1):
res = requests.get(
"https://api.example.com/posts",
params={"page": page},
headers={"Accept": "application/json"}
)
res.raise_for_status()
return res.json()
print(get_posts())
GET, POST, PUT, DELETE
Think of these as verbs.
GET
asks for data.
POST
creates something.
PUT
replaces something.
PATCH
updates part of something.
DELETE
removes it.
Pick the verb that matches what you want to do.
Path and query
Two ways to pass details.
Path is the thing itself.
GET /users/42
means the user with id 42.
Query is about filters, pages, and options.
GET /posts?page=3&limit=20
Headers are like tone and context
Accept: application/json
says you want JSON back.
Content-Type: application/json
says you are sending JSON.
Authorization: Bearer <token>
says who you are.
Auth without pain
const res = await fetch("https://api.example.com/me", {
headers: {
"Accept": "application/json",
"Authorization": `Bearer ${token}`
}
});
Keep tokens out of client code if they are secret.
Use env vars on the server.
Rotate them if they leak.
Errors are part of the chat
400 Bad Request
- your message was wrong.
401 Unauthorized
- you are not logged in.
403 Forbidden
- you are logged in but not allowed.
404 Not Found
- the thing is not there.
429 Too Many Requests
- slow down.
500+
- the server is broken.
Always handle the common ones.
Pagination is just the server saying, one page at a time
Most lists are paged.
The server gives you a slice and a pointer to the next.
async function getAllPosts() {
let cursor = null;
const all = [];
while (true) {
const url = new URL("https://api.example.com/posts");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await request(url.toString());
all.push(...res.items);
cursor = res.nextCursor;
if (!cursor) break;
}
return all;
}
Rate limits are the server saying, slow down
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
async function safeRequest(url, opts) {
for (let attempt = 1; attempt <= 5; attempt++) {
try {
return await request(url, opts);
} catch (e) {
if (e.status === 429 || e.status >= 500) {
const wait = Math.min(1000 * 2 ** (attempt - 1), 8000);
await sleep(wait);
continue;
}
throw e;
}
}
throw new Error("Gave up after retries");
}
Idempotent actions save you from double clicks
POST /charges
Idempotency-Key: a2b1-unique-123
Content-Type: application/json
{ "amount": 500, "currency": "usd" }
If the same key is sent again, the server returns the same result.
Caching is just remembering the answer
const cache = new Map();
async function cachedGet(url, ttlMs = 10_000) {
const hit = cache.get(url);
if (hit && Date.now() < hit.expires) return hit.data;
const data = await request(url);
cache.set(url, { data, expires: Date.now() + ttlMs });
return data;
}
Make your UI honest
Disable buttons during requests.
Show loading text or a spinner.
Show clear success or error messages.
Do not lie.
<button id="create" type="button">Create</button>
<p id="status" role="status" aria-live="polite"></p>
<script>
const btn = document.getElementById("create");
const statusEl = document.getElementById("status");
btn.addEventListener("click", async () => {
btn.disabled = true;
statusEl.textContent = "Working...";
try {
const res = await request("https://api.example.com/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Sample" })
});
statusEl.textContent = "Done";
} catch (e) {
statusEl.textContent = "Failed. Try again.";
} finally {
btn.disabled = false;
}
});
</script>
A quick mental model you can keep
Path says what you are talking about.
Verb says what you want to do.
Headers set the tone and context.
Body carries the content.
Status code is the reply mood.
JSON is the language you both agreed to use.
APIs are not magic.
They are just conversations.
Ask clearly.
Listen to the reply.
Handle the awkward parts with grace.
You will be fine.