Lesson 3
APIs & How Systems Talk
Discover how applications communicate through APIs, with a focus on REST principles and practical design.
28 min read · Beginner
The contracts between systems
When your mobile app needs user data, it does not reach into the database directly. When your frontend needs to process a payment, it does not talk to the bank’s internal systems. Instead, programs communicate through APIs — Application Programming Interfaces. An API is a defined contract: “If you send me this request in this format, I will respond with that data in that format.”
APIs are the glue of modern software architecture. They let teams work independently, let systems evolve at different speeds, and let you replace one component without rewriting everything.
What makes a good API
A well-designed API is predictable, consistent, and documented. Developers who have never seen your codebase should be able to use your API by reading its documentation alone.
The most common style for web APIs today is REST (Representational State Transfer). REST organizes APIs around resources — nouns like /users, /orders, or /todos — and uses HTTP methods to express actions:
| Method | Action | Example |
|---|---|---|
| GET | Read | GET /todos — list all todos |
| POST | Create | POST /todos — create a new todo |
| PUT | Update | PUT /todos/42 — update todo #42 |
| DELETE | Remove | DELETE /todos/42 — delete todo #42 |
Calling an API from the client
On the client side, you typically use fetch() to call an API. Here is a realistic example that lists todos with authentication:
const response = await fetch("https://api.example.com/api/todos", {
method: "GET",
headers: {
"Authorization": "Bearer eyJhbGciOiJIUzI1NiIs...",
"Accept": "application/json",
},
});
if (!response.ok) {
if (response.status === 401) {
throw new Error("Not logged in — redirect to login");
}
throw new Error("API error: " + response.status);
}
const todos = await response.json();
console.log(todos); // [{ id: 1, title: "Buy milk", done: false }, ...]Notice three important patterns: include the Authorization header, check response.ok (true only for 2xx status codes), and handle errors before calling .json().
What the server receives
From the server’s perspective, every API call arrives as a raw HTTP message. The server does not see your JavaScript — it sees text:
GET /api/todos HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Accept: application/jsonFor a POST that creates data, the server also receives a body:
POST /api/todos HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Length: 42
{"title": "Learn architecture", "done": false}The server parses the request line (method + path), reads headers for auth and content type, then parses the JSON body if present.
Building an API endpoint (server-side)
Here is what a typical server handler looks like for POST /api/todos. This is Express-style pseudocode — readable for any backend framework:
app.post("/api/todos", async (req, res) => {
// 1. Authenticate — who is making this request?
const user = await verifyToken(req.headers.authorization);
if (!user) return res.status(401).json({ error: "unauthorized" });
// 2. Validate input — is the data correct?
const { title, done } = req.body;
if (!title || typeof title !== "string") {
return res.status(400).json({ error: "title is required" });
}
// 3. Business logic + database write
const todo = await db.todos.create({
title,
done: done ?? false,
userId: user.id,
});
// 4. Return success with the created resource
res.status(201).json(todo);
});The order matters: reject bad auth before touching the database, validate input before running business logic, and always return a meaningful status code.
Complete create-todo flow
Putting it all together — here is the full journey when a user creates a todo:
Each step can fail independently. Auth failures return 401, validation failures return 400, success returns 201.
The request body the client sends:
{
"title": "Learn architecture",
"done": false
}On success, the server responds with 201 Created and the new resource:
{
"id": 42,
"title": "Learn architecture",
"done": false,
"userId": 7,
"createdAt": "2026-06-15T10:30:00Z"
}If validation fails, the server responds with 400 Bad Request — never 200 with an error buried in the body:
{
"error": "validation_failed",
"message": "title is required",
"fields": {
"title": "must be a non-empty string"
}
}Authentication in practice
Most APIs require proof of identity on every request. The most common pattern is a Bearer token in the Authorization header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3In0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cThat long string is often a JWT (JSON Web Token). A JWT has three dot-separated parts: header.payload.signature. The header says how it is signed, the payload contains claims like user ID and expiry, and the signature proves the token was not tampered with. The server verifies the signature on every request — it does not need to look up a session in a database (though many apps still do for instant revocation).
For beginners: treat the token like a temporary password. Store it securely, send it on every request, and never log it or expose it in URLs.
PATCH vs PUT
Both methods update existing resources, but they behave differently:
| PUT | PATCH | |
|---|---|---|
| Semantics | Replace the entire resource | Update only the fields you send |
| Idempotent? | Yes | Not always |
| Use when | Client sends the full object | Client sends partial changes |
// Client sends only what changed
await fetch("/api/todos/42", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer ...",
},
body: JSON.stringify({ done: true }),
});
// Server updates only { done: true }, leaves title unchangedUse PATCH for most UI-driven updates (toggling a checkbox, editing one field). Use PUT when the client owns the full resource and replaces it entirely.
How API calls flow through a system
REST API request flow
Mobile App
The client application that needs data. It constructs an HTTP request — say, GET /api/users/123 — and sends it to the API server. It does not know or care how the data is stored, only what the API contract promises. The client includes an Authorization header with a token obtained during login.
REST vs GraphQL vs gRPC
Three popular API styles, each with different tradeoffs:
REST is the default for web APIs. GraphQL when clients need flexible queries. gRPC for internal service-to-service calls.
| Style | Best for | Request shape | Response format |
|---|---|---|---|
| REST | Public web APIs, CRUD apps | Fixed endpoints per resource | JSON (usually) |
| GraphQL | Complex UIs needing varied data | Single endpoint, client specifies fields | JSON matching query |
| gRPC | Internal microservices | Protobuf-defined methods | Binary (fast, compact) |
For beginners, start with REST. It is widely understood, well-tooled, and maps naturally to HTTP. Adopt GraphQL when your frontend team complains about over-fetching or under-fetching data. Use gRPC when you need maximum performance between your own services.
Real example: Stripe-style resource design
Stripe’s API is considered a gold standard. Notice the patterns:
POST /v1/customers → Create customer
GET /v1/customers/:id → Retrieve customer
POST /v1/customers/:id → Update customer
DELETE /v1/customers/:id → Delete customer
GET /v1/customers → List customers (paginated)
POST /v1/charges → Create charge
GET /v1/charges/:id → Retrieve charge
Key design choices:
- Versioned (
/v1/) — old clients keep working when you ship/v2/ - Nouns, not verbs —
/customersnot/createCustomer - Consistent error format — every error has
type,message, andcode - Pagination —
?limit=10&starting_after=cus_abcfor large lists - Idempotency keys —
Idempotency-Keyheader prevents duplicate charges on retry
API responses and errors
APIs typically return JSON. A successful response:
{
"id": 123,
"name": "Alex",
"email": "alex@example.com"
}
Errors should also be structured:
{
"error": "not_found",
"message": "User with id 999 does not exist"
}
Use HTTP status codes consistently: 200 for success, 201 for created, 400 for bad input, 401 for unauthorized, 404 for not found, 429 for rate limited, and 500 for server errors.
Versioning, pagination, and rate limiting
Versioning
When you need to make breaking changes, version your API:
| Strategy | Example | When to use |
|---|---|---|
| URL path | /v1/users, /v2/users | Most common, clearest |
| Header | Accept: application/vnd.api+json; version=2 | When URLs must stay stable |
| Query param | /users?version=2 | Rarely recommended |
Ship /v2/ alongside /v1/, give clients months to migrate, then deprecate /v1/.
Pagination
Never return unbounded lists. Use cursor or offset pagination:
GET /api/todos?limit=20&offset=40 ← offset-based (simple)
GET /api/todos?limit=20&cursor=abc123 ← cursor-based (better for live data)
Response should include metadata:
{
"data": [...],
"pagination": { "next_cursor": "abc123", "has_more": true }
}
Rate limiting
Protect your API from abuse and overload:
- Return
429 Too Many Requestswhen limits are exceeded - Include
Retry-Afterheader telling clients when to try again - Common limits: 100 requests/minute for free tier, 1000/minute for paid
In practice
When designing your first API, write the documentation before the code. List every endpoint, request body, response shape, and error case. If you cannot document it clearly, the design is not ready.
Test your understanding
API Basics Quiz
Question 1 of 6
Which HTTP method should you use to create a new resource?
Key takeaways
- APIs are contracts between systems — they define how programs request and exchange data
- REST uses resources and HTTP methods to create predictable, readable endpoints
- APIs sit between clients and data, enforcing security and business rules
- Version, paginate, and rate-limit your APIs from day one
- Good APIs are consistent, documented, and return meaningful errors
- JSON is the lingua franca of modern web APIs
Common mistakes
- Using verbs in URL paths — prefer
/todosover/getTodos - Returning 200 OK with an error in the body — use proper HTTP status codes
- Exposing internal database structure — API responses should be designed for consumers
- No pagination — returning 50,000 records crashes browsers and databases
Go deeper
- Fielding’s REST Dissertation — the original definition of REST
- Stripe API Docs — a gold standard for API documentation and design
- JSON:API Specification — a convention for structuring JSON API responses