API Foundations

REST API Design

The language every service on the internet speaks. From GitHub to Stripe to your own backend β€” here's how to design APIs that developers actually want to use.

8 Think Firsts 22 SVG Diagrams 15 Sections 5 Exercises 18 Tooltips
Section 1

TL;DR β€” The One-Minute Version

Mental Model: REST treats everything as a NOUN you can verb. Users, orders, payments β€” they're all resources with addresses (URLs). You GET them, POST new ones, PUT updates, DELETE them. That's the entire vocabulary.

Here's REST in plain English. You're building an APIApplication Programming Interface β€” a way for programs to talk to each other. When a mobile app fetches your profile from Instagram, it's calling Instagram's API. Think of it as a waiter taking your order to the kitchen and bringing back food. β€” a way for other programs to talk to your system. Instead of inventing a brand-new command for every action (getUser, fetchAllOrders, createNewProduct, removeItemFromCart), you organize everything around resources β€” the "things" in your system. Users, orders, products, carts. Each thing gets a URL. And the action you want to take? That's just the HTTP methodThe verb in an HTTP request β€” GET (read), POST (create), PUT (replace), PATCH (update partially), DELETE (remove). Think of them as the 5 things you can do to any resource. β€” GET to read, POST to create, PUT to replace, PATCH to tweak, DELETE to remove.

Don't take our word for it. Open a terminal right now and try this:

A real REST API call β€” right now
# This is a REAL API call to GitHub. Try it.
curl https://api.github.com/users/torvalds

# What you get back:
# {
#   "login": "torvalds",
#   "id": 1024025,
#   "name": "Linus Torvalds",
#   "company": "Linux Foundation",
#   "public_repos": 7,
#   "followers": 221000,
#   ...
# }

# That's REST. The URL (/users/torvalds) is the NOUN.
# The HTTP method (GET) is the VERB.
# The response is JSON β€” structured data.

That's a real REST API running in production right now. GitHub serves billions of these requests every day. The URL /users/torvalds is the resource (the noun β€” "Linus Torvalds's profile"). The HTTP method GET is the action (the verb β€” "read it"). The response is JSONJavaScript Object Notation β€” a lightweight format for sending data between systems. It looks like: {"name": "Linus", "repos": 7}. Nearly every REST API uses JSON because it's human-readable and easy for code to parse. β€” structured data your code can parse.

REST = Resources (nouns) + HTTP Methods (verbs) RESOURCES (Nouns) /users /orders /products /payments Every "thing" gets a URL + METHODS (Verbs) GET = Read POST = Create PUT = Replace DELETE = Remove Only 5 standard actions = REAL APIs GET /users/torvalds GitHub POST /v1/charges Stripe DELETE /2/tweets/123 Twitter/X Same pattern everywhere Learn the pattern once, use every API on the planet.

Once you learn this pattern, you can use any REST API β€” Stripe, GitHub, Twilio, Twitter β€” without reading much documentation, because they all speak the same language. The URLUniform Resource Locator β€” the address of a resource on the internet. In REST, the URL tells you WHAT thing you're working with (/users/42 = user #42). The HTTP method tells you WHAT you're doing to it (GET = read it, DELETE = remove it). tells you what you're talking about, and the method tells you what you want to do.

One-line takeaway: REST is just a convention: every "thing" in your system gets a URL, and you use standard HTTP methods (GET, POST, PUT, DELETE) to interact with it. No custom verbs, no guessing. Open Chrome DevTools right now, go to Network, and click any XHR request β€” you're looking at REST.
Section 2

Why You Need This β€” The API That Nobody Can Use

You join a startup. The product is growing fast. There's a backend with 47 API endpoints. Your first task: build a new feature in the mobile app that shows order history. You ask the backend team, "What's the endpoint for getting a user's orders?"

Silence. Then: "Check Slack, someone posted it once." You dig through Slack messages and piece together a few endpoints. They look like this:

The 47-endpoint API you inherited
# User endpoints β€” different naming styles
POST /getUserProfile           ← RPC-style, verb in URL
GET  /api/v2/fetch-orders?userId=5&sortBy=date  ← mixed conventions
POST /deleteAccount            ← POST for deletion??
GET  /api/getProductsByCategory?cat=electronics&page=2&limit=50

# Cart endpoints β€” even the team doesn't agree
POST /addToCart                 ← no resource noun
POST /cart/remove-item          ← kebab-case here
GET  /getCartItems?userId=7    ← camelCase there

# Order endpoints β€” chaos
POST /submitOrder
GET  /getOrderStatus?orderId=99
POST /order/cancel              ← completely different structure

A new developer joins and asks: "Where's the documentation?" There is none. "OK, how do I update a user?" Nobody knows if it's PUT or POST or PATCH. Three different people wrote these endpoints over 18 months, and each one followed their own convention. The API isn't wrong β€” it works. But nobody can use it without asking someone.

Mobile developer: "Why is 'delete account' a POST? I assumed POST means create. And /cart/remove-item is also POST? How do I know which POSTs create things and which ones destroy things?"

Partner integration team: "Is it getUserProfile or fetchUser or user.info? Every endpoint uses a different naming convention. I've been guessing for three days."

New hire (week 1): "There are 47 endpoints. I have to memorize each one? There's no pattern I can learn once and apply everywhere?"

QA engineer: "I put a CDNContent Delivery Network β€” servers spread around the world that cache responses close to users. A CDN automatically caches GET requests (because GET means 'just reading, safe to store'). But it won't cache POST requests (because POST means 'doing something'). If your API uses POST for everything, the CDN can't help you. in front of the API. It cached nothing because everything is POST. Our server costs are 10x what they should be."

A study at a large fintech company found their developers spent 30% of API integration time just figuring out endpoint names, parameter formats, and which HTTP method to use. That's almost a third of your engineering time burned on guessing games.

The real cost isn't the confusion β€” it's the compounding effect. Every new hire goes through the same discovery process. Every partner integration requires hand-holding. Every new feature requires reading source code instead of guessing the URL pattern. The API becomes a tax on everyone who touches it.

Slack's real story: Slack's API is RPC-style β€” chat.postMessage, channels.list, users.info. It works, but developers can't guess endpoint names. You must read the docs for every call. Compare this to GitHub's API, where once you learn GET /repos/{owner}/{repo}, you can guess that GET /repos/{owner}/{repo}/pulls exists β€” and you'd be right.

The Messy API β€” Every Endpoint Is a Surprise New Developer Day 1 on the job Which verb? Which URL format? GET or POST? camelCase or kebab? /api/v2 or /api? Query param or path? Asks Slack every time 47 ENDPOINTS β€” NO PATTERN POST /getUserProfile ← verb in URL GET /api/v2/fetch-orders?userId=5 ← mixed style POST /deleteAccount ← POST for delete?? POST /addToCart ← no resource noun POST /cart/remove-item ← kebab-case here GET /getCartItems?userId=7 ← camelCase there POST /submitOrder ← who named this? POST /order/cancel ← totally different 3 developers, 18 months, zero conventions = pure chaos
Think First

Look at the 47-endpoint API above. Can you spot at least 4 inconsistencies? Think about naming, HTTP methods, URL structure, and parameter style. Write down your list before scrolling down β€” you'll see every one of these problems dissected in the next two sections.

Section 3

The First Attempt β€” "Just Name Each Action" (RPC-Style APIs)

The messy API in Section 2 isn't random chaos. It's actually following a very common approach called RPC-styleRemote Procedure Call β€” an API style where each endpoint is a function name (verb + noun), like calling a method on a remote server. Examples: /getUser, /createOrder, /deleteItem. The URL IS the function name.. The idea is dead simple: every endpoint is a function call. You name the URL after what it does, exactly like you'd name a function in your code.

This isn't some outdated idea that nobody uses. Slack's entire API works this way right now. Open their API docs and you'll see real endpoints like these:

Slack's real API β€” RPC-style in production
# Slack's actual API endpoints (still in use today)
POST https://slack.com/api/chat.postMessage
POST https://slack.com/api/channels.list
POST https://slack.com/api/users.info
POST https://slack.com/api/files.upload
POST https://slack.com/api/reactions.add
POST https://slack.com/api/conversations.history

# Notice: EVERYTHING is POST.
# The URL IS the function name.
# The action is baked into the URL path.

And honestly? For small projects, this works fine. If you have 5 endpoints, naming them /getUser, /createUser, /deleteUser, /getOrders, /createOrder is easy enough. Everyone on the team knows what each one does. Slack has hundreds of millions of users and their RPC-style API handles it just fine.

RPC Style β€” Every Endpoint Is a Function Name getUser(id) createUser(data) deleteUser(id) updateUser(data) getOrders(userId) createOrder(data) cancelOrder(id) getOrderStatus(id) addToCart(item) removeFromCart(id) checkout(cartId) refundOrder(id) 12 endpoints, 12 unique names. Now imagine 200 of these β€” that's a real company.

The problem isn't that RPC is "wrong." Slack proves it works at massive scale. The problem is it doesn't scale as a convention. With 5 endpoints, you can memorize them. With 50, you need documentation for every single one. With 200, even the people who built the API can't remember all the names. There's no pattern to learn β€” every endpoint is a snowflake.

Here's the real kicker: can you guess what Slack's endpoint for editing a message is? Is it chat.editMessage? Or chat.update? Or messages.edit? (It's actually chat.update β€” you'd never guess that without reading the docs.) Now compare that to GitHub's REST API: if you know GET /repos/{owner}/{repo} fetches a repo, can you guess how to get its pull requests? GET /repos/{owner}/{repo}/pulls. And you'd be right. That's the difference between a convention you have to memorize vs. a convention you can predict.

The Guessing Game: RPC (Slack) vs REST (GitHub) SLACK (RPC) β€” Must Read Docs Send a message? chat.postMessage Edit a message? chat.update ?? Delete a message? chat.delete Pin a message? pins.add not chat.pin! No pattern. Must read docs every time. GITHUB (REST) β€” Just Guess Get a repo? GET /repos/{owner}/{repo} Get its PRs? GET /repos/.../pulls yes! Get PR #42? GET /repos/.../pulls/42 yes! Create a PR? POST /repos/.../pulls yes! Learn the pattern once. Guess everything.
The core issue: RPC-style APIs are verb-first β€” the URL describes what to do. This means every new action requires inventing a new endpoint name. REST flips this: the URL describes the thing, and the HTTP method describes the action. Fewer names to invent, fewer names to memorize, and developers can predict endpoints they've never seen before.
Section 4

Where It Breaks β€” Three Problems That Cost Real Money

RPC-style APIs don't fail in theory. They fail in practice, at scale, with real money on the line. Here are three specific problems β€” each one illustrated with a real company that hit it.

Think First

If every endpoint in your API uses POST (like Slack's does), what happens when you put a CDNContent Delivery Network β€” servers around the world that store copies of frequently-requested data close to users. CDNs only cache GET requests automatically, because GET means "just reading, safe to store a copy." POST means "doing something" so CDNs don't cache it. in front of it? What about browser caching? Think about it before reading the numbers below.

Problem 1: You Can't Cache Anything

HTTP cachingThe mechanism where browsers, CDNs, and proxy servers store copies of responses so they don't have to ask the server again. It only works on GET requests β€” because GET means "I'm just reading, the data won't change." POST means "I'm doing something," so caches never store POST responses. is one of the most powerful performance tools on the internet. Every browser, every CDN, every proxy server already knows how to cache. But they only cache GET requests β€” because GET means "I'm just reading, this is safe to store." POST means "I'm changing something, don't store this."

If your API uses POST for everything (like Slack and many RPC APIs do), you've thrown away all of that free caching infrastructure. Let's do the math on what that actually costs:

The Caching Tax: RPC vs REST at 100M Requests/Day RPC (All POST) Requests/day: 100,000,000 CDN cache hit rate: 0% Requests hitting server: 100,000,000 Server cost/month: $50,000 Browser re-fetches: Every single time POST = caches can't help you REST (GET for Reads) Requests/day: 100,000,000 CDN cache hit rate: 90% Requests hitting server: 10,000,000 Server cost/month: $5,000 Browser re-fetches: Only when ETag changes GET = entire internet helps you cache Same traffic. 10x cost difference. Just because of POST vs GET.

This isn't hypothetical. When you use GET /users/42, the CDN can cache that response. The next 1,000 people asking for user #42 get the answer from the CDN β€” your server never even sees the request. But POST /getUser? The CDN won't touch it. Every single request hits your server. At scale, this is the difference between a $5K/month server bill and a $50K/month one.

GitHub's API makes this concrete. Try this in your terminal:

GitHub's caching headers β€” real response
# HEAD request β€” just the headers, no body
curl -I https://api.github.com/users/torvalds

# Response headers (the ones that matter):
# HTTP/2 200
# cache-control: public, max-age=60, s-maxage=60
# etag: "abc123..."
# last-modified: Mon, 15 Jan 2024 10:30:00 GMT
# x-ratelimit-limit: 60
# x-ratelimit-remaining: 57

# See those? cache-control, etag, last-modified.
# These tell CDNs and browsers: "Cache this for 60 seconds.
# After that, check if it changed using the ETag."
# This ONLY works because it's a GET request.

Problem 2: Nobody Agrees on Error Handling

Here's something that actually happened with the early Twitter API. You'd make a request that failed β€” maybe the tweet didn't exist, or you hit the rate limit. The API would respond with HTTP 200 OK (which means "everything is fine!") and then put the error inside the JSON body. Your code had to parse the body to find out the request actually failed.

This wasn't unique to Twitter. It's a common RPC-style pattern because RPC treats every endpoint as a function call, and function calls return results β€” including error results. The HTTP status code is just a transport detail, not a semantic signal.

Slack error response β€” HTTP 200 with error in body
// HTTP 200 OK  ← says "everything is fine!"
// But wait...
{
  "ok": false,
  "error": "channel_not_found",
  "response_metadata": {
    "scopes": ["chat:write"]
  }
}

// The HTTP status says success.
// The body says failure.
// Your code has to parse the body to know what happened.
// Every error check looks like: if (!response.ok) ...
GitHub error response β€” proper HTTP status codes
// HTTP 404 Not Found  ← tells you immediately
{
  "message": "Not Found",
  "documentation_url": "https://docs.github.com/..."
}

// HTTP 403 Forbidden  ← rate limited
{
  "message": "API rate limit exceeded for 1.2.3.4",
  "documentation_url": "https://docs.github.com/..."
}

// HTTP 422 Unprocessable Entity  ← bad input
{
  "message": "Validation Failed",
  "errors": [{"field": "title", "code": "missing"}]
}

// No ambiguity. The status code tells you what happened
// BEFORE you even parse the body.
Stripe error response β€” detailed AND proper status
// HTTP 402 Payment Required  ← card was declined
{
  "error": {
    "type": "card_error",
    "code": "card_declined",
    "decline_code": "insufficient_funds",
    "message": "Your card has insufficient funds.",
    "param": "source"
  }
}

// The status code (402) tells middleware & monitoring
// "this is a payment problem, not a server bug."
// The body tells the developer exactly what went wrong.
// Both layers carry meaning.

Why does this matter? Because HTTP status codesThree-digit numbers that tell the client what happened. 2xx = success. 3xx = redirect. 4xx = client made a mistake (bad request, not found, unauthorized). 5xx = server had an error. Every piece of internet infrastructure β€” browsers, load balancers, monitoring tools β€” understands these codes natively. aren't just numbers for developers to read. They're signals that the entire internet understands. Your monitoring system can alert on 5xx spikes. Your load balancer can retry 503s but not 400s. Your browser shows a "page not found" error for 404s. When you return 200 for everything, all of that tooling goes blind.

Problem 3: Endpoint Explosion

Let's count. Your startup has 8 types of things (resources): users, products, orders, carts, reviews, payments, shipments, and coupons. In RPC-style, each thing needs its own set of endpoints:

Endpoint Explosion: RPC vs REST RPC β€” Unique Name Each getUser, createUser, updateUser, deleteUser getProduct, createProduct, updateProduct... getOrder, createOrder, cancelOrder... getCart, addToCart, removeFromCart... getReview, createReview, flagReview... processPayment, refundPayment... getShipment, trackShipment, updateShipment getCoupon, validateCoupon, applyCoupon... 47+ unique endpoints 47 names to invent, document, memorize REST β€” Same 5 Verbs, Reused GET|POST|PUT|PATCH|DELETE /users GET|POST|PUT|PATCH|DELETE /products GET|POST|PUT|PATCH|DELETE /orders GET|POST|PUT|PATCH|DELETE /carts GET|POST|PUT|PATCH|DELETE /reviews GET|POST|PUT|PATCH|DELETE /payments GET|POST|PUT|PATCH|DELETE /shipments GET|POST|PUT|PATCH|DELETE /coupons 8 resources x 5 verbs = 40 8 nouns to learn. Same 5 verbs everywhere.

The numbers look similar (47 vs 40), but the difference is cognitive, not mathematical. With RPC, you need to memorize 47 unique endpoint names. With REST, you learn 8 resource names and 5 verbs β€” and you can predict every combination. That's 13 things to learn instead of 47. And when someone adds a 9th resource (say, /notifications), you already know all its endpoints without being told.

Facebook's interesting detour: Facebook's Graph API looks REST-like (/me/friends, /me/photos) but is actually a graph query languageInstead of standard REST resources, Facebook treats everything as a "node" with "edges." You can traverse relationships: /me/friends/photos gives you photos of your friends. It's more like a database query than a REST endpoint, which is why Facebook eventually created GraphQL as a separate query language.. You can write /me/friends?fields=name,email&limit=10 β€” essentially querying a graph database through URLs. It works, but it's not REST. This confusion about "what counts as REST" is exactly why understanding the actual principles matters.
Section 5

The Breakthrough β€” Think in Resources, Not Actions

In the year 2000, a PhD student named Roy Fielding at UC Irvine was writing his doctoral dissertation. He'd been one of the principal authors of the HTTP specification β€” the protocol that makes the entire web work. His thesis, titled "Architectural Styles and the Design of Network-Based Software Architectures," had a chapter (Chapter 5) that would change how we build APIs forever. He called the idea REST β€” REpresentational State TransferThe full name is rarely used. What matters is the core idea: organize your API around resources (things, nouns) that clients interact with by transferring representations (usually JSON) of those resources' state. In plain English: "here's the data for user #42, as JSON.".

The key insight was deceptively simple: stop thinking in functions, start thinking in resources.

Think First

Take the messy API from Section 2. Try to reorganize those endpoints using only these resource nouns: /users, /orders, /products, /carts. Use GET, POST, PUT, PATCH, DELETE as your verbs. How many unique names do you need to invent?

Here's what Fielding figured out. Instead of inventing a new URL for every action:

The transformation: verbs in URLs to nouns in URLs
# BEFORE: The URL is a verb (what to DO)
/getUser?id=5         β†’ GET  /users/5
/createOrder          β†’ POST /orders
/updateProduct?id=3   β†’ PUT  /products/3
/deleteAccount?id=7   β†’ DELETE /accounts/7
/getUserOrders?id=5   β†’ GET  /users/5/orders

# The URL is always a NOUN (the thing).
# The HTTP method is always the VERB (the action).
# This combination is so predictable that a developer
# can GUESS the API without reading docs.
The Transformation: RPC to REST BEFORE (RPC-Style) POST /getUserProfile GET /api/v2/fetch-orders?userId=5 POST /deleteAccount POST /addToCart POST /cart/remove-item GET /getCartItems?userId=7 POST /submitOrder GET /getOrderStatus?orderId=99 POST /order/cancel GET /getProductsByCategory?cat=... 10 unique names. 3 naming styles. No pattern. AFTER (REST-Style) /users GET /users/5 (profile) DELETE /users/5 (delete) /orders GET /users/5/orders (list) POST /orders (create) GET /orders/99 (status) DELETE /orders/99 (cancel) /carts/{userId}/items GET /carts/7/items (list) POST /carts/7/items (add) DELETE /carts/7/items/42 3 resources. Same 5 verbs. Predictable. Stripe's real API follows exactly this pattern: POST /v1/charges, GET /v1/customers/{id}

Look what happened. Ten unique endpoint names with three different naming styles collapsed into three resource nouns with the same five verbs. A developer who's never seen your API can guess the endpoints. "I bet there's a GET /orders and a GET /orders/99." And they'd be right. That's the power of a convention β€” you learn the pattern once and apply it everywhere.

And this is exactly what the best real-world APIs do. Here's Stripe β€” widely considered the gold standardStripe's API is so well-designed that other companies use it as a reference when building their own. It follows REST conventions strictly: resources as nouns, proper HTTP methods, proper status codes, cursor-based pagination, and idempotency keys for safe retries. of API design:

Stripe's real API β€” the gold standard
# Creating a charge (real Stripe API call)
curl -X POST https://api.stripe.com/v1/charges \
  -u sk_test_xxx: \
  -d amount=2000 \
  -d currency=usd \
  -d source=tok_visa

# Response: HTTP 201 Created
# {
#   "id": "ch_1234",
#   "object": "charge",
#   "amount": 2000,
#   "currency": "usd",
#   "status": "succeeded"
# }

# Notice the design choices:
# - POST (creating a new charge) β€” not /createCharge
# - /v1/charges (noun, plural) β€” the resource
# - HTTP 201 (not 200) β€” "created", not just "ok"
# - Versioning via URL path (/v1/)
# - Auth via HTTP Basic (-u flag)
Stripe's API β€” Pure Resource-Oriented Design /v1/customers POST /v1/customers GET /v1/customers/{id} POST /v1/customers/{id} DELETE /v1/customers/{id} GET /v1/customers Same 5 operations /v1/charges POST /v1/charges GET /v1/charges/{id} POST /v1/charges/{id} GET /v1/charges Same 5 operations /v1/subscriptions POST /v1/subscriptions GET /v1/subscriptions/{id} POST /v1/subscriptions/{id} DELETE /v1/subscriptions/{id} GET /v1/subscriptions Same 5 operations 50+ resources, all following the exact same pattern. Learn one, know them all.

This is RESTREpresentational State Transfer β€” an architectural style for APIs defined by Roy Fielding in his 2000 PhD dissertation at UC Irvine. The key idea: organize your API around resources (nouns) and use standard HTTP methods (verbs) to manipulate them. The "representational state transfer" part means: the client sends and receives representations (usually JSON) of a resource's current state. β€” REpresentational State Transfer. Don't memorize that name. Just remember: the URL is always a noun (the thing), the HTTP method is always a verb (the action). That's the entire philosophy.

REST doesn't add any new technology. There's no library to install, no protocol to learn. It's purely a convention β€” an agreement about how to organize URLs and use HTTP methods in a predictable way. The same HTTP that loads web pages in your browser already speaks REST. You just need to use it properly.

The aha moment: REST works because it works with the web, not against it. GET requests get cached. Status codes get monitored. URLs get bookmarked. IdempotentAn operation is idempotent if doing it once has the same effect as doing it 10 times. PUT /users/5 with the same data always produces the same result. DELETE /users/5 deletes the user β€” calling it again just returns 404 (already gone). This matters for retries: if a network glitch happens, you can safely retry idempotent requests. methods get retried safely. Every piece of internet infrastructure already understands HTTP. REST is the realization that APIs should speak the same language the web already speaks.
Section 6

How It Works β€” The Six Building Blocks

Think First

A client sends DELETE /users/42 twice in a row. Should the second request fail with an error, or succeed silently? What about POST /users sent twice β€” should it create one user or two? Think about what makes an operation safe to repeat vs dangerous to repeat.

REST is built on six core concepts. You already understand the main idea β€” resources as nouns, HTTP methods as verbs. Now let's go deeper into each building block so you can design APIs that are clean, consistent, and production-ready.

The Six Building Blocks of REST Resources The nouns Methods The verbs Status Codes The outcomes Req / Resp The conversation Headers The metadata Content Neg. The format Master these six, and you can design any REST API. Click each card below to explore.

1. Resources & URLs β€” Naming the Things in Your System

A resourceAny "thing" in your system that has identity and can be addressed by a URL. Users, orders, products, comments, payments β€” all resources. Even abstract concepts like "search results" can be resources. is any "thing" your API deals with. Users, books, orders, payments, reviews. Each resource gets a URL β€” its address on the web. The URL tells the client what they're talking about.

Here are the rules that every good REST API follows for naming resources:

  • Use nouns, never verbs β€” /books not /getBooks. The HTTP method IS the verb.
  • Use plurals β€” /users not /user. The collection is plural; a single item is /users/42.
  • Use lowercase with hyphens β€” /order-items not /orderItems or /OrderItems.
  • Nest for relationships β€” /users/7/orders means "orders belonging to user 7".
  • Keep it shallow β€” max 2-3 levels deep. /users/7/orders/99/items is okay. Going deeper is a smell.
URL Anatomy Collection GET /books Single Item GET /books/42 Nested Resource GET /books/42/reviews "All the books" "The book with ID 42" "Reviews of book 42" Common mistakes: /getBooks βœ— /book βœ— /BooksList βœ— /api/v1/get-all-books βœ—

2. HTTP Methods β€” The Five Things You Can Do

HTTP gives you five standard methods. Think of them as the five actions you can perform on any resource β€” like a universal remote control that works with every TV.

The Five HTTP Methods METHOD MEANING EXAMPLE SAFE? IDEMPOTENT? GET Read / Fetch GET /books/42 Yes Yes POST Create new POST /books No No PUT Replace entirely PUT /books/42 No Yes PATCH Update partially PATCH /books/42 No Sometimes DELETE Remove DELETE /books/42 No Yes Safe = doesn't change data. Idempotent = calling it 10 times has the same effect as calling it once.

Two words worth knowing here. SafeA "safe" HTTP method doesn't modify anything on the server. GET is safe β€” it just reads. POST, PUT, PATCH, DELETE are not safe β€” they change state. Browsers and proxies use this to decide what's safe to retry or cache. means the method doesn't change anything β€” GET just reads, like looking through a shop window. IdempotentAn operation is idempotent if doing it once or doing it ten times gives the same result. PUT and DELETE are idempotent β€” deleting the same resource twice doesn't delete it "more." POST is NOT β€” posting the same order twice creates two orders. means doing it once or ten times gives the same result. DELETE is idempotent β€” deleting a book that's already deleted doesn't delete it "more." POST is NOT idempotent β€” posting the same order twice creates two orders. This matters hugely for retries and error handling, which we'll cover in Section 7.

PUT vs PATCH β€” the difference that trips everyone up: PUT means "here's the complete replacement" β€” if you PUT a book with just { "title": "New Title" }, the price, author, and every other field gets erased. PATCH means "here are just the changes" β€” you can send { "price": 29.99 } and only the price updates. Most real APIs use PATCH for updates.

3. Status Codes β€” How the Server Says "Here You Go" or "Nope"

Every HTTP response comes with a three-digit number that tells the client what happened. You don't need to memorize all 70+ status codes β€” just these categories and the 10-12 most common ones.

Status Code Families 2xx SUCCESS 200 OK 201 Created 204 No Content "It worked!" 3xx REDIRECT 301 Moved Perm. 304 Not Modified 307 Temp. Redirect "Look elsewhere" 4xx CLIENT ERROR 400 Bad Request 401 Unauthorized 404 Not Found "You messed up" 5xx SERVER ERROR 500 Internal Error 502 Bad Gateway 503 Unavailable "We messed up" Which Code for Which Situation? GET /books/42 β†’ 200 (found it!) or 404 (no such book) POST /books β†’ 201 (created!) or 400 (invalid data) PUT /books/42 β†’ 200 (updated!) or 404 (no such book) DELETE /books/42 β†’ 204 (deleted!) or 404 (already gone)

A quick rule of thumb: 2xx means everything's fine, 4xx means the client did something wrong (bad data, wrong URL, no permissions), and 5xx means the server broke. The most common mistake is returning 200 for everything and hiding the error in the response body β€” don't do that. Clients, monitoring tools, and load balancersA server that distributes incoming traffic across multiple backend servers. Load balancers use status codes to decide if a backend is healthy β€” too many 5xx responses means "stop sending traffic there." all rely on status codes to behave correctly.

4. Request & Response β€” The Conversation Format

Every API call is a conversation: the client asks, the server answers. Let's look at what a real exchange looks like.

POST /books HTTP/1.1
Host: api.bookstore.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOi...

{
  "title": "Designing Data-Intensive Applications",
  "author": "Martin Kleppmann",
  "price": 45.99,
  "isbn": "978-1449373320"
}

Line 1: Method + URL + HTTP version. "I want to CREATE a new book."

Headers: Metadata β€” what format the body is in, who I am (auth token).

Body: The actual data β€” the book I want to create.

HTTP/1.1 201 Created
Content-Type: application/json
Location: /books/143

{
  "id": 143,
  "title": "Designing Data-Intensive Applications",
  "author": "Martin Kleppmann",
  "price": 45.99,
  "isbn": "978-1449373320",
  "created_at": "2026-03-18T10:30:00Z"
}

Status line: 201 Created β€” "I made the book for you."

Location header: "Here's where you can find it: /books/143"

Body: The created resource, now with an ID and timestamp the server added.

Notice how the response echoes back the full resource, not just "ok." This lets the client update its local state without making a second GET request. The Location header tells the client the URL of the newly created resource β€” a small courtesy that saves a round trip.

5. Headers β€” The Envelope, Not the Letter

If the URL and body are the "letter" you're sending, headers are the "envelope" β€” they carry information about the message without being the message itself. Think of headers like the labels on a package: who sent it, how fragile it is, when it expires.

Headers You'll Use Every Day AUTHENTICATION Authorization: Bearer <token> "Who I am" β€” JWT or API key X-API-Key: sk_live_abc123 CONTENT Content-Type: application/json "The body is JSON" Accept: application/json CACHING Cache-Control: max-age=3600 "Cache this for 1 hour" ETag: "v2-abc123" RATE LIMITING X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 847 "You have 847 calls left" PAGINATION Link: </books?page=3>; rel="next" "Next page is at /books?page=3" X-Total-Count: 1472 DEBUGGING X-Request-Id: req_abc123 "Trace this call in logs" X-Response-Time: 47ms

6. Content Negotiation β€” "Send Me JSON, Please"

Content negotiation is a fancy term for a simple idea: the client tells the server what format it wants the data in, and the server tries to comply. It's like walking into a restaurant and saying "I'd like the menu in English, please" β€” the food is the same, but the presentation matches your preference.

The client sends an Accept header saying what format it can handle:

GET /books/42 HTTP/1.1
Accept: application/json

The server responds with the data in that format, and tells you what it sent via Content-Type:

HTTP/1.1 200 OK
Content-Type: application/json

{ "id": 42, "title": "DDIA", "price": 45.99 }

In practice, 99% of modern REST APIs use JSON exclusively. You'll occasionally see XML in legacy enterprise systems, or CSV for data export endpoints. But JSON won the format war years ago, so content negotiation is mostly a formality β€” your API will almost certainly speak JSON and nothing else.

Quick tip: Always set Content-Type: application/json on your responses, even if you think it's obvious. Some HTTP clients behave differently when the content type is missing β€” they might try to parse the response as HTML or plain text instead of JSON.
Section 7

Going Deeper β€” The Details That Separate Good APIs from Great Ones

You now know the fundamentals β€” resources, methods, status codes, headers. That's enough to build a working REST API. But a great API handles the hard stuff too: How do you change your API without breaking everyone? How do you return 10 million records? How do clients retry safely? Let's dig in.

Versioning β€” Changing Your API Without Breaking the World

Your API is live. Thousands of apps depend on it. Now you need to change the response format β€” maybe rename a field, or restructure the data. If you just change it, every client breaks. That's why APIs need versioningA strategy for evolving your API over time while maintaining backward compatibility. Old clients keep using v1, new clients use v2. You support both until everyone migrates. β€” a way to evolve without breaking existing users.

Think First

You have an API with 500 active clients. You need to rename user_name to username in the response. How would you roll this out safely? Think of at least 2 approaches.

Three Versioning Strategies URL Path (Most Common) /v1/books /v2/books + Simple to understand + Easy to route in load balancers + Visible in browser/docs - Pollutes the URL namespace Used by: Stripe, GitHub, Twilio Custom Header GET /books API-Version: 2 + Clean URLs + Separates concerns - Hidden from casual browsing - Harder to test in browser Used by: Azure, some Microsoft APIs Query Parameter /books?version=2 /books?api-version=2 + Easy to test + Optional (default to latest) - Mixes versioning with data - Can break caching Used by: Google, Amazon

The industry favorite is URL path versioning β€” /v1/books, /v2/books. It's the simplest to understand, the easiest to route, and the most visible. Stripe, GitHub, and Twilio all use it. For most teams, this is the right choice. Don't overthink it.

Pagination β€” Returning a Million Records Without Crashing

Your bookstore has 2 million books. A client calls GET /books. Do you return all 2 million in one response? Of course not β€” that would be a 500MB JSON blob that takes 30 seconds to download and crashes the client's browser. You need paginationBreaking a large collection into smaller chunks ("pages") that clients can request one at a time. Like reading a book one chapter at a time instead of the whole thing at once. β€” breaking the results into pages.

The simple approach: tell the server "give me page 3, with 20 items per page."

GET /books?page=3&limit=20

Response:
{
  "data": [ ... 20 books ... ],
  "pagination": {
    "page": 3,
    "limit": 20,
    "total": 1847,
    "total_pages": 93
  }
}

Pros: Easy to implement. Clients can jump to any page. Great for UI with page numbers.

Cons: If someone inserts a new book while you're paginating, you might see duplicates or skip items. At high offsets (page=50000), the database has to scan and skip millions of rows β€” very slow.

The scalable approach: instead of a page number, use a "bookmark" β€” a pointer to where you left off.

GET /books?limit=20&after=eyJpZCI6MTAwfQ==

Response:
{
  "data": [ ... 20 books ... ],
  "pagination": {
    "next_cursor": "eyJpZCI6MTIwfQ==",
    "has_more": true
  }
}

Pros: Consistent even when data changes. Fast at any depth β€” the database uses an index seek, not a scan. Perfect for infinite scroll.

Cons: Can't jump to "page 500." Clients must follow the chain: page 1 β†’ page 2 β†’ page 3. The cursor is opaque β€” clients shouldn't try to decode or construct it.

Offset vs Cursor β€” Performance at Scale OFFSET: page=5000, limit=20 ⟡ scan & skip 99,980 rows ⟢ 20 rows Slow! DB reads 100K rows, returns 20. O(offset + limit) β€” gets worse with depth Page 1: 2ms | Page 5000: 3,400ms CURSOR: after=id_99980, limit=20 ↓ seek directly 20 rows Fast! DB seeks to cursor, reads 20. O(limit) β€” constant regardless of depth Page 1: 2ms | Page 5000: 2ms
Rule of thumb: Use offset pagination for admin dashboards and small datasets where users need "page 5 of 10." Use cursor pagination for feeds, timelines, search results, and any dataset that might grow large. When in doubt, cursor is the safer bet.

Filtering & Sorting β€” Finding the Needle in the Haystack

Pagination gives you pages. But clients rarely want all books β€” they want science fiction books under $20, sorted by rating. That's filteringNarrowing down results based on criteria. In REST, filters are typically query parameters: /books?genre=sci-fi&max_price=20. The server applies these as WHERE clauses in the database query. and sortingOrdering results by a specific field. In REST: /books?sort=price (ascending) or /books?sort=-price (descending, with a minus prefix). Can sort by multiple fields: ?sort=-rating,price..

# Filter by genre AND price, sort by rating (descending)
GET /books?genre=sci-fi&max_price=20&sort=-rating&limit=20

# Filter by author, sort by publication date (newest first)
GET /books?author=kleppmann&sort=-published_at

# Search with a query string
GET /books?q=distributed+systems&in_stock=true

The conventions: use query parameters for filters, prefix with - for descending sort, use q for full-text search. Keep filter names consistent with your response field names β€” if the JSON says "genre": "sci-fi", the filter should be ?genre=sci-fi, not ?book_genre=sci-fi.

Idempotency Keys β€” Making Retries Safe

Here's a scary scenario. A client sends POST /orders to place an order. The server creates the order and charges the credit card. But the response never arrives β€” the network drops it. The client doesn't know if the order went through, so it retries. Without protection, the customer gets charged twice.

Think First

How would you prevent duplicate orders when clients retry? The server already processed the first request β€” how can it recognize the second request as a duplicate? Think about it before reading on.

The solution is idempotency keysA unique ID that the client generates and sends with each request. If the server receives the same key twice, it returns the original response instead of processing the request again. Stripe popularized this pattern β€” every payment API uses it now.. The client generates a unique ID and sends it with the request. The server stores this ID alongside the result. If the same ID shows up again, the server returns the original response instead of processing the request a second time.

Idempotency Keys β€” Safe Retries Client Server Idempotency Store 1st Request: Idempotency-Key: abc-123 Not found β†’ process Store: abc-123 β†’ 201 Created 201 Created (order #99) βœ— Response lost! Network failure. Retry (same key): Idempotency-Key: abc-123 Found! Return cached. 201 Created (order #99) β€” same response, no duplicate!

Stripe uses this for every payment. You send an Idempotency-Key header, and Stripe stores the result for 24 hours. If the same key appears again, Stripe returns the original result without charging the card again. This is such a critical pattern that it should be built into every API that processes money, creates resources, or triggers side effects.

Rate Limiting Headers β€” "You're Calling Too Much"

APIs need to protect themselves from clients that call too often β€” whether it's a bug in someone's code, a DDoS attack, or a script gone wild. Rate limitingRestricting how many requests a client can make in a given time window. E.g., "1000 requests per hour." When the limit is exceeded, the server returns 429 Too Many Requests. caps the number of requests per time window, and good APIs tell you exactly where you stand via headers.

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000        # Max requests per hour
X-RateLimit-Remaining: 847     # Requests left in this window
X-RateLimit-Reset: 1710761400  # When the window resets (Unix timestamp)

# When you hit the limit:
HTTP/1.1 429 Too Many Requests
Retry-After: 342               # Wait 342 seconds before retrying

Good clients check X-RateLimit-Remaining proactively and slow down before hitting the limit. The Retry-After header on a 429 response tells the client exactly when it's safe to try again β€” so it doesn't have to guess.

Section 8

The Variations β€” REST vs RPC vs GraphQL

Think First

A mobile app needs to display a user's profile, their 3 most recent orders, and a list of recommended products β€” all on one screen. With REST, that's 3 separate API calls (GET /users/42, GET /users/42/orders?limit=3, GET /recommendations). What if the mobile app is on 3G with 800ms latency per round trip? That's 2.4 seconds just in network time. Can you think of a way to get all this data in a single request?

REST isn't the only way to build APIs. It's the most popular, but two other styles are widely used β€” and each one exists because REST can't solve every problem perfectly. Let's compare them honestly, without fanboyism.

Three API Styles β€” Pick the Right Tool REST "Resources + HTTP methods" GET /users/42 POST /orders + Cacheable (GET) + Universal convention + Works with HTTP infra + Easy to understand - Over-fetching (get all fields) - N+1 calls for related data Best for: Public APIs, CRUD apps, microservices, mobile backends Stripe, GitHub, Twilio RPC / gRPC "Call remote functions" userService.GetUser(42) orderService.Create(data) + Very fast (binary protocol) + Strongly typed contracts + Bi-directional streaming + Code generation - Not browser-friendly - Harder to debug (binary) Best for: Internal service-to-service, high-perf microservices Google, Netflix, Uber GraphQL "Ask for exactly what you need" { user(id:42) { name, orders { total } }} + No over-fetching + One request for nested data + Self-documenting schema + Great developer tools - Hard to cache (POST only) - Complex server implementation Best for: Frontend-driven apps, mobile with varied data needs Facebook, Shopify, GitHub (v4)

Here's the practical truth: most companies use REST for public APIs and gRPC for internal service-to-service communication. GraphQL thrives when you have many different clients (mobile, web, third-party) that each need different slices of the same data. GitHub actually offers both β€” their REST API (v3) and GraphQL API (v4) coexist, and you pick whichever fits your use case.

REST's weakness #1: You always get the entire resource, even if you only need one field.

# You just want the user's name. REST returns EVERYTHING:
GET /users/42

{
  "id": 42,
  "name": "Alice",           ← you wanted this
  "email": "alice@...",      ← didn't need this
  "avatar_url": "...",       ← or this
  "address": { ... },        ← definitely not this (1KB of data)
  "preferences": { ... }     ← or this
}

# GraphQL lets you ask for just what you need:
# { user(id: 42) { name } }  β†’  { "name": "Alice" }

For desktop apps on fast connections, over-fetching barely matters. For mobile apps on 3G? Sending 10KB when you need 50 bytes adds up fast across hundreds of API calls.

REST's weakness #2: To get a user and their 5 most recent orders, you need multiple calls.

# Step 1: Get the user
GET /users/42           β†’ { "id": 42, "name": "Alice" }

# Step 2: Get their orders (separate call!)
GET /users/42/orders    β†’ [ { "id": 1, ... }, { "id": 2, ... } ]

# Step 3: Get items for each order (5 MORE calls!)
GET /orders/1/items     β†’ [ ... ]
GET /orders/2/items     β†’ [ ... ]
...

# That's 7 round trips for one screen of data!

# GraphQL does it in ONE:
# { user(id:42) { name, orders { id, items { name } } } }

This is the N+1 problemWhen fetching a list of N items requires N additional requests to get related data. 1 request for the list + N requests for details = N+1 total. GraphQL and RPC solve this with nested queries or batching.. REST APIs often address it with ?include=orders,items or ?expand=orders.items query parameters, but it's a bolt-on β€” not a native feature.

So should you always use GraphQL? No. GraphQL adds complexity (schema definitions, resolvers, query cost analysis, N+1 on the server side). REST is simpler to build, simpler to cache, simpler to monitor. Pick GraphQL when you have genuinely different data needs across clients. Pick REST when your API is straightforward CRUD.
Section 9

At Scale β€” How Stripe, GitHub, and Twilio Do It

Think First

Stripe handles payments β€” an operation where failures are catastrophic. A customer clicks "Pay" and the network hiccups. The client retries. How would you design the API so that retrying POST /v1/charges doesn't charge the customer twice? Stripe solved this elegantly β€” think about what you'd put in the request header.

Theory is nice, but how do the world's best APIs actually work? Let's look at three APIs that are consistently rated among the best β€” and what specific choices they made that you can steal for your own designs.

The Gold Standard β€” APIs Developers Love Stripe Payments Billions of $ processed/year "The API that every API aspires to be" GitHub Developer Platform 100M+ developers "REST v3 + GraphQL v4 coexist" Twilio Communications SMS, Voice, Video "So simple, you can call it from curl"

Stripe β€” The API Every API Aspires to Be

Stripe processes billions of dollars in payments. Their API is so well-designed that it's become the gold standard for how REST APIs should work. Here's what they do right:

// Stripe error response β€” clear, actionable, consistent
{
  "error": {
    "type": "card_error",
    "code": "card_declined",
    "message": "Your card was declined.",
    "param": "source",
    "decline_code": "insufficient_funds"
  }
}

GitHub β€” REST and GraphQL Living Together

GitHub is interesting because they offer both API styles. Their REST API (v3) has been around for years and is battle-tested. Their GraphQL API (v4) launched in 2017 to solve the over-fetching problem that mobile developers complained about.

Twilio β€” So Simple You Can Use It From curl

Twilio's design philosophy is radical simplicity. Sending an SMS is one API call. Making a phone call is one API call. They optimized for "time to first hello world" β€” a new developer should be able to send their first text message in under 5 minutes.

Steal These Ideas for Your Own APIs Consistency Same naming, same patterns, same error format on every endpoint. Learn once, use everywhere. All three do this Great Errors Machine-readable codes + human-readable messages + which field caused it. Developers can fix fast. Stripe is the gold standard Discoverability Links to related resources in every response. Clients navigate via links, not hard-coded URLs. GitHub does this best Time to Hello How fast can a new developer make their first API call? Twilio: under 5 minutes. That's the target. Twilio leads here
Section 10

The Anti-Lesson β€” When REST Is the Wrong Choice

REST is wonderful for most APIs. But it's not the answer to everything. Forcing REST onto problems it wasn't designed for leads to ugly, inefficient APIs. Here are the situations where you should reach for something else.

When REST Is the Wrong Tool Real-Time Data (chat, live scores, stock tickers) REST is request-response. Client asks, server answers. But for chat, the server needs to PUSH to the client. Use instead: WebSockets, Server-Sent Events, gRPC streaming Complex Queries (analytics, dashboards) "Get me sales by region, grouped by month, filtered by product category, compared to last year." Use instead: GraphQL or a dedicated query API High-Performance Internal Services Service A calls Service B 50,000 times per second. JSON parsing overhead matters at this scale. Use instead: gRPC (binary protocol, 10x faster) Actions That Don't Map to CRUD "Merge these two accounts." "Recalculate all prices." "Run this report." These are verbs, not nouns. Pragmatic fix: POST /accounts/42/merge (action endpoint) Forcing REST onto non-CRUD operations creates awkward, confusing APIs.

The last point is worth emphasizing. Purists insist that everything must be a noun. But POST /accounts/42/actions/merge is genuinely worse than POST /accounts/42/merge. The best APIs are pragmatic β€” they follow REST conventions where they fit and deviate where they don't. Stripe uses POST /charges/{id}/capture for capturing a payment. That's a verb, and it's fine.

The #1 REST mistake in interviews: Using REST for everything, then struggling to explain how to model "transfer money from account A to account B" as a resource. Just say: "This is an action, so I'd use POST /transfers with a body containing source and destination. Not everything has to be pure REST."
Section 11

Common Mistakes β€” What Engineers Get Wrong

These are the mistakes I see in code reviews, interviews, and production APIs. Each one seems minor, but at scale they compound into APIs that are painful to use.

What people do: GET /getUsers, POST /createOrder, DELETE /deleteBook/42

Why it's wrong: The HTTP method IS the verb. Adding a verb to the URL is redundant β€” like saying "I want to GET the getUsers." It also breaks the REST convention, making your API harder to learn.

The fix: GET /users, POST /orders, DELETE /books/42. Let the method do its job.

What people do: Return 200 OK with { "success": false, "error": "Not found" }

Why it's wrong: Monitoring tools count 4xx/5xx errors to trigger alerts. Load balancers use status codes to route traffic. Returning 200 for errors means your infrastructure thinks everything is fine when it's not. Your error rate dashboard shows 0% while users are getting errors.

The fix: Use the correct status code: 404 for not found, 400 for bad input, 401 for unauthorized, 500 for server errors. Save 200 for actual success.

What people do: Mix camelCase, snake_case, and kebab-case in the same API. URLs use /orderItems, JSON uses order_items, headers use X-Order-Items.

Why it's wrong: Inconsistency forces developers to memorize exceptions. "Was it userId or user_id?" Every moment spent on this is a moment NOT spent building features.

The fix: Pick one convention and stick with it. The most common: snake_case in JSON (Stripe, GitHub), kebab-case in URLs (/order-items). Whatever you choose, document it and enforce it.

What people do: /users/1, /users/2, /users/3 β€” sequential auto-increment IDs from the database.

Why it's wrong: Attackers can iterate through all users. Competitors can estimate your growth ("they have 50,000 users"). Internal structure leaks into the public API.

The fix: Use UUIDs (/users/f47ac10b-58cc) or opaque identifiers. Stripe uses prefixed IDs like cus_abc123 β€” you can't guess the next one, and the prefix tells you it's a customer.

What people do: Return 400 Bad Request with no body, or a generic { "error": "Invalid request" }.

Why it's wrong: The developer debugging this has no idea WHAT was invalid. Was it a missing field? A wrong type? A value out of range? They end up guessing or reading source code.

The fix: Return a structured error body with a machine-readable code, a human-readable message, and β€” critically β€” which field caused the error:

{
  "error": {
    "code": "validation_error",
    "message": "Price must be a positive number",
    "field": "price",
    "value": -5.99
  }
}
Section 12

Interview Playbook β€” How REST Shows Up in Interviews

REST API design comes up in two contexts: dedicated API design rounds ("Design an API for X") and as part of system design rounds ("Here's the architecture β€” now design the API layer"). Here's how to handle both.

The 4-Step API Design Process for Interviews Step 1 Identify resources "What are the nouns?" users, posts, comments Step 2 Define endpoints CRUD for each resource + relationships Step 3 Add pagination filtering, sorting, error format Step 4 Trade-offs versioning, auth, rate limiting Say each step out loud. Interviewers love seeing a structured approach.

Beginner: Show You Know the Basics

  • Name resources as nouns β€” "I'd have /users, /posts, and /comments as my three main resources."
  • Use correct HTTP methods β€” "Creating a post is POST /posts. Reading one is GET /posts/{id}. Deleting is DELETE /posts/{id}."
  • Use correct status codes β€” "201 for creation, 404 when not found, 400 for validation errors."
  • Return the resource β€” "After creating, I return the full resource with its new ID so the client doesn't need a second call."

Intermediate: Show You Think About Edge Cases

  • Pagination β€” "For lists, I'd use cursor-based pagination with ?limit=20&after=cursor. Offset pagination has performance issues at depth."
  • Filtering β€” "Clients can filter with ?status=published&author=alice&sort=-created_at."
  • Error format β€” "Every error returns a JSON body with code, message, and field. Machine-readable and human-readable."
  • Versioning β€” "I'd version the API via URL path: /v1/posts. It's the simplest and most common approach."

Advanced: Show You've Built Production APIs

  • Idempotency β€” "For POST endpoints that create resources or trigger payments, I'd require an Idempotency-Key header. Retries are safe."
  • Rate limiting β€” "I'd return X-RateLimit-Remaining headers and 429 with Retry-After when exceeded."
  • HATEOAS β€” "Each response includes links to related resources and next actions. The client doesn't need to hard-code URLs."
  • Trade-offs β€” "For this particular use case, I'd consider GraphQL instead of REST because the clients have very different data needs β€” mobile wants minimal fields, web wants everything."
The biggest interview trap: Designing a beautiful REST API without asking what the client actually needs. Always start with: "Who are the consumers? Mobile app? Web? Third-party integrations?" The answer changes your design decisions β€” mobile needs sparse responses, third-party needs stability and versioning.
Section 13

Hands-On Challenges β€” Design APIs Yourself

The best way to learn REST API design is to practice it. These five challenges progress from beginner to expert. For each one, write down your endpoints before opening the solution. In interviews, you won't have answers to peek at β€” build that muscle now.

Challenge 1: Blog API Beginner

Design a REST API for a blog platform. Users can create posts, comment on posts, and like posts. List all the resources, endpoints, and the HTTP methods for each.

Time target: 5 minutes

Start by listing the nouns: posts, comments, likes, users. Then think about relationships β€” comments belong to posts, likes belong to posts. Should likes be a sub-resource of posts, or their own top-level resource?

# Posts
GET    /posts              # List all posts (paginated)
GET    /posts/{id}         # Get single post
POST   /posts              # Create a post
PATCH  /posts/{id}         # Update a post
DELETE /posts/{id}         # Delete a post

# Comments (nested under posts)
GET    /posts/{id}/comments       # List comments on a post
POST   /posts/{id}/comments       # Add a comment
DELETE /posts/{id}/comments/{cid} # Delete a comment

# Likes (sub-resource of posts)
POST   /posts/{id}/likes          # Like a post
DELETE /posts/{id}/likes          # Unlike a post (your own)
GET    /posts/{id}/likes          # List who liked it

# Users
GET    /users/{id}         # Get user profile
GET    /users/{id}/posts   # Get a user's posts

Key decisions: Likes are a sub-resource because they only make sense in context of a post. Comments are nested because they belong to a specific post. Users are top-level because they exist independently.

Challenge 2: E-Commerce API with Pagination Intermediate

Design the product search and cart API for an e-commerce site. Products have categories, prices, ratings, and stock status. The catalog has 2 million products. Include pagination, filtering, and sorting in your design.

Time target: 7 minutes

With 2M products, offset pagination will be slow for deep pages. Think about cursor-based pagination. For filtering, think about which fields users would commonly filter by. For the cart, should cart items be a sub-resource?

# Product catalog (cursor pagination for 2M products)
GET /products?category=electronics&min_price=10&max_price=500
    &in_stock=true&sort=-rating&limit=20&after=eyJpZCI6MTAwfQ

# Response includes:
# { "data": [...], "pagination": { "next_cursor": "...", "has_more": true } }

GET /products/{id}          # Single product with full details
GET /products/{id}/reviews  # Reviews for a product

# Cart
GET    /carts/me            # Current user's cart
POST   /carts/me/items      # Add item: { "product_id": 42, "quantity": 2 }
PATCH  /carts/me/items/{id} # Update quantity: { "quantity": 5 }
DELETE /carts/me/items/{id} # Remove item from cart

# Checkout
POST /orders                # Convert cart to order: { "cart_id": "...", "payment_method": "..." }

Key decisions: Cursor pagination because 2M products makes offset slow. /carts/me uses the authenticated user's ID (no need to pass it). Checkout is a POST to /orders (creating a new order), not a verb endpoint.

Challenge 3: Error Response Design Intermediate

Design a consistent error response format for a payment API. Cover these scenarios: invalid card number, expired card, insufficient funds, rate limit exceeded, internal server error, and authentication failure. Show the exact JSON response for each.

Time target: 5 minutes

Think about what a developer needs to programmatically handle each error. A machine-readable code is essential (you can't parse English messages). Also think about which HTTP status code matches each scenario β€” not everything is 400.

// 422 Unprocessable Entity β€” invalid card
{ "error": { "type": "card_error", "code": "invalid_card_number",
  "message": "The card number is not a valid credit card number.",
  "param": "card_number" } }

// 402 Payment Required β€” insufficient funds
{ "error": { "type": "card_error", "code": "insufficient_funds",
  "message": "The card has insufficient funds.",
  "decline_code": "insufficient_funds" } }

// 429 Too Many Requests β€” rate limited
{ "error": { "type": "rate_limit_error", "code": "rate_limit_exceeded",
  "message": "Too many requests. Retry after 60 seconds." } }
// + Retry-After: 60 header

// 401 Unauthorized β€” bad API key
{ "error": { "type": "authentication_error", "code": "invalid_api_key",
  "message": "The API key provided is invalid." } }

// 500 Internal Server Error β€” our fault
{ "error": { "type": "api_error", "code": "internal_error",
  "message": "An unexpected error occurred. Please retry.",
  "request_id": "req_abc123" } }

Pattern: Every error has the same shape: type (category), code (machine-readable), message (human-readable), and optional param/request_id. Clients switch on type and code, never parse the message string.

Challenge 4: API Versioning Strategy Advanced

Your API has 200 active clients. You need to rename user_name to username in all user-related responses, and also change the date format from "March 18, 2026" to "2026-03-18T00:00:00Z" (ISO 8601). Design the migration strategy. How do you roll this out without breaking anyone?

Time target: 8 minutes

Think about whether you need a full version bump (v1 β†’ v2) or if you can do this more gradually. Consider: can you return BOTH field names temporarily? How would you communicate the change? What's your timeline?

  1. Phase 1 β€” Dual-write (Week 1): Return BOTH user_name AND username in responses. Both have the same value. Old clients keep working; new clients can start using the new field. Same for dates β€” return both formats.
  2. Phase 2 β€” Deprecation notice (Week 2): Add a Deprecation: true header and a Sunset: 2026-05-18 header on responses that use the old field names. Email all 200 clients. Add a deprecation warning to docs.
  3. Phase 3 β€” Monitor usage (Weeks 3-8): Track which clients still use the old field name (check request bodies for user_name). Reach out to the holdouts directly.
  4. Phase 4 β€” Remove old fields (Week 9): Stop returning user_name. Old date format goes away. Monitor error rates for 24 hours.

Alternative: If the changes are too risky for gradual migration, bump to v2: /v2/users with new field names. Keep /v1/users alive for 6 months. This is cleaner but more work to maintain two versions.

Challenge 5: Design Stripe's Payment Intent API Expert

Design the API for a payment system that supports: creating payments, confirming payments, capturing authorized payments, refunding, and handling 3D Secure authentication redirects. Include idempotencyThe property that performing an operation multiple times has the same effect as performing it once. Critical for payments β€” you never want to charge a customer twice because of a network retry., proper status codes, and webhook notifications. This is Stripe's actual problem.

Time target: 12 minutes

A payment goes through stages: created β†’ requires_confirmation β†’ requires_action (3DS) β†’ processing β†’ succeeded/failed. Think of the payment as a resource with a state machine. Actions like "confirm" and "capture" are sub-resources or action endpoints. Webhooks handle async status changes.

# Create a payment intent (requires Idempotency-Key)
POST /v1/payment-intents
Idempotency-Key: pi_unique_123
{ "amount": 2000, "currency": "usd", "payment_method": "pm_card_visa" }
β†’ 201 Created { "id": "pi_abc", "status": "requires_confirmation" }

# Confirm the payment (client-side SDK typically does this)
POST /v1/payment-intents/pi_abc/confirm
β†’ 200 { "status": "requires_action", "next_action": {
    "type": "redirect_to_url",
    "redirect_url": "https://bank.com/3ds?token=xyz" } }

# After 3DS, the status updates asynchronously via webhook:
# POST https://yourapp.com/webhooks/stripe
# { "type": "payment_intent.succeeded", "data": { "id": "pi_abc" } }

# Capture an authorized payment (for auth-then-capture flow)
POST /v1/payment-intents/pi_abc/capture
β†’ 200 { "status": "succeeded", "amount_captured": 2000 }

# Refund (full or partial)
POST /v1/refunds
{ "payment_intent": "pi_abc", "amount": 500 }
β†’ 201 { "id": "re_xyz", "status": "pending", "amount": 500 }

# List refunds for a payment
GET /v1/payment-intents/pi_abc/refunds

Key decisions: /confirm and /capture are action endpoints (verbs) β€” this is where pure REST breaks down and pragmatism wins. Webhooks handle async state transitions. Every mutating call accepts an idempotency key. Refunds are their own resource because they have lifecycle and metadata.

Section 14

Quick Reference β€” REST API Cheat Sheet

Everything from this page on one screen. Pin this for your next API design session or interview prep.

HTTP Methods
GET    β†’ Read (safe, idempotent)
POST   β†’ Create (NOT idempotent)
PUT    β†’ Replace (idempotent)
PATCH  β†’ Partial update
DELETE β†’ Remove (idempotent)

Safe = doesn't change data
Idempotent = same result if repeated
Status Codes
200 OK           β†’ Success
201 Created      β†’ POST success
204 No Content   β†’ DELETE success
400 Bad Request  β†’ Invalid input
401 Unauthorized β†’ Bad/missing auth
403 Forbidden    β†’ No permission
404 Not Found    β†’ Bad URL / no data
409 Conflict     β†’ Duplicate/state err
422 Unprocessable→ Validation failed
429 Too Many Req β†’ Rate limited
500 Server Error β†’ Our fault
URL Conventions
Nouns, not verbs:
  /users NOT /getUsers

Plurals:
  /books NOT /book

Lowercase + hyphens:
  /order-items NOT /orderItems

Nesting for relationships:
  /users/42/orders

Max 3 levels deep:
  /users/42/orders/99/items ← OK
  /.../items/3/details ← too deep
Pagination
Offset (simple, slow at depth):
  ?page=3&limit=20

Cursor (fast, no jump-to-page):
  ?limit=20&after=eyJpZCI6MTAwfQ

Response includes:
  "next_cursor": "..."
  "has_more": true

Rule: cursor for feeds/search,
offset for admin dashboards
Versioning
URL path (most common):
  /v1/books  β†’  /v2/books

Header:
  API-Version: 2

Query param:
  /books?version=2

Best practice:
  URL path (Stripe, GitHub, Twilio)
  Support old version for 6-12 months
  Deprecation headers + sunset date
Error Format
{
  "error": {
    "type": "validation_error",
    "code": "invalid_email",
    "message": "Email is invalid",
    "field": "email"
  }
}

Always include:
  - Machine-readable code
  - Human-readable message
  - Which field caused it
  - Request ID for debugging
Section 15

Connected Topics β€” Where to Go Next

REST API design connects to almost every other system design topic. Here's how what you learned feeds into the rest of your learning journey.

REST API DESIGN Resources, Methods, Status Codes Authentication "Who can call your API?" Rate Limiting "Protect your API from abuse" Load Balancers "Distribute API traffic" Caching "Cache GET responses" Webhooks "Push events to clients" API Gateway "Single entry point"