1 HTTP APIs & REST design practices
Imagine a restaurant. You don't walk into the kitchen and cook โ you talk to a waiter. You say "I'd like the pasta," the waiter carries your order to the kitchen, and brings back a plate. An API is that waiter: a polite, agreed-upon way to ask a program for something and get an answer back, without ever touching the kitchen. You only need to know what to ask for and how to ask โ not how the food is made.
An API (Application Programming Interface) is a contract: a set of requests one program promises to understand, and the responses it promises to give back. When that contract travels over the web, it almost always rides on HTTP (HyperText Transfer Protocol) โ the same protocol your browser uses to load pages. A Web API is just an API you reach by sending HTTP requests to a URL.
The shape of an HTTP request
Every HTTP request has four parts: a method (the verb โ what you want to do), a URL (what you want to do it to), optional headers (extra info like who you are), and an optional body (the data you're sending). The response comes back with a status code (did it work?), headers, and a body (the answer).
HTTP methods โ the verbs
HTTP gives us a small set of verbs. Each says what kind of action you intend, which keeps APIs predictable.
| Method | What it means | Everyday analogy |
|---|---|---|
GET | Read data. Should never change anything. | Reading the menu. |
POST | Create something new. | Placing a new order. |
PUT | Replace a thing entirely with what you send. | Re-writing the whole order from scratch. |
PATCH | Update part of a thing. | "Actually, make it extra spicy." |
DELETE | Remove a thing. | Cancelling the order. |
Status codes โ did it work?
The server replies with a three-digit status code. The first digit tells you the category at a glance.
| Range | Meaning | Common examples |
|---|---|---|
| 2xx | Success | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirection | 301 Moved Permanently, 304 Not Modified |
| 4xx | You (the client) made a mistake | 400 Bad Request, 401 Unauthorized, 404 Not Found, 429 Too Many Requests |
| 5xx | The server broke | 500 Internal Server Error, 503 Service Unavailable |
Resources & URLs โ the nouns
The dominant style for designing web APIs is REST (REpresentational State Transfer).
Its core idea: model everything as resources (nouns), each living at a clean URL,
and act on them with HTTP verbs. So you don't invent a verb-y URL like /createUser;
you POST to the noun /users.
| Goal | Method + URL |
|---|---|
| List all users | GET /users |
| Get one user | GET /users/42 |
| Create a user | POST /users |
| Replace user 42 | PUT /users/42 |
| Update part of user 42 | PATCH /users/42 |
| Delete user 42 | DELETE /users/42 |
| A user's messages | GET /users/42/messages |
Statelessness
REST APIs are stateless: each request must carry everything the server needs to understand it (like a login token). The server does not remember anything about you between requests. This is exactly the same idea we met with LLMs being stateless โ and it's powerful here for the same reason: any server in a fleet can handle any request, so you can add more servers freely (recall scaling from earlier sessions). The "state" (like your session) lives in a token you resend, or in a shared database.
Idempotency
Pressing a regular elevator button 5 times is the same as pressing it once โ the elevator still comes to your floor exactly once. That's idempotent: doing it again doesn't cause extra stuff to happen.
Idempotency means: making the same request many times has the same effect as making it once. This matters because networks are flaky โ if you don't get a reply, you might retry, and you don't want retries to create five identical orders.
GET,PUT,DELETEare idempotent: reading twice is fine; replacing user 42 twice leaves the same result; deleting twice still ends with it gone.POSTis not idempotent by default: two POSTs to/orderscreate two orders. The fix is an idempotency key โ a unique ID the client sends so the server can recognise a retry and ignore the duplicate.
Versioning
Once real apps depend on your API, you can't just change it โ you'd break them. So you publish a new
version and keep the old one running. Common approaches: in the URL
(/v1/users, /v2/users) or in a header
(Accept: application/vnd.myapp.v2+json). URL versioning is the simplest and most
common.
Good API design principles
- Use nouns, not verbs in URLs; let HTTP methods be the verbs.
- Be consistent: plural nouns (
/messages), lowercase, hyphenated paths. - Return correct status codes โ don't reply
200with an error inside. - Filter/paginate big lists:
GET /messages?limit=20&page=2. - Version from day one so you can evolve safely.
- Clear error bodies: tell the caller what went wrong and how to fix it.
Creating a message. Request:
POST /v1/messages HTTP/1.1 Host: api.chatapp.com Authorization: Bearer abc123 Content-Type: application/json { "chat_id": 7, "sender_id": 42, "text": "Hello!" }
Response:
HTTP/1.1 201 Created Content-Type: application/json Location: /v1/messages/9981 { "id": 9981, "chat_id": 7, "sender_id": 42, "text": "Hello!", "created_at": "2026-06-20T10:31:00Z" }
Notice the verb (POST), the noun URL (/v1/messages),
the JSON body, the 201 Created code, and the Location
header pointing at the new resource. That's textbook REST.
2 Monolith vs Microservices
A monolith is one giant LEGO castle built as a single solid block โ beautiful, but if you want to change one window you risk wobbling the whole thing, and only one kid can work on it at a time. Microservices are like building the castle from many small, snap-together rooms. Each room is built by a different kid, and you can replace the kitchen without touching the towers.
A monolith is an application built and deployed as one single unit. All the code โ users, messages, billing, search โ lives in one codebase, runs in one process, and ships together. Microservices is the opposite approach: you split the application into many small, independent services, each owning one area of the business, each deployed on its own, each talking to the others over the network (usually via the HTTP APIs from Topic 1).
What makes a service "micro"?
Not lines of code โ it's about ownership and independence. A good microservice:
- Owns one area (e.g. "messages") and its own data store.
- Can be deployed, scaled, and even rewritten without touching the others.
- Talks to other services only through their public APIs โ never by reaching into their database.
Bounded contexts โ where to cut
The hard part is deciding the seams. The guiding idea is the bounded context (a term from Domain-Driven Design): a boundary inside which a set of words and rules has one clear meaning. The word "user" might mean a login account in one context and a billing customer in another โ those are natural places to split. Cut along business capabilities, not technical layers. "Messaging," "Payments," "Notifications" are good services; "the database layer" is not.
Microservices trade simplicity for independence. You accept the real cost of a distributed system (network calls, partial failures, more moving parts) in exchange for teams shipping independently and scaling each piece on its own. It's a tooling-and-organisation decision as much as a technical one.
Side by side
| Aspect | Monolith | Microservices |
|---|---|---|
| Codebase | One | Many, one per service |
| Deployment | All together | Each independently |
| Scaling | Scale the whole app, even if only one part is hot | Scale only the busy service |
| Tech choices | Usually one language/stack | Each service can pick its own |
| Failure blast radius | A bug can take down everything | One service can fail in isolation |
| Communication | In-process function calls (fast, simple) | Network calls (slower, can fail) |
| Data | One shared database | A database per service |
| Team workflow | Teams coordinate on one repo | Teams own services independently |
| Operational complexity | Low | High (monitoring, networking, deploys) |
When to split (and when not to)
Start with a monolith. For a new product or small team, a monolith is faster to build, easier to debug, and has no network between parts. Split into microservices when you feel real pain:
- Different parts need to scale very differently (search is hammered; billing is quiet).
- Multiple teams keep stepping on each other in one codebase.
- You want to deploy parts on different schedules or in different languages.
- A bug in one area keeps taking down unrelated features.
Microservices are not "free modularity." A function call that used to be instant and reliable becomes a network request that can be slow, fail, or arrive twice. You now need service discovery, retries, timeouts, distributed tracing, and a database per service. Splitting too early โ especially before you understand the domain โ is one of the most common architecture mistakes. (We'll see why partial failure is so tricky in the CAP and replication discussion next session.)
3 Building a chat app with microservices
Think of a post office. One clerk's whole job is to take your letters and deliver them โ that's the mail desk. In a back room, someone quietly keeps a tally: how many letters were sent today, who's the busiest sender โ that's the counting desk. The mail desk never waits on the counting desk; counting happens calmly in the background. Our chat app is built the same way: one service delivers messages, another counts and analyses them.
Let's design a simple chat application as two microservices. Keeping it to two makes the boundaries crystal clear and shows off the bounded-context idea from Topic 2.
The two services
| Service | Owns | Responsibilities |
|---|---|---|
| Message Service | The messages themselves + its own messages database | Accept a new message, validate it, store it, return it; list a chat's recent messages; deliver to recipients. This is the critical, user-facing path โ it must be fast and reliable. |
| Analytics Service | Counts, stats, trends + its own analytics store | Track messages sent per user/per chat, active users, busiest hours. This is a background concern โ it can be a little slow or briefly behind without hurting the chat experience. |
How they fit together
The key design choice: the Message Service does not phone the Analytics Service and wait. It just stores the message, replies to the user immediately, and emits an event โ a small "this happened" note. The Analytics Service picks that up on its own time. If analytics is slow or down, chatting still works perfectly. (We'll formalise this synchronous-vs-asynchronous choice in Topic 4.)
A real endpoint โ the Message Service
Here's a small FastAPI service (FastAPI is a popular Python web framework) exposing
the POST /v1/messages endpoint we designed in Topic 1.
from fastapi import FastAPI, HTTPException from pydantic import BaseModel from datetime import datetime, timezone app = FastAPI() # In a real service this is a database; a dict keeps the demo simple. messages = {} next_id = 1 class NewMessage(BaseModel): # validates the request body chat_id: int sender_id: int text: str def emit_event(name, payload): # Publish to a message queue (Topic 4). Fire-and-forget. print(f"event: {name} -> {payload}") @app.post("/v1/messages", status_code=201) def create_message(msg: NewMessage): global next_id if not msg.text.strip(): raise HTTPException(status_code=400, detail="text required") record = { "id": next_id, "chat_id": msg.chat_id, "sender_id": msg.sender_id, "text": msg.text, "created_at": datetime.now(timezone.utc).isoformat(), } messages[next_id] = record next_id += 1 # Tell the world a message was sent โ Analytics will react later. emit_event("message.sent", {"chat_id": msg.chat_id, "sender_id": msg.sender_id}) return record # FastAPI sends it back as JSON with 201 @app.get("/v1/chats/{chat_id}/messages") def list_messages(chat_id: int, limit: int = 20): chat_msgs = [m for m in messages.values() if m["chat_id"] == chat_id] return chat_msgs[-limit:]
The Analytics Service runs separately. It consumes the message.sent events
(off a queue) and just increments counters in its own database โ something like
messages_per_user[42] += 1. It then exposes its own read API, e.g.
GET /v1/stats/users/42, returning {"messages_sent": 1287}.
Two services, two databases, one event flowing between them.
- Alice's phone sends
POST /v1/messageswith her text. - Message Service validates (non-empty), saves it, returns
201with the saved message โ Alice sees a delivered tick in < 100 ms. - In the same step it emits
message.sentto the queue, then forgets about it. - Seconds later, the Analytics Service reads that event and bumps Alice's "messages sent today" count.
- If Analytics was down for an hour, the events wait in the queue and get processed when it's back โ Alice never noticed.
4 Inter-service communication
Two ways to ask a friend for help. You can phone them and stand there waiting for the answer before you do anything else โ that's synchronous. Or you can leave a sticky note on their door and walk away; they'll deal with it when they can โ that's asynchronous. Both are useful; you pick based on whether you actually need the answer right now.
Once you have many services (Topic 2), they must talk. There are two big questions: what format do they use, and do they wait for the reply?
What format: HTTP/REST vs RPC (gRPC)
We met REST over HTTP in Topic 1 โ human-friendly URLs and JSON. An alternative is
RPC (Remote Procedure Call): you call a function on another service as if it were a
local function (analytics.GetUserStats(42)) and the framework handles the network. The
most popular modern RPC framework is gRPC (from Google), which sends compact binary
data (using Protocol Buffers) over a fast connection.
| Aspect | REST / HTTP + JSON | gRPC (RPC) |
|---|---|---|
| Data format | JSON (text, human-readable) | Protocol Buffers (binary, compact) |
| Speed / size | Slower, larger | Faster, smaller |
| Readability | Easy to read & debug by hand | Needs tooling to inspect |
| Contract | Loose (docs/conventions) | Strict .proto schema, code-generated |
| Best for | Public APIs, browser clients, simple internal calls | High-traffic internal service-to-service calls |
Do they wait: synchronous vs asynchronous
This is the deeper choice. Both REST and gRPC are usually synchronous request/response: you call, you block, you get an answer. Asynchronous communication instead uses a message queue (a buffer in the middle, like RabbitMQ, Kafka, or AWS SQS): the sender drops a message and moves on; a receiver picks it up later. This is the "sticky note" model โ exactly what our chat app used to feed the Analytics Service.
| Aspect | Synchronous (REST/gRPC) | Asynchronous (queue) |
|---|---|---|
| Caller waits? | Yes โ blocks for the reply | No โ fire and forget |
| Coupling | Tight: callee must be up now | Loose: callee can be down/slow; messages buffer |
| Failure handling | Caller sees errors immediately | Queue absorbs spikes & outages; retries built in |
| Gets a return value? | Yes, right away | Not directly โ results come via another event |
| Best for | "I need the answer to continue" (e.g. check a password) | "Just go do this eventually" (e.g. analytics, emails, thumbnails) |
- Synchronous: The Message Service must ask an Auth Service "is this token valid?" before storing the message. It needs the answer now, so it makes a blocking REST/gRPC call.
- Asynchronous: Updating analytics counts can happen later, so the Message Service
drops a
message.sentevent on a queue and doesn't wait.
Rule of thumb: if you need the result to proceed, go synchronous; if it can happen "eventually," go asynchronous.
Chains of synchronous calls are fragile: if service A calls B calls C and C is slow, A is stuck too โ one slow service can stall a whole request, and a cascade of timeouts can take the system down. Async queues add resilience (they buffer spikes and outages) but cost you simplicity: there's no immediate answer, and you must handle messages that arrive twice or out of order. This loose coupling leads to eventual consistency โ the analytics count is briefly stale โ a theme we explore fully next session.
5 Handling image uploads in messages
Imagine your address book. You don't glue an entire photo of your friend's house onto the tiny contact card โ that would make the book fat, slow, and impossible to flip through. Instead you write down the address of the house. The photo (the big thing) lives somewhere roomy; the card just holds a pointer to it. Databases work the same way with images: store the picture elsewhere, keep only its address in the message.
People send photos in chat. The naive idea is to shove the image bytes straight into the messages database. Don't. A raw image file is a blob (Binary Large OBject) โ and databases are terrible at storing big blobs.
Why not store blobs in the database?
- Bloat & slowness: images are huge compared to text. The database balloons, backups take forever, and queries that used to be instant get sluggish.
- Expensive: database storage and memory are far pricier per gigabyte than plain file storage.
- Bad at serving: a database can't efficiently stream a 4 MB photo to a browser the way a purpose-built file server can.
- Wasted cache: the database's memory should hold hot rows, not photo bytes.
The right way: object storage + CDN + a URL
Put the actual image in object storage โ a service built to hold arbitrary files cheaply and durably (Amazon S3, Google Cloud Storage, Azure Blob Storage). Then store only a small URL / reference in the message row. To make downloads fast worldwide, serve those files through a CDN (Content Delivery Network) โ a fleet of edge servers that cache copies of your files physically close to users (recall caching from earlier sessions; a CDN is caching for static files).
The database stores a pointer, not the payload. A message row becomes
{"text": "look!", "image_url": "https://cdn.chatapp.com/img/abc123.jpg"}. The heavy
bytes live in S3 and are served via the CDN โ keeping the database small, fast, and cheap.
The upload flow
The clean pattern is a pre-signed URL: the client uploads the image directly to object storage (not through your service), so big bytes never clog your servers. Here's the flow.
Step 2 in code. The Message Service asks S3 for a short-lived, signed URL and gives it to the client.
import boto3, uuid from fastapi import FastAPI app = FastAPI() s3 = boto3.client("s3") BUCKET = "chatapp-images" @app.post("/v1/uploads") def create_upload(content_type: str = "image/jpeg"): key = f"img/{uuid.uuid4().hex}.jpg" # unique file name # A temporary URL the client can PUT the image to directly. upload_url = s3.generate_presigned_url( "put_object", Params={"Bucket": BUCKET, "Key": key, "ContentType": content_type}, ExpiresIn=300, # valid for 5 minutes ) return { "upload_url": upload_url, # client PUTs the bytes here # the public address to store in the message later: "image_url": f"https://cdn.chatapp.com/{key}", }
The client then uploads the file with a plain PUT to
upload_url, and finally sends POST /v1/messages with
image_url in the body โ the same endpoint from Topic 3, now carrying a pointer
instead of bytes.
Because the upload skips your service, your API stays light. You can also fire an async event (Topic 4) to a worker that makes thumbnails or scans for malware in the background โ and the CDN means a user in Tokyo downloads from a nearby edge server, not your origin in Virginia.
โ Putting it all together
You just learned how the pieces of a modern system talk to each other and how to build one. Here's the one-paragraph story connecting all five topics:
Programs talk over HTTP APIs, and the cleanest style is REST โ resources at URLs, acted on by verbs, with honest status codes, statelessness, idempotency, and versioning. When one big monolith grows painful, we split it into independently deployable microservices along bounded contexts. We built exactly that for a chat app: a fast Message Service and a background Analytics Service. Those services communicate either synchronously (REST/gRPC, when you need the answer now) or asynchronously via a message queue (when work can happen later). And when users send photos, we never stuff blobs in the database โ we put them in object storage, serve them through a CDN, and keep just a URL in the message.
Quick self-check
Which HTTP method would you use to create a new message, and what success code should it return?
POST to the /messages resource, returning
201 Created (ideally with a Location header pointing at the
new message).
Why is PUT idempotent but POST is not?
PUT replaces a resource with what you send, so repeating it leaves the
same end state. POST creates a new thing each time, so repeating it creates duplicates โ
which is why you guard POSTs with an idempotency key.
You're building a brand-new product with a two-person team. Monolith or microservices?
Start with a monolith. It's simpler, faster to build, and has no network between parts. Split into microservices later, when real pain (independent scaling, multiple teams, separate deploys) demands it.
Why does the Message Service emit an event instead of calling the Analytics Service directly?
So chatting stays fast and reliable. Analytics is background work โ using an asynchronous queue means the Message Service never waits on it, and if Analytics is down the events simply buffer and get processed later.
When would you choose gRPC over REST/JSON between two services?
For high-traffic internal service-to-service calls where speed and small payloads matter. gRPC uses compact binary Protocol Buffers and a strict schema. REST/JSON wins for public APIs, browser clients, and easy debugging.
A teammate wants to store user photos as bytes in the messages table. What's wrong, and what's better?
Blobs bloat the database, slow queries and backups, and cost more โ databases aren't built to serve big files. Better: upload the image to object storage (e.g. S3) via a pre-signed URL, serve it through a CDN, and store only the URL in the message row.
๐ References & Further Reading
Class material
- ๐ Original course notes / handout (source sheet) โ open the shared class material for this session.
- Class handout: "[SST-2028] Chat Application / Microservices".
Papers, docs & deep dives
- Martin Fowler โ Microservices โ the canonical definition and trade-offs of microservices vs monoliths.
- Richardson Maturity Model โ a structured way to judge how RESTful an HTTP API really is.
- gRPC โ Introduction โ the high-performance RPC option for inter-service communication.
- Kafka: a Distributed Messaging System for Log Processing (original paper) โ async messaging between chat services.