Module 2: REST Principles

Real-world APIs: analyzing REST design

Capsule overview

You already know the REST principles: resources as nouns, predictable URIs, HTTP methods mapped to CRUD, idempotency, statelessness, versioning, pagination. You have the theory. Now you need to see how it's applied (and how it's violated) in the real world. Because the reality of public APIs is messier than any tutorial suggests — there are APIs that follow REST elegantly, APIs that follow it partially, and APIs that basically ignore REST and work anyway.

Studying real APIs gives you something theory can't: judgment. When you analyze the GitHub API, you'll see high-quality REST design. When you analyze JSONPlaceholder, you'll see REST simplified for learning. When you analyze OpenWeather, you'll see a commercially successful API that violates fundamental REST principles. Understanding why each one chose its approach is more valuable than memorizing rules.

This capsule turns you from someone who knows the REST principles into someone who can evaluate any API, identify what it does well, what it does badly, and why it made those decisions. That's exactly what you need for the next capsule's project, where you'll design your own API from scratch.


Setup

pip install requests
import requests
import json

For GitHub, you can use the API without authentication (60 requests/hour) or with a personal token (5,000/hour). For OpenWeather you need a free API key — sign up at openweathermap.org.


API 1: the GitHub REST API — REST done right

The GitHub API is one of the REST APIs most often cited as an example of good design. Let's analyze it piece by piece.

Resources and URI patterns

GitHub models its domain with clear resources. The URIs follow a hierarchy that reflects natural ownership:

Base resources:
  /users/{username}
  /repos/{owner}/{repo}
  /orgs/{org}

Nested resources (ownership):
  /repos/{owner}/{repo}/issues
  /repos/{owner}/{repo}/issues/{number}
  /repos/{owner}/{repo}/pulls
  /users/{username}/repos

Deep resources (up to 3 levels):
  /repos/{owner}/{repo}/issues/{number}/comments
  /repos/{owner}/{repo}/pulls/{number}/reviews

Plural nouns, lowercase, IDs in the path, a hierarchy that expresses ownership. The URI communicates it without documentation.

base = "https://api.github.com"

r = requests.get(f"{base}/users/octocat")
print(f"GET /users/octocat → {r.status_code}")
user = r.json()
print(f"  Login: {user['login']}")
print(f"  Repos URL: {user['repos_url']}")

r = requests.get(f"{base}/users/octocat/repos", params={"per_page": 3})
print(f"\nGET /users/octocat/repos → {r.status_code}")
for repo in r.json()[:3]:
    print(f"  - {repo['name']} ({repo['language']})")

r = requests.get(f"{base}/repos/octocat/Hello-World/issues", params={"per_page": 3, "state": "all"})
print(f"\nGET /repos/octocat/Hello-World/issues → {r.status_code}")
for issue in r.json()[:3]:
    print(f"  #{issue['number']}: {issue['title'][:50]}")

Versioning: header-based

GitHub versions by headers, not by URL. The version isn't part of the resource, it's part of the representation:

headers = {"Accept": "application/vnd.github.v3+json"}

r = requests.get(f"{base}/users/octocat", headers=headers)
print(f"Content-Type: {r.headers.get('Content-Type')}")
print(f"X-GitHub-Media-Type: {r.headers.get('X-GitHub-Media-Type')}")

The header Accept: application/vnd.github.v3+json tells GitHub: "I want version 3 of your API, in JSON format." When they migrate to v4 (GraphQL already exists as an alternative), clients that specify v3 will keep working.

Advantage: the URL /users/octocat is always the same, regardless of the version.

Disadvantage: it's less visible — a developer who sees the URL doesn't know which version they're using without inspecting the headers.

Pagination: Link headers

GitHub implements pagination with query parameters (per_page, page) and responds with the Link header containing navigation URLs:

r = requests.get(
    f"{base}/repos/octocat/Hello-World/issues",
    params={"per_page": 2, "page": 1, "state": "all"}
)
print(f"Status: {r.status_code}")
print(f"Items on this page: {len(r.json())}")

link_header = r.headers.get("Link", "No pagination")
print(f"\nLink header:\n{link_header}")

Expected output:

Status: 200
Items on this page: 2

Link header:
<https://api.github.com/repositories/.../issues?per_page=2&page=2&state=all>; rel="next",
<https://api.github.com/repositories/.../issues?per_page=2&page=500&state=all>; rel="last"

The Link header with rel="next" and rel="last" follows the RFC 8288 standard. You don't need to build pagination URLs by hand — the server hands them to you. This is HATEOAS in practice.

Authentication and rate limiting

Without authentication: 60 requests/hour. With a Bearer token: 5,000. GitHub tells you your status in every response:

r = requests.get(f"{base}/rate_limit")
limits = r.json()["rate"]
print(f"Limit:     {limits['limit']} requests/hour")
print(f"Remaining: {limits['remaining']}")
print(f"Reset:     {limits['reset']} (Unix timestamp)")

The headers X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset come with every response. Total transparency.

Status codes

200 OK            → Resource retrieved correctly
201 Created       → Resource created (successful POST)
204 No Content    → Deleted successfully (DELETE)
304 Not Modified  → Cache is still valid (ETag/If-None-Match)
401 Unauthorized  → No authentication or an invalid token
403 Forbidden     → Rate limit exceeded or no permissions
404 Not Found     → The resource doesn't exist
422 Unprocessable → Validation failed (invalid fields)

GitHub uses 422 for validation errors, not 400. It's more specific: 400 is "your request doesn't make sense syntactically", 422 is "your request is valid but the data doesn't pass validation."

r = requests.get(f"{base}/users/this-user-does-not-exist-xyz-999")
print(f"Nonexistent user: {r.status_code}")  # 404
print(f"  Message: {r.json()['message']}")

r = requests.get(f"{base}/users/octocat")
print(f"\nExisting user: {r.status_code}")  # 200

What it does well and what isn't standard

What it does well:

  • 🏗️ Hierarchical URIs that reflect real ownership
  • 📋 Precise status codes (422 for validation, 403 for rate limit)
  • 📄 Pagination with Link headers (the RFC 8288 standard)
  • 🔄 Header versioning without polluting the URL
  • 📊 Transparent rate limit in every response
  • 🔗 Partial HATEOAS: every resource includes URLs to related resources

What isn't standard REST:

  • 🔍 /search/repositories?q=python uses a different structure than the CRUD endpoints
  • 📎 Some endpoints use PATCH where PUT would be more appropriate
  • 📦 Search responses ({"total_count": N, "items": [...]}) differ from the normal collection format (a direct array)

API 2: JSONPlaceholder — REST simplified for learning

JSONPlaceholder is a fake API designed for prototypes and learning. Free, no authentication, clean REST structure. Its limitation: write operations don't persist.

Resources and structure

/posts      → 100 posts       /albums   → 100 albums
/comments   → 500 comments     /photos   → 5000 photos
/todos      → 200 todos       /users    → 10 users
base = "https://jsonplaceholder.typicode.com"

r = requests.get(f"{base}/posts", params={"_limit": 3})
print(f"GET /posts → {r.status_code}")
for post in r.json():
    print(f"  ID {post['id']}: {post['title'][:40]}...")

r = requests.get(f"{base}/posts/1")
print(f"\nGET /posts/1 → {r.status_code}")
post = r.json()
print(f"  Title: {post['title']}")
print(f"  UserId: {post['userId']}")

Nested resources and filtering

JSONPlaceholder supports nested resources and filtering — two ways to reach the same data:

r = requests.get(f"{base}/posts/1/comments")
print(f"GET /posts/1/comments → {r.status_code} ({len(r.json())} comments)")

r = requests.get(f"{base}/users/1/posts")
print(f"GET /users/1/posts → {r.status_code} ({len(r.json())} posts)")

# An equivalent alternative with a query parameter
r = requests.get(f"{base}/posts", params={"userId": 1})
print(f"GET /posts?userId=1 → {r.status_code} ({len(r.json())} posts)")

Write operations (fake)

POST, PUT, PATCH and DELETE return correct responses but they don't persist data:

new_post = {"title": "My test post", "body": "Content", "userId": 1}
r = requests.post(f"{base}/posts", json=new_post)
print(f"POST /posts → {r.status_code}")  # 201 Created
print(f"  Assigned ID: {r.json()['id']}")

r = requests.patch(f"{base}/posts/1", json={"title": "I'm only changing the title"})
print(f"PATCH /posts/1 → {r.status_code}")  # 200 OK
print(f"  Title: {r.json()['title']}")

r = requests.delete(f"{base}/posts/1")
print(f"DELETE /posts/1 → {r.status_code}")  # 200 OK

# But the data does NOT persist:
r = requests.get(f"{base}/posts/101")
print(f"GET /posts/101 → {r.json()}")  # {} — it was never really created

POST returns 201, PUT/PATCH return 200, DELETE returns 200. Correct status codes, but it's REST theater — the responses imitate what a real API would do.

What JSONPlaceholder teaches

What it does well:

  • 🏗️ A clean, predictable REST structure
  • 📋 Correct status codes for every operation
  • 🔄 It supports every HTTP method
  • 🔗 Nested resources and filtering by query parameter
  • 🎓 Zero friction: no auth or setup required

Limitations:

  • ⚠️ Writes don't persist
  • ⚠️ Pagination with _limit and _start (non-standard)
  • ⚠️ No rate limiting, no versioning
  • ⚠️ Static responses

API 3: the OpenWeather API — REST in the real world (impure)

OpenWeather is a commercial API that millions of developers use. It's successful, functional, and it violates several REST principles. Analyzing it teaches you that REST is a spectrum, not a binary.

The query-param-heavy structure

GitHub (RESTful):
  GET /repos/octocat/Hello-World
  → The resource lives in the path

OpenWeather (query-param heavy):
  GET /data/2.5/weather?q=London&appid=YOUR_KEY
  → The resource lives in the query parameters

In pure REST, the URI identifies the resource. /weather/London would be more RESTful than /data/2.5/weather?q=London. OpenWeather chose query parameters because it lets you search by name, coordinates, ZIP code, or city ID — flexibility that doesn't fit easily into a path.

API_KEY = "YOUR_API_KEY"  # Replace it with your key from openweathermap.org

url = "https://api.openweathermap.org/data/2.5/weather"
params = {"q": "Mexico City", "appid": API_KEY, "units": "metric", "lang": "en"}
r = requests.get(url, params=params)

if r.status_code == 200:
    data = r.json()
    print(f"City: {data['name']}")
    print(f"Temperature: {data['main']['temp']}°C")
    print(f"Description: {data['weather'][0]['description']}")
elif r.status_code == 401:
    print("Invalid or missing API key")

Analyzing the response

The response structure is functional but it doesn't follow REST conventions:

example_response = {
    "coord": {"lon": -99.1269, "lat": 19.4285},
    "weather": [{"id": 802, "main": "Clouds", "description": "scattered clouds"}],
    "main": {"temp": 22.5, "humidity": 45, "pressure": 1018},
    "wind": {"speed": 3.6, "deg": 350},
    "dt": 1700000000,
    "sys": {"country": "MX", "sunrise": 1699963200, "sunset": 1700004600},
    "name": "Mexico City",
    "cod": 200
}
Problem                           REST alternative
────────────────────────────────  ─────────────────────────────────
"cod": 200 in the body            Redundant — you already have the HTTP status code
"dt" for the timestamp            "timestamp" or "observed_at" would be clear
"weather" is a list               Unnecessary for a single result
Cryptic fields ("base", "sys")    Descriptive names improve usability
Everything in one flat object     Separate the resource from the metadata

The API key in a query parameter

OpenWeather puts the API key in the URL (appid=YOUR_KEY). That's a security problem:

OpenWeather (key in the URL — not recommended):
  GET /data/2.5/weather?q=London&appid=abc123
  → The key is visible in logs, browser history, proxies, shared URLs

GitHub (key in the header — recommended):
  GET /users/octocat
  Authorization: Bearer ghp_abc123
  → The key lives only in the headers, never visible in the URL

Multiple ways to identify the same resource

lookup_methods = [
    {"desc": "By name",        "params": "q=London"},
    {"desc": "By coordinates", "params": "lat=51.5&lon=-0.12"},
    {"desc": "By ZIP code",    "params": "zip=10001,us"},
    {"desc": "By city ID",     "params": "id=2643743"},
]
print("Ways to look up the weather for a place:")
for m in lookup_methods:
    print(f"  {m['desc']:20s} → /data/2.5/weather?{m['params']}")

In pure REST, a resource has one canonical URI. OpenWeather treats the endpoint as a function that takes parameters, not as a resource with an identity. It's an RPC approach dressed up as REST.

What OpenWeather teaches

What works (despite not being pure REST):

  • 💰 A commercially successful API with millions of users
  • 📊 Clear documentation of every parameter
  • 🌍 Query flexibility (name, coordinates, ZIP)

What it violates from REST:

  • ❌ Resources identified by query params, not by URI
  • ❌ API key in the URL instead of headers
  • ❌ Status code duplicated in the body ("cod": 200)
  • ❌ Cryptic naming (dt, sys, cod)
  • ❌ Non-uniform structure across endpoints
  • ❌ Versioning in the path (/data/2.5/) with an unusual format

Key lesson: an API doesn't need to be RESTful to be successful. But a RESTful API is easier to learn, use and maintain. OpenWeather works despite its design, not because of it.


Comparison: three APIs, three approaches

Criterion             │ GitHub             │ JSONPlaceholder     │ OpenWeather
──────────────────────┼────────────────────┼─────────────────────┼────────────────────
Resources in the URI  │ ✅ /repos/{o}/{r}  │ ✅ /posts/{id}      │ ❌ Query params
Plural nouns          │ ✅ repos, issues   │ ✅ posts, users     │ ⚠️ /weather (sing.)
URI hierarchy         │ ✅ /repos/.../iss  │ ✅ /posts/1/comments│ ❌ Flat
HTTP methods          │ ✅ All of them     │ ✅ All of them      │ ⚠️ GET only
Status codes          │ ✅ Precise (422)   │ ✅ Correct          │ ⚠️ Redundant in body
Versioning            │ ✅ Header-based    │ ❌ None             │ ⚠️ URL (/data/2.5/)
Pagination            │ ✅ Link headers    │ ⚠️ _limit/_start   │ ❌ Not applicable
Authentication        │ ✅ Bearer token    │ ❌ Not required     │ ⚠️ Key in the URL
Rate limiting         │ ✅ Clear headers   │ ❌ None             │ ✅ Documented
HATEOAS               │ ⚠️ Partial (URLs) │ ❌ None             │ ❌ None
Consistent naming     │ ✅ Yes             │ ✅ Yes              │ ❌ Cryptic
Uniform response      │ ⚠️ Almost (search≠)│ ✅ Yes              │ ❌ Not uniform
api_scores = {
    "GitHub": {"uri": True, "nouns": True, "hierarchy": True, "methods": True,
               "status": True, "versioning": True, "pagination": True,
               "auth": True, "rate_limit": True, "naming": True, "uniform": False},
    "JSONPlaceholder": {"uri": True, "nouns": True, "hierarchy": True, "methods": True,
               "status": True, "versioning": False, "pagination": False,
               "auth": True, "rate_limit": False, "naming": True, "uniform": True},
    "OpenWeather": {"uri": False, "nouns": False, "hierarchy": False, "methods": False,
               "status": False, "versioning": False, "pagination": False,
               "auth": False, "rate_limit": True, "naming": False, "uniform": False},
}

print("REST Compliance Score:")
print("=" * 45)
for api, criteria in api_scores.items():
    score = sum(criteria.values())
    total = len(criteria)
    bar = "█" * score + "░" * (total - score)
    print(f"  {api:20s} {bar} {score}/{total}")

Expected output:

REST Compliance Score:
=============================================
  GitHub               █████████░░ 9/11
  JSONPlaceholder      ███████░░░░ 7/11
  OpenWeather          █░░░░░░░░░░ 1/11

A checklist for evaluating an API

When you come across a new API, use this checklist:

Structure and URIs

  • Are the resources identified in the URI (not just in query params)?
  • Are the names plural nouns in lowercase?
  • Does the hierarchy reflect real ownership (/users/1/posts)?
  • Are the URIs predictable without documentation?

Methods and status codes

  • Does it use GET to read, POST to create, PUT/PATCH to update, DELETE to remove?
  • Does it return correct status codes (201 for creation, 404 for not found)?
  • Do the errors include descriptive messages?
  • Does it distinguish between client errors (4xx) and server errors (5xx)?

Pagination and filtering

  • Do large collections have pagination?
  • Does it support filtering by query parameters?
  • Is the pagination consistent across endpoints?

Authentication and security

  • Does the API key/token go in the headers (not in the URL)?
  • Does it report rate limits in the responses?
  • Does it use HTTPS?

Consistency

  • Do the responses have the same structure across every endpoint?
  • Is the naming consistent (does it avoid mixing userId with user_id)?
  • Does the documentation match the real behavior?

Connection to your project

In the next capsule you're going to design a REST API Design Doc — a complete document specifying a REST API from scratch. Everything you saw here prepares you directly:

  • From GitHub you took patterns for hierarchical URIs, header versioning, and pagination with Link headers
  • From JSONPlaceholder you understood the canonical REST structure — simple, clean, predictable
  • From OpenWeather you learned what not to do: API keys in URLs, cryptic names, inconsistency

Your Design Doc will include: resources, URIs, methods, expected status codes, pagination, and error handling. Use this capsule's checklist to validate your design before you hand it in.


Troubleshooting

"I get a 403 Forbidden on GitHub"

GitHub returns 403 when you exceed the rate limit. Without authentication you get 60 requests/hour. Solution: wait an hour or create a Personal Access Token at github.com/settings/tokens and use it in the Authorization: Bearer your_token header.

"JSONPlaceholder still returns data even though I did a DELETE"

Expected behavior. JSONPlaceholder simulates responses but doesn't persist changes. It isn't a bug — it's a fake API for practicing requests.

"OpenWeather returns cod: 401"

The API key is invalid, expired, or you didn't include it. Check that your key is active in your OpenWeather dashboard. New keys can take up to 2 hours to activate.

"GitHub's and OpenWeather's responses have very different structures"

Correct — and that's the lesson. There's no universal standard for REST response structure. Your job as a developer is to adapt to each API, and when you design your own, to pick a consistent structure and document it.


Exercises

Exercise 1: GitHub API explorer

Write a script that explores the GitHub API for a given user: get their profile, list their repos (paginated 5 at a time), and for each repo print the count of open issues.

See solution
import requests

def explore_github_user(username):
    base = "https://api.github.com"
    r = requests.get(f"{base}/users/{username}")
    if r.status_code != 200:
        print(f"Error: user '{username}' not found ({r.status_code})")
        return

    user = r.json()
    print(f"PROFILE: {user['login']}")
    print(f"  Name: {user.get('name', 'N/A')} | Repos: {user['public_repos']} | Followers: {user['followers']}")

    r = requests.get(f"{base}/users/{username}/repos", params={"per_page": 5, "sort": "updated"})
    print(f"\nREPOS (last 5 updated):")
    for repo in r.json():
        lang = repo.get("language") or "N/A"
        print(f"  📁 {repo['name']}{lang} | ⭐ {repo['stargazers_count']} | Issues: {repo['open_issues_count']}")

explore_github_user("octocat")

Exercise 2: Full CRUD with JSONPlaceholder

Write a script that runs the 5 CRUD operations on JSONPlaceholder (/posts): list, get one, create, partially update, and delete. For each operation, print the HTTP method, the status code, and verify it's the expected one.

See solution
import requests

base = "https://jsonplaceholder.typicode.com"
ops = [
    ("GET",    f"{base}/posts",   {"params": {"_limit": 2}},                            200, "LIST"),
    ("GET",    f"{base}/posts/1", {},                                                    200, "READ"),
    ("POST",   f"{base}/posts",   {"json": {"title": "New", "body": "X", "userId": 1}}, 201, "CREATE"),
    ("PATCH",  f"{base}/posts/1", {"json": {"title": "Updated"}},                        200, "UPDATE"),
    ("DELETE", f"{base}/posts/1", {},                                                    200, "DELETE"),
]

print("Full CRUD — JSONPlaceholder /posts")
print("=" * 50)
for method, url, kwargs, expected, name in ops:
    r = requests.request(method, url, timeout=10, **kwargs)
    match = "✅" if r.status_code == expected else "❌"
    print(f"  {match} {name:8s} {method:6s}{r.status_code} (expected: {expected})")

Exercise 3: Response structure comparator

Write a script that does a GET to GitHub's /users/octocat and JSONPlaceholder's /users/1, and compares: how many keys each response has, how many URLs it includes, and which naming convention it uses (snake_case vs camelCase).

See solution
import requests

def analyze(name, url):
    data = requests.get(url, timeout=10).json()
    urls = sum(1 for v in data.values() if isinstance(v, str) and v.startswith("http"))
    snake = sum(1 for k in data if "_" in k)
    return {"name": name, "keys": len(data), "urls": urls, "snake_case": snake}

gh = analyze("GitHub /users/octocat", "https://api.github.com/users/octocat")
jp = analyze("JSONPlaceholder /users/1", "https://jsonplaceholder.typicode.com/users/1")

for api in [gh, jp]:
    print(f"{api['name']}:")
    print(f"  Keys: {api['keys']} | Embedded URLs: {api['urls']} | snake_case fields: {api['snake_case']}")

print(f"\nGitHub embeds {gh['urls']} URLs (partial HATEOAS) vs JSONPlaceholder's {jp['urls']}")

Exercise 4: REST violation detector

Write a function detect_violations(url, response) that takes a URL and a requests response, and returns a list of violations: verbs in the URL, credentials in query params, a status code duplicated in the body.

See solution
import requests
from urllib.parse import urlparse, parse_qs

def detect_violations(url, response):
    violations = []
    parsed = urlparse(url)
    path_parts = [p for p in parsed.path.lower().split("/") if p]

    for verb in ["get", "fetch", "create", "delete", "update", "remove"]:
        if verb in path_parts:
            violations.append(f"Verb in the URL: '{verb}'")

    for key in parse_qs(parsed.query):
        if key.lower() in ["key", "apikey", "api_key", "appid", "token", "secret"]:
            violations.append(f"Credential in the URL: '{key}' should go in the headers")

    if response.status_code == 200:
        try:
            body = response.json()
            if isinstance(body, dict):
                for field in ["cod", "status_code", "statusCode"]:
                    if field in body:
                        violations.append(f"Status code duplicated in the body: '{field}'")
        except ValueError:
            pass

    if parsed.scheme != "https":
        violations.append("It doesn't use HTTPS")

    return violations

# Try it
urls = [
    "https://api.github.com/users/octocat",
    "https://jsonplaceholder.typicode.com/posts/1",
    "https://api.openweathermap.org/data/2.5/weather?q=London&appid=fake_key",
    "http://example.com/api/getUsers",
]

for url in urls:
    try:
        r = requests.get(url, timeout=10)
    except requests.RequestException:
        class FakeResp:
            status_code = 200
            def json(self): return {}
        r = FakeResp()

    viols = detect_violations(url, r)
    path = urlparse(url).path
    if viols:
        print(f"\n  {path}")
        for v in viols:
            print(f"    ⚠️  {v}")
    else:
        print(f"\n  {path}\n    ✅ No violations")

Exercise 5: Comparative documentation of new APIs

Pick two APIs from this list: PokeAPI, Dog CEO API, REST Countries, Open Library API. Make requests to 3 endpoints of each and generate a report that includes: URI patterns, status codes, response structure, naming convention, and a REST compliance score using the checklist criteria.

See solution
import requests

def analyze_api(name, base_url, endpoints):
    report = {"name": name, "endpoints": []}
    for path in endpoints:
        try:
            r = requests.get(f"{base_url}{path}", timeout=10)
            data = r.json() if r.status_code == 200 else None
            keys = list(data.keys())[:6] if isinstance(data, dict) else f"[list: {len(data)} items]" if isinstance(data, list) else "N/A"
            report["endpoints"].append({"path": path, "status": r.status_code, "keys": keys})
        except Exception as e:
            report["endpoints"].append({"path": path, "error": str(e)[:40]})
    return report

poke = analyze_api("PokeAPI", "https://pokeapi.co/api/v2", ["/pokemon/pikachu", "/pokemon?limit=3", "/type/electric"])
countries = analyze_api("REST Countries", "https://restcountries.com/v3.1", ["/name/mexico", "/alpha/MX", "/region/americas"])

for api in [poke, countries]:
    print(f"\n{'=' * 50}")
    print(f"API: {api['name']}")
    for ep in api["endpoints"]:
        if "error" in ep:
            print(f"  ❌ {ep['path']}: {ep['error']}")
        else:
            print(f"  {ep['status']} {ep['path']}")
            print(f"       Keys: {ep['keys']}")

Note: the value of this exercise is in the analysis, not in the "correct" answer. Document which REST patterns you identify and which design decisions each API would make differently.


Summary

  • The GitHub API is the benchmark for REST done right: hierarchical URIs, header versioning, pagination with Link headers, precise status codes, transparent rate limits
  • JSONPlaceholder is clean but simplified REST: ideal for learning, it doesn't persist data and has no real versioning or pagination
  • OpenWeather shows that commercially successful APIs can violate REST principles: query params for resources, API key in the URL, cryptic naming
  • REST is a spectrum, not a binary — no real API complies 100% with every principle
  • The evaluation checklist lets you analyze any new API systematically
  • Judgment > dogma — knowing what an API violates is more valuable than rejecting it for not being "pure"
  • Adapting to imperfect APIs is a key professional skill

Next capsule: Project: REST API Design Doc — you're going to design your own REST API from scratch, applying everything you learned in this module. You'll use this capsule's checklist to validate your design.


Additional resources

  1. GitHub REST API Documentation — Official documentation with interactive examples
  2. JSONPlaceholder Guide — Usage guide with every endpoint and operation
  3. OpenWeather API Docs — API documentation with every available endpoint
  4. REST API Design Best Practices (Stack Overflow Blog) — Industry conventions with real examples
  5. Microsoft REST API Guidelines — Detailed corporate guide for API design
  6. API Design Patterns (Manning) — Reference book on API design patterns