Arch Tutor

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:

MethodActionExample
GETReadGET /todos — list all todos
POSTCreatePOST /todos — create a new todo
PUTUpdatePUT /todos/42 — update todo #42
DELETERemoveDELETE /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:

Client-side API calljavascript
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 }, ...]
Always check response.ok before parsing JSON — a 401 or 500 still returns a response object.

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 request as the server sees ithttp
GET /api/todos HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Accept: application/json

For a POST that creates data, the server also receives a body:

POST request as the server sees ithttp
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:

Server endpoint handlerjavascript
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);
});
Every endpoint follows the same pattern: authenticate, validate, execute, respond.

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:

Create-todo request flow
DatabaseAuthAPI ServerClientDatabaseAuthAPI ServerClientPOST /api/todosverify tokenuser OKINSERTnew row201 + JSON

Each step can fail independently. Auth failures return 401, validation failures return 400, success returns 201.

The request body the client sends:

Request bodyjson
{
"title": "Learn architecture",
"done": false
}

On success, the server responds with 201 Created and the new resource:

Success response (201)json
{
"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:

Validation error (400)json
{
"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 headerhttp
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI3In0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

That 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:

PUTPATCH
SemanticsReplace the entire resourceUpdate only the fields you send
Idempotent?YesNot always
Use whenClient sends the full objectClient sends partial changes
PATCH — partial updatejavascript
// 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 unchanged
Only the done field changes; title and other fields stay untouched.

Use 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:

API styles compared

REST

GraphQL

gRPC

REST is the default for web APIs. GraphQL when clients need flexible queries. gRPC for internal service-to-service calls.

StyleBest forRequest shapeResponse format
RESTPublic web APIs, CRUD appsFixed endpoints per resourceJSON (usually)
GraphQLComplex UIs needing varied dataSingle endpoint, client specifies fieldsJSON matching query
gRPCInternal microservicesProtobuf-defined methodsBinary (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/customers not /createCustomer
  • Consistent error format — every error has type, message, and code
  • Pagination?limit=10&starting_after=cus_abc for large lists
  • Idempotency keysIdempotency-Key header 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:

StrategyExampleWhen to use
URL path/v1/users, /v2/usersMost common, clearest
HeaderAccept: application/vnd.api+json; version=2When URLs must stay stable
Query param/users?version=2Rarely 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 Requests when limits are exceeded
  • Include Retry-After header 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 /todos over /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