APIs & REST
How the frontend and backend talk to each other
You are at a fancy hotel. You want concert tickets for tonight, a restaurant reservation for tomorrow, and a taxi to the airport on Sunday. You do not hunt down the box office, call every restaurant in the city, and negotiate with cab companies yourself. You pick up the phone and call the concierge. You tell them what you need. They handle everything behind the scenes -- calling venues, checking availability, booking reservations -- and they call you back with the results. You never see the complexity. You just get the tickets. An API (Application Programming Interface) is that concierge. Your frontend does not reach directly into the database, talk to the payment processor, or figure out how to send emails. It calls the API with a request, and the API handles everything behind the scenes and sends back a response. This separation -- the frontend asks, the API handles -- is what makes modern web applications possible.
What Is an API, Really?
You have been using APIs all day today. Every time your weather app showed the current temperature, an API call happened. Every time your social feed loaded new posts, an API call happened. Every time you searched for something online, an API call happened. You just did not see it.
An API is a contract between two pieces of software. It says: "If you send me a request in this specific format, I will send you back a response in this specific format." That is the whole deal. No ambiguity, no guessing. A clear agreement on how to communicate.
In web development, "API" almost always means a web API -- a set of URLs (called endpoints) that accept HTTP requests and return data, usually as JSON. But the concept is much bigger than that. The browser has APIs (the DOM API, the Fetch API you used in Chapter 2, the Web Storage API). Libraries have APIs. Every time you call a function from a package, you are using its API -- its public interface that defines what you can do with it.
Here is the question that makes APIs click: why bother? Why not just let the frontend talk directly to the database?
Because APIs create boundaries. The frontend team and the backend team can work independently, as long as they agree on the API contract. The frontend does not care if the backend is written in Node.js, Python, or Go. The backend does not care if the frontend uses React or Vue. The API is the handshake between two worlds. Change everything behind it, and as long as the contract stays the same, nothing breaks. This is why API design is one of the most valuable skills in software engineering.
API Request & Response Cycle
How the client communicates with the server via HTTP
Fetch Call
fetch('/api/users', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})JSON Response
{
"users": [
{ "id": 1, ... },
{ "id": 2, ... }
]
}Fetch Call
fetch('/api/users', {
method: 'GET',
headers: {
'Content-Type':
'application/json'
}
})Update UI
const data = await
response.json()
// { users: [...] }
setUsers(data.users)JSON Response
{
"users": [
{ "id": 1, ... },
{ "id": 2, ... }
]
}HTTP Methods: The Verbs of the Web
Every request you send to an API starts with a verb. Not a noun, not a description -- a verb. What do you want to DO?
This is important. HTTP defines several methods that tell the server what action to perform, and picking the right one is not just a style choice -- it changes how the entire system behaves.
The four you will use constantly map to the four basic operations of any data system (called CRUD: Create, Read, Update, Delete). GET retrieves data. You are asking the concierge "what reservations do I have?" Nothing changes -- you are just looking. POST creates something new. You are saying "book me a table at that Italian place." PUT (or PATCH) updates something that already exists. "Actually, change that reservation to 8pm instead of 7pm." DELETE removes something. "Cancel the reservation entirely."
Before reading on, think about this: why does it matter which verb you use? Could you just use POST for everything?
You could, but here is why you should not. The system treats these verbs differently. GET requests can be cached by browsers -- your concierge remembers the answer and gives it to you instantly next time. POST requests are never cached, because creating something new should always go through. Search engines follow GET links but ignore POST forms. Firewalls and proxies can be configured to allow certain methods but block others. Using the right HTTP method is not just about being tidy. It is a technical decision that affects caching, security, and how your app behaves at scale.
Remember `fetch` from Chapter 2? That is the tool you will use to make all of these requests. Now you know what those `method: "POST"` and `method: "DELETE"` options actually mean.
// GET: Retrieve data (no body needed)
const response = await fetch("/api/posts")
const posts = await response.json()
// POST: Create a new resource (send data in body)
const newPost = await fetch("/api/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: "My First Post",
content: "Hello world!",
}),
})
// PUT: Replace an entire resource
await fetch("/api/posts/42", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: "Updated Title",
content: "Updated content",
}),
})
// PATCH: Partially update a resource
await fetch("/api/posts/42", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Just Changing the Title" }),
})
// DELETE: Remove a resource
await fetch("/api/posts/42", {
method: "DELETE",
})Status Codes: The Server Always Writes Back
You call the concierge and ask for concert tickets. They will always call you back. Sometimes it is good news: "Your tickets are confirmed." Sometimes it is not: "That show is sold out." Sometimes it is awkward: "You have not given us your credit card yet." And sometimes it is bad: "Our entire phone system is down."
Every HTTP response comes with a three-digit status code that tells you exactly how the request went. Learning to read these codes is like learning to read the concierge's tone of voice.
The codes are grouped by their first digit, and once you see the pattern, you will never forget it. 2xx means success -- the request worked. 200 (OK) means "here is what you asked for." 201 (Created) means "done, I made the new thing." 204 (No Content) means "done, but there is nothing to send back." 3xx means redirection -- "what you are looking for has moved, follow this new address." 4xx means YOU made a mistake. 400 (Bad Request) means your data was malformed. 401 (Unauthorized) means you need to log in first. 403 (Forbidden) means you are logged in but do not have permission. 404 (Not Found) means that thing does not exist. 429 (Too Many Requests) means slow down, you are overwhelming the server. 5xx means the SERVER broke. 500 (Internal Server Error) and 503 (Service Unavailable) are the most common -- something went wrong on their end, not yours.
Here is what separates a solid application from a fragile one: how it handles the bad news. Your frontend should always check `response.ok` (which is true for any 2xx status) and respond appropriately. Show the user a helpful message. Retry if it makes sense. Redirect to a login page on 401. Never assume a request will succeed. The server always writes back -- make sure you are reading the response.
async function fetchWithErrorHandling(url: string) {
const response = await fetch(url)
// response.ok is true for status 200-299
if (!response.ok) {
switch (response.status) {
case 400:
throw new Error("Bad request -- check your input data")
case 401:
// Redirect to login
window.location.href = "/login"
return
case 403:
throw new Error("You don't have permission to access this")
case 404:
throw new Error("Resource not found")
case 429:
throw new Error("Too many requests -- please slow down")
case 500:
throw new Error("Server error -- try again later")
default:
throw new Error(`Unexpected error: ${response.status}`)
}
}
return response.json()
}
// Usage with proper error handling
try {
const data = await fetchWithErrorHandling("/api/users/42")
console.log(data)
} catch (error) {
showErrorToast(error.message)
}JSON and Headers: The Language and the Envelope
How do two computers agree on a language? When your frontend sends data to a server and gets data back, both sides need to speak the same format. That format is almost always JSON.
JSON (JavaScript Object Notation) looks exactly like the JavaScript objects and arrays you already know from Chapter 2. Keys and values. Nested structures. Arrays of things. It is human-readable, easy to parse in every programming language ever invented, and has become the universal language of web APIs. When your React component fetches data from an API, it arrives as JSON. When it sends data to create a new user, it sends JSON. It is JSON all the way through.
But data is only half the conversation. Every HTTP request and response also carries headers -- metadata about the message itself. Think of it this way: JSON is the letter, and headers are the envelope. The envelope tells the postal system how to handle the delivery without opening it up and reading the letter.
Some headers you will encounter constantly. `Content-Type: application/json` tells the receiver "the body of this message is JSON." `Authorization: Bearer <token>` carries your login credentials so the server knows who you are. `Accept: application/json` tells the server "I want JSON back, please." `Cache-Control` says how long a response can be stored before it goes stale.
Here is a question: what happens if you send JSON to a server but forget the `Content-Type` header? The server receives a blob of text and has no idea what format it is in. It might try to parse it as form data, fail, and send you back a confusing 400 error. The headers are not optional decoration. They are instructions.
// JSON: the universal data format for APIs
const userData = {
name: "Alice Chen",
email: "alice@example.com",
preferences: {
theme: "dark",
notifications: true,
},
tags: ["developer", "mentor"],
}
// Sending JSON in a request
const response = await fetch("/api/users", {
method: "POST",
headers: {
// Tell the server we're sending JSON
"Content-Type": "application/json",
// Tell the server we want JSON back
"Accept": "application/json",
// Send our auth token
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIs...",
},
body: JSON.stringify(userData),
})
// Reading response headers
console.log(response.headers.get("Content-Type"))
// "application/json; charset=utf-8"
// Parsing the JSON response
const result = await response.json()
console.log(result)
// { id: 42, name: "Alice Chen", ... }REST: The Predictable Pattern
What if every API you ever used followed the same predictable pattern? What if, after learning how one endpoint works, you could correctly guess how every other endpoint works? That is the REST promise.
REST (Representational State Transfer) is a set of conventions for designing web APIs. It is not a strict protocol or a specification you install. It is more like an agreement -- a way of organizing things so that everyone is on the same page.
The core idea is simple: URLs identify resources (things), and HTTP methods identify actions (what you want to do to those things). Resources are nouns. `/api/users` is the collection of all users. `/api/users/42` is one specific user. `/api/users/42/posts` is all posts belonging to that user. The verb comes from the HTTP method: GET `/api/users` lists all users. POST `/api/users` creates a new user. PUT `/api/users/42` updates user 42. DELETE `/api/users/42` removes them. See the pattern?
Once you internalize this, something powerful happens. You encounter a brand-new API, and you can navigate it almost by instinct. "I need to get all products? Probably GET /api/products. I need to delete order number 7? Probably DELETE /api/orders/7." REST makes APIs learnable.
It is worth knowing that REST is not the only game in town. GraphQL lets clients request exactly the fields they need, nothing more. tRPC gives you end-to-end type safety between your TypeScript frontend and backend. WebSockets enable real-time, two-way communication. But REST remains the most widely used pattern by a large margin, and it is what you will encounter in the vast majority of APIs you work with.
In Chapter 5, you will flip to the other side of this conversation. Instead of sending requests, you will build the server that receives them. You will create the API endpoints, decide what the URLs look like, handle the HTTP methods, and send back the status codes. Everything you learned here will look completely different -- and completely familiar -- from the other side.
// RESTful API pattern: resources as URLs, methods as actions
// --- Users resource ---
// GET /api/users -> list all users
// POST /api/users -> create a new user
// GET /api/users/:id -> get a specific user
// PUT /api/users/:id -> update a specific user
// DELETE /api/users/:id -> delete a specific user
// --- Nested resources ---
// GET /api/users/:id/posts -> get all posts by a user
// POST /api/users/:id/posts -> create a post for a user
// A practical example: building a simple API client
class ApiClient {
private baseUrl: string
constructor(baseUrl: string) {
this.baseUrl = baseUrl
}
async getUsers() {
const res = await fetch(`${this.baseUrl}/api/users`)
return res.json()
}
async getUser(id: number) {
const res = await fetch(`${this.baseUrl}/api/users/${id}`)
return res.json()
}
async createUser(data: { name: string; email: string }) {
const res = await fetch(`${this.baseUrl}/api/users`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
})
return res.json()
}
async deleteUser(id: number) {
await fetch(`${this.baseUrl}/api/users/${id}`, {
method: "DELETE",
})
}
}
const api = new ApiClient("https://myapp.com")
const users = await api.getUsers()- An API is a contract: 'send me a request in this format, and I will send you a response in this format.' In web development, this means HTTP endpoints that accept requests and return JSON. The API is the concierge that handles everything behind the scenes.
- HTTP methods are verbs that tell the server what to do: GET (read), POST (create), PUT/PATCH (update), DELETE (remove). Using the right method is not just convention -- it affects caching, security, and system behavior.
- Status codes are the server's reply: 2xx means success, 4xx means you made a mistake, 5xx means the server broke. Always check response.ok and handle errors gracefully -- never assume a request will succeed.
- REST organizes APIs around resources (nouns in URLs) and actions (HTTP methods), creating a predictable pattern: once you learn how one endpoint works, you can guess the rest.
- JSON is the universal language of web APIs, and HTTP headers are the envelope -- carrying critical metadata like authentication tokens, content types, and caching instructions.
In the hotel concierge analogy, what role does the API play?