Chapter 10: Testing & Documentation
"An API without tests is a promise you can't keep.
An API without documentation is a promise no one can find."
1. Physical Reality First
You have designed and built a REST API. Before you ship it, two questions must be answered:
- Does it work? — Testing
- Does anyone know how to use it? — Documentation
These are not afterthoughts. They are the difference between code that runs in production and a product that others can build on.
The cost of bad testing: a regression breaks a client's integration at 2am. You find out from an angry Slack message, not your own monitoring.
The cost of bad documentation: developers spend hours guessing how your API works. They build incorrect integrations. They file support tickets. They give up and use a competitor.
Good testing means: you know immediately when something breaks. Good documentation means: developers can integrate without asking you anything.
This chapter closes the loop on everything we've built. Testing verifies the correctness of every design decision from Chapters 1–9. Documentation communicates those decisions to the world.
2. The Testing Pyramid for APIs
┌──────────┐
│ E2E │ ← Few, slow, expensive
│ Tests │ Test full user journeys
┌┴──────────┴┐
│ Integration │ ← Medium, test real DB + HTTP
│ Tests │
┌┴────────────┴┐
│ Contract │ ← OpenAPI schema validation
│ Tests │
┌┴──────────────┴┐
│ Unit Tests │ ← Many, fast, test logic in isolation
└────────────────┘
Each layer tests different things. Don't rely on only one. An API covered only by unit tests can have every unit pass while the integrated system is completely broken.
3. Unit Testing — Logic in Isolation
Unit tests test individual functions without the network, database, or external services. They run in milliseconds and should be your largest test layer.
What to unit test in an API:
- Validation logic: does
validate_email("not-an-email")raise the right error? - Business logic: does
calculate_order_total([...])apply discounts correctly? - Authorization logic: does
can_user_access_order(user, order)return the right boolean? - Serialization: does
OrderSchema.from_db(order)produce the right JSON shape? - Error mapping: does a DB constraint violation map to a 409 response?
# Example: testing validation logic in isolation
def test_create_order_validates_quantity():
with pytest.raises(ValidationError) as exc_info:
CreateOrderRequest(
items=[{"product_id": 1, "quantity": 0}] # quantity must be >= 1
)
errors = exc_info.value.errors()
assert any(e["loc"] == ("items", 0, "quantity") for e in errors)
assert any(e["type"] == "greater_than_equal" for e in errors)
def test_order_total_applies_bulk_discount():
items = [{"price": 1000, "quantity": 10}]
total = calculate_order_total(items, user_tier="gold")
assert total == 9000 # 10% discount for gold tier
def test_cannot_access_other_users_order():
user = User(id=42, role="user")
order = Order(id=99, user_id=77) # belongs to different user
assert can_user_access_order(user, order) is False
def test_admin_can_access_any_order():
user = User(id=42, role="admin")
order = Order(id=99, user_id=77)
assert can_user_access_order(user, order) is True
Mocking in unit tests:
When your function calls a database or external service, mock it:
from unittest.mock import patch, MagicMock
def test_send_order_confirmation_calls_email_service():
with patch("app.services.email.send") as mock_send:
confirm_order(order_id=99, user_email="t@example.com")
mock_send.assert_called_once_with(
to="t@example.com",
template="order_confirmation",
data={"order_id": 99}
)
The test verifies behavior (email service was called correctly) without actually sending email.
4. Integration Testing — The Real HTTP Layer
Integration tests spin up your actual application with a real (test) database and send real HTTP requests through the full stack. They test that all the layers work together: HTTP parsing → middleware → auth → route handler → DB → response serialization.
# FastAPI / pytest integration test
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.database import get_db, Base, engine
@pytest.fixture(scope="function")
def client(db):
"""Test client with a clean DB per test"""
yield TestClient(app)
@pytest.fixture(scope="function")
def db():
"""Create tables, yield, drop tables"""
Base.metadata.create_all(bind=engine)
yield
Base.metadata.drop_all(bind=engine)
@pytest.fixture
def auth_headers(client):
"""Create a user and return auth headers"""
client.post("/users", json={"email": "t@example.com", "password": "Password123!"})
response = client.post("/auth/login", json={"email": "t@example.com", "password": "Password123!"})
token = response.json()["access_token"]
return {"Authorization": f"Bearer {token}"}
class TestCreateOrder:
def test_create_order_success(self, client, auth_headers):
response = client.post(
"/orders",
json={"items": [{"product_id": 1, "quantity": 2}]},
headers=auth_headers
)
assert response.status_code == 201
data = response.json()
assert data["status"] == "pending"
assert len(data["items"]) == 1
assert "id" in data
assert "created_at" in data
def test_create_order_requires_auth(self, client):
response = client.post(
"/orders",
json={"items": [{"product_id": 1, "quantity": 2}]}
)
assert response.status_code == 401
def test_create_order_validates_quantity(self, client, auth_headers):
response = client.post(
"/orders",
json={"items": [{"product_id": 1, "quantity": 0}]},
headers=auth_headers
)
assert response.status_code == 422
errors = response.json()["errors"]
assert any(e["field"] == "items[0].quantity" for e in errors)
def test_cannot_read_other_users_order(self, client, auth_headers, other_user_order):
response = client.get(f"/orders/{other_user_order.id}", headers=auth_headers)
assert response.status_code == 404 # 404 not 403 — don't reveal existence
What integration tests catch that unit tests miss:
- Middleware applying in wrong order (auth before rate limiting, or reverse)
- SQL queries that are syntactically correct but return wrong data
- Missing DB indexes causing correct-but-slow queries
- Response serialization including fields it shouldn't (password hash in user response)
- CORS headers missing on actual responses
- Content-Type header wrong or missing
Use a test database, never production. Either a separate DB instance, or SQLite in-memory for fast tests.
5. Contract Testing — OpenAPI as Truth
A contract is the formal agreement between your API and its clients. OpenAPI (formerly Swagger) is the standard contract format for REST APIs.
5.1 What OpenAPI Is
OpenAPI is a YAML/JSON schema that describes your entire API:
# openapi.yaml
openapi: "3.1.0"
info:
title: "Orders API"
version: "1.0.0"
paths:
/orders:
post:
summary: "Create an order"
operationId: "createOrder"
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateOrderRequest"
responses:
"201":
description: "Order created"
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"422":
$ref: "#/components/responses/ValidationError"
/orders/{orderId}:
get:
summary: "Get an order"
parameters:
- name: orderId
in: path
required: true
schema:
type: integer
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"404":
$ref: "#/components/responses/NotFound"
components:
schemas:
Order:
type: object
required: [id, status, items, created_at]
properties:
id:
type: integer
status:
type: string
enum: [pending, processing, shipped, delivered, cancelled]
items:
type: array
items:
$ref: "#/components/schemas/OrderItem"
total:
type: integer
description: "Total in paise (smallest currency unit)"
created_at:
type: string
format: date-time
CreateOrderRequest:
type: object
required: [items]
properties:
items:
type: array
minItems: 1
items:
$ref: "#/components/schemas/OrderItemRequest"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
5.2 Code-First vs Design-First
Design-first: Write OpenAPI spec → generate server stubs → implement logic.
openapi.yaml → code generator → app/routes/orders.py (skeleton)
Forces you to think about the API contract before implementation. Client teams can start building against the spec before the server is done.
Code-first: Write implementation → generate OpenAPI spec from code.
# FastAPI generates OpenAPI automatically from type hints
@app.post("/orders", response_model=OrderResponse, status_code=201)
def create_order(body: CreateOrderRequest, user: User = Depends(get_current_user)):
...
FastAPI, Spring Boot, and most modern frameworks generate the spec from code annotations. Faster to start, but the spec reflects what you built — not necessarily what you intended.
Recommendation: Design-first for public APIs (contract is the product). Code-first for internal APIs (speed matters more).
5.3 Contract Testing — Validating Both Sides
Contract testing validates that:
- Your API responses match the OpenAPI schema (server side)
- Client code correctly handles the API responses (client side)
Server-side contract validation:
# schemathesis: automatically generates test cases from OpenAPI spec
import schemathesis
schema = schemathesis.from_path("openapi.yaml", base_url="http://localhost:8000")
@schema.parametrize()
def test_api_matches_spec(case):
"""
Schemathesis reads openapi.yaml, generates hundreds of test inputs
(valid, invalid, edge cases), sends them, and validates responses
match the declared schema.
"""
response = case.call()
case.validate_response(response)
This one test catches:
- Missing required fields in responses
- Wrong field types (string where integer expected)
- Undeclared status codes being returned
- Required fields absent in error responses
Client-side contract testing (Pact):
Pact lets the consumer (client) define what responses they expect. The provider (server) verifies it satisfies those expectations:
# Consumer (client) side — defines expectations
pact = Consumer("mobile-app").has_pact_with(Provider("orders-api"))
pact.given("order 99 exists").upon_receiving("a request for order 99").with_request(
method="GET",
path="/orders/99",
headers={"Authorization": "Bearer valid-token"}
).will_respond_with(
status=200,
body={
"id": 99,
"status": Like("pending"), # any string
"created_at": Like("2024-01-15T10:30:00Z")
}
)
# Provider (server) side — verifies it satisfies consumer expectations
# Run in CI: fetches consumer pacts from Pact Broker, runs against real server
Pact is powerful for microservice teams where the consumer and provider are different teams — each can test independently.
6. What to Test — The Complete Checklist
For every endpoint, verify:
Happy path:
- Correct status code (201 for POST, 200 for GET, 204 for DELETE)
- Response body matches schema
- All required fields present
- Field types correct (no string where integer expected)
- Timestamps in ISO 8601 format
- Created resource is actually persisted (GET after POST)
Authentication:
- Returns 401 with no token
- Returns 401 with expired token
- Returns 401 with malformed token
- Returns 401 with token signed by wrong key
Authorization:
- Returns 404 (or 403) when accessing another user's resource
- Admin can access all resources
- Correct role required for privileged operations
Validation:
- Returns 422 for each missing required field
- Returns 422 for each field with invalid format
- Returns 422 for out-of-range values
- All validation errors returned at once (not one at a time)
- Error response matches error schema
Edge cases:
- What happens with an empty list in a batch request?
- What happens with the maximum allowed input (100 items, 1000 char string)?
- What happens when a referenced resource doesn't exist (FK constraint)?
- What happens on concurrent modification (optimistic locking)?
- What happens when an idempotency key is reused?
7. Load Testing — Does It Scale?
Functional tests tell you it works correctly. Load tests tell you it works at scale.
7.1 k6 — The Standard Tool
// k6 load test script
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
stages: [
{ duration: "2m", target: 100 }, // ramp up to 100 virtual users
{ duration: "5m", target: 100 }, // stay at 100 for 5 minutes
{ duration: "2m", target: 200 }, // ramp up to 200
{ duration: "5m", target: 200 }, // stay at 200
{ duration: "2m", target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ["p95<500"], // 95th percentile response time < 500ms
http_req_failed: ["rate<0.01"], // error rate < 1%
},
};
export default function () {
const res = http.get("https://api.example.com/products", {
headers: { Authorization: "Bearer test-token" },
});
check(res, {
"status is 200": (r) => r.status === 200,
"response time < 500ms": (r) => r.timings.duration < 500,
"has data field": (r) => JSON.parse(r.body).data !== undefined,
});
sleep(1); // think time between requests
}
Run it: k6 run load_test.js
7.2 What to Measure
Throughput: requests per second (RPS) your API can handle
Latency: P50, P95, P99 response times (50th, 95th, 99th percentile)
Error rate: % of requests returning 5xx
Saturation: at what RPS do P99 latencies start degrading?
The four golden signals (from Google SRE book):
- Latency: How long requests take
- Traffic: Requests per second
- Errors: Rate of failed requests
- Saturation: How "full" your service is (CPU, memory, DB connections)
7.3 What Load Tests Reveal
- N+1 query problem:
GET /ordersissues 1 query for the list + N queries for each order's user. At 1 user it's fine. At 100 concurrent users with 20 orders each: 2100 queries per request. - Missing indexes: Tests pass with 100 rows in test DB. Load test with 10M rows shows 10-second query times.
- Connection pool exhaustion: 200 concurrent requests each hold a DB connection. Pool size is 100. 100 requests queue and time out.
- Memory leaks: After 30 minutes of load, memory grows and never releases. Server OOMs.
8. API Documentation — The Living Kind
8.1 What Good Documentation Contains
For every endpoint:
1. What it does (one clear sentence)
2. Authentication required? What scope?
3. Request: URL params, query params, request body (all fields, types, required/optional)
4. Response: all possible status codes with example responses for each
5. Error codes: every error code this endpoint can return, with fix instructions
6. Code examples: curl + one SDK language minimum
7. Rate limits that apply
8.2 Swagger UI — Auto-Generated from OpenAPI
If you have an OpenAPI spec, you get interactive documentation for free:
# FastAPI serves Swagger UI automatically
# Visit http://localhost:8000/docs
# Customize with rich descriptions
@app.post(
"/orders",
status_code=201,
summary="Create an order",
description="""
Creates a new order for the authenticated user.
**Idempotency:** Send an `Idempotency-Key` header to safely retry failed requests.
**Inventory:** Stock is reserved at creation time. If payment fails, stock is released.
""",
response_description="The created order with server-assigned ID and timestamps"
)
def create_order(
body: CreateOrderRequest,
idempotency_key: Optional[str] = Header(None, alias="Idempotency-Key"),
user: User = Depends(get_current_user)
):
...
8.3 The Three Levels of API Documentation
Level 1 — Reference docs (generated from OpenAPI):
Every endpoint, every field, every response. Accurate because it's generated from code. But not enough on its own — reference docs tell you what, not why or how.
Level 2 — Guides and tutorials:
Written prose walking through common use cases. "How to process a payment," "How to set up webhooks," "How to handle card declines." These must be written by humans.
Level 3 — Changelog:
Every breaking change, every new feature, every deprecation. Dated. Explicit. Clients read this to know if they need to update their integration.
Stripe's documentation is the gold standard. Study it: https://stripe.com/docs/api
8.4 Writing Great Code Examples
Every endpoint should have a curl example that works:
# Create an order
curl -X POST https://api.example.com/v1/orders \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-req-$(uuidgen)" \
-d '{
"items": [
{"product_id": 42, "quantity": 2}
],
"shipping_address": {
"street": "MG Road",
"city": "Bengaluru",
"postal_code": "560001"
}
}'
And the expected response:
{
"id": 1234,
"status": "pending",
"items": [
{
"product_id": 42,
"quantity": 2,
"unit_price": 85000,
"total": 170000
}
],
"total": 170000,
"created_at": "2024-01-15T10:30:00Z"
}
If a developer can copy-paste your curl example and get a real response in under 2 minutes, your documentation is good.
8.5 Postman / Insomnia Collections
Export your API as a Postman collection — a clickable, executable set of all your API endpoints with pre-filled examples. Many API teams ship this alongside documentation.
// postman_collection.json (simplified)
{
"info": {"name": "Orders API v1"},
"item": [
{
"name": "Create Order",
"request": {
"method": "POST",
"url": "{{base_url}}/v1/orders",
"header": [
{"key": "Authorization", "value": "Bearer {{api_key}}"},
{"key": "Content-Type", "value": "application/json"}
],
"body": {
"raw": "{\"items\": [{\"product_id\": 42, \"quantity\": 2}]}"
}
}
}
]
}
Developers import this, set their API key as an environment variable, and can immediately call every endpoint without reading the docs first.
9. Testing Webhooks
Webhooks are HTTP requests your server makes to a client's server (reverse of normal). Testing them requires:
Locally: Use webhook.site or ngrok to expose localhost:
ngrok http 8000
# → https://a1b2c3d4.ngrok.io
# Register webhook URL in your API
curl -X POST https://api.example.com/webhooks \
-d '{"url": "https://a1b2c3d4.ngrok.io/webhooks", "events": ["order.created"]}'
In integration tests: Spin up a minimal HTTP server to receive webhooks and assert on the payload:
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
received_webhooks = []
class WebhookReceiver(BaseHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers["Content-Length"])
body = self.rfile.read(content_length)
received_webhooks.append(json.loads(body))
self.send_response(200)
self.end_headers()
def test_order_created_webhook_delivered():
server = HTTPServer(("localhost", 9999), WebhookReceiver)
thread = threading.Thread(target=server.handle_request)
thread.start()
# Register webhook
client.post("/webhooks", json={"url": "http://localhost:9999", "events": ["order.created"]})
# Create order
client.post("/orders", json={"items": [...]}, headers=auth_headers)
# Wait for webhook delivery
thread.join(timeout=5)
assert len(received_webhooks) == 1
assert received_webhooks[0]["event"] == "order.created"
assert "order" in received_webhooks[0]["data"]
# Verify signature (Chapter 9: HMAC)
payload = received_webhooks[0]
expected_sig = hmac.new(WEBHOOK_SECRET, json.dumps(payload).encode(), hashlib.sha256).hexdigest()
assert received_webhooks[0]["signature"] == expected_sig
10. CI/CD Integration
Tests are only valuable if they run automatically. Wire everything into your CI pipeline:
# .github/workflows/api-tests.yml
name: API Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run unit tests
run: pytest tests/unit/ -v --tb=short
- name: Run integration tests
run: pytest tests/integration/ -v --tb=short
env:
DATABASE_URL: postgresql://postgres:test@localhost/test_db
- name: Contract test (schema validation)
run: schemathesis run openapi.yaml --url http://localhost:8000 --checks all
- name: Security scan
run: bandit -r app/ -ll # static security analysis
load-test:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # only on main
steps:
- name: Run load test
run: k6 run --vus 50 --duration 60s tests/load/smoke_test.js
The rule: Every PR must pass all tests before merge. Load tests run on every merge to main. Contract tests run on every deploy to production.
11. How Real Systems Test and Document
Stripe:
- OpenAPI spec maintained at github.com/stripe/openapi
- Test mode API keys — safe to run full integration tests with real API calls
stripe trigger payment_intent.succeededCLI command fires test webhook events- Every SDK has its own test suite against the spec
- Documentation includes interactive code examples in 8 languages that run in the browser
GitHub:
- OpenAPI spec at github.com/github/rest-api-description
- Octokit SDK generated from the spec
- Every API change goes through spec review before code review
- Breaking change detection: CI compares new spec to previous version, fails on breaking changes
Twilio:
- Helper libraries (SDKs) generated from OpenAPI spec for 7 languages
- Exhaustive error documentation: every error code has a docs page with common causes and fixes
- Test credentials: separate API keys that don't actually send SMS/calls
- Webhook testing with live event inspector in the dashboard
12. Vocabulary Map
| Term | Definition |
|---|---|
| OpenAPI | Standard specification format for describing REST APIs. Formerly Swagger. YAML or JSON. |
| Swagger UI | Auto-generated interactive documentation from an OpenAPI spec. |
| Contract test | Test validating that API responses match a defined schema (OpenAPI). |
| Pact | Contract testing framework for consumer-driven contracts between services. |
| Integration test | Test that exercises the full stack (HTTP → middleware → handler → DB → response). |
| Unit test | Test of a single function in isolation, with dependencies mocked. |
| Load test | Test simulating multiple concurrent users to measure throughput, latency, error rate. |
| k6 | Modern load testing tool. Tests written in JavaScript. |
| schemathesis | Tool that generates and runs tests from an OpenAPI spec. |
| P95 latency | The response time below which 95% of requests complete. Better than average for capacity planning. |
| N+1 query | Performance bug: fetching a list runs 1 query, then 1 more query per item. O(N) DB calls. |
| Design-first | Write the OpenAPI spec before implementing. Spec is the contract, code follows. |
| Code-first | Write the implementation, generate the OpenAPI spec from annotations. |
| Webhook | HTTP request your server makes to a client's URL when an event occurs. |
| ngrok | Tool that creates a public URL tunneling to your localhost. Used for webhook testing. |
| Test client | An HTTP client that calls your app directly without network, used in integration tests. |
| Golden signals | Latency, traffic, errors, saturation — the four metrics that define service health. |
| Smoke test | Minimal load test (low VUs, short duration) to verify basic functionality under any load. |
| Postman collection | Exported set of API requests with examples, shareable with developers. |
| Changelog | Dated list of API changes. Breaking changes explicitly noted. Required for public APIs. |
13. Practice Problems
Problem 1 — Failure Analysis:
Your integration tests all pass. You deploy to production and GET /users?sort=created_at takes 45 seconds. Your test database had 500 rows. Production has 8 million rows. Identify the root cause. What type of test would have caught this before deployment? Write the specific test (schema, data volume, assertion) that would have caught it. What index is missing?
Problem 2 — Design From Scratch:
Design the complete testing strategy for a payments API (POST /charges, GET /charges/{id}, POST /refunds). Write: (a) 3 unit tests for charge validation logic, (b) 3 integration tests covering auth + happy path + failure, (c) the OpenAPI schema snippet for the Charge response object, (d) the load test configuration (stages, thresholds) for an API expected to handle 500 RPS at P95 < 200ms, (e) the CI pipeline stages in order.
Problem 3 — Synthesis:
Your team uses design-first OpenAPI. The spec says POST /orders returns a 201 with an Order object including id, status, total, and created_at as required fields. A developer implements the handler but accidentally omits total from the response. Three mechanisms should catch this before it reaches production. Name all three (hint: one is a test type, one is a tool, one is a CI check). How does each one catch it?
Problem 4 — Go Deeper:
You are writing a load test for GET /orders with cursor-based pagination. The endpoint authenticates via JWT, queries Postgres with a keyset WHERE clause, and caches results in Redis for 30 seconds. Design a realistic load test that: (a) generates realistic JWTs for 1000 different test users, (b) simulates cursor pagination (first request has no cursor; subsequent requests use the cursor from the previous response), (c) measures separately: cache hit latency vs cache miss latency, (d) detects when Redis eviction causes a cache miss spike. Write the k6 script structure (pseudocode is fine).
Problem 5 — Connect to Previous (Full Circle): You have built a complete REST API over these 10 chapters. A junior engineer joins and asks: "Why do we need all these tests? The code looks right." Write the answer using one concept from each chapter — (Ch1) HTTP, (Ch2) REST constraints, (Ch3) resource design, (Ch4) response design, (Ch5) auth, (Ch6) errors, (Ch7) versioning, (Ch8) caching, (Ch9) security — showing how a test failure in each area manifests as a real production bug. This is your synthesis of the entire book.
The Book Is Complete
You started with raw bytes on a wire. You end with a complete, tested, documented, secure, performant REST API system.
What You Now Know
The Foundation (Chapters 1–2)
- HTTP is structured text over TCP. Every API call is bytes. Latency is physics.
- REST is 6 constraints, not a URL style. Statelessness, uniform interface, and cacheability are what make APIs scalable.
The Design (Chapters 3–4)
- URLs are addresses for resources (nouns), not calls to functions (verbs).
- Request and response bodies are contracts. Field types, null handling, date formats, and money representation are not cosmetic choices — they determine whether clients break.
The Trust (Chapter 5)
- Authentication and authorization are separate. API keys for M2M, JWTs for stateless auth, OAuth 2.0 for delegated access.
- IDOR — failing to check resource ownership — is the most common and most dangerous bug.
The Communication (Chapters 6–7)
- Errors are for developers. RFC 7807, machine-readable codes, all validation errors at once.
- APIs must evolve. URL versioning, additive-only changes, 12-month deprecation windows, cursor pagination for large datasets.
The Performance (Chapter 8)
Cache-Control, ETags, and 304 Not Modified can eliminate 90% of origin load with headers alone.- gzip/Brotli compress JSON by 70–85%.
Vary: Accept-Encodingkeeps caches correct.
The Safety (Chapter 9)
- OWASP API Top 10 is the threat model. BOLA, mass assignment, and SSRF are the hardest to see.
- CORS is a browser instruction, not a server security control. HSTS enforces HTTPS. Never store secrets in code.
The Proof (Chapter 10)
- Unit tests verify logic. Integration tests verify the full stack. Contract tests verify the spec. Load tests verify scale.
- OpenAPI is your contract with the world. Documentation is part of the product.
What's Next
This book made you a backend API engineer. The next level:
Go deeper on any one chapter:
Distributed transactions (Ch13 of the Distributed Systems book) is what POST /payments requires at scale. Kafka (Ch15) is what powers webhooks at millions of events/day. Caching at the DB layer (Ch9 of distributed systems) is what makes GET /products serve millions of users.
Build something:
Design a full API for a domain you know. Use every principle in this book. Implement it. Load test it. Document it. The gap between knowing and building is where the real learning lives.
Study great APIs:
Stripe, GitHub, Twilio, and Shopify are the best API designs in the world. Read their docs, not as a user, but as an engineer asking: why did they make this choice? Every decision you've read about in this book — you'll see it in their APIs.
The curriculum is complete.