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=pythonuses a different structure than the CRUD endpoints - 📎 Some endpoints use
PATCHwherePUTwould 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
_limitand_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
userIdwithuser_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
- GitHub REST API Documentation — Official documentation with interactive examples
- JSONPlaceholder Guide — Usage guide with every endpoint and operation
- OpenWeather API Docs — API documentation with every available endpoint
- REST API Design Best Practices (Stack Overflow Blog) — Industry conventions with real examples
- Microsoft REST API Guidelines — Detailed corporate guide for API design
- API Design Patterns (Manning) — Reference book on API design patterns