■
The Complete API
Guide
Concepts - Design - Python - Testing -
Security
From zero to production — everything you need to understand, build,
consume, test and secure APIs — all explained with Python.
REST - GraphQL - WebSocket - gRPC - Webhooks - OAuth - JWT
Flask - FastAPI - Django REST - requests - pytest - Swagger
March 2026 - v1.0
The Complete API Guide — Concepts - Python - Testing - Security Page 1
CHAPTER
Table of Contents
1 What Is an API?
— The restaurant analogy
— Client-server model
— Protocols & interfaces
— Why APIs matter
2 Types of APIs
— REST
— GraphQL
— SOAP
— gRPC
— WebSocket
— Webhooks
— Comparison table
3 HTTP Deep Dive
— Request/response anatomy
— Methods (GET POST PUT PATCH DELETE)
— Status codes
— Headers
— Body & content types
4 REST API Design
— Resources & URLs
— CRUD mapping
— Versioning
— Pagination
— Filtering & sorting
— Best practices
5 Building APIs with Flask
— Setup
— Routes & methods
— Request parsing
— Responses & status codes
— Blueprints
— Error handling
6 Building APIs with FastAPI
— Why FastAPI
— Pydantic models
— Path & query params
— Request body
— Auto docs
— Async endpoints
7 Building APIs with Django REST Framework
— Serializers
— ViewSets
— Routers
— Permissions
The Complete API Guide — Concepts - Python - Testing - Security Page 2
— Pagination
— Filtering
8 Consuming APIs with Python
— requests library
— Sessions
— Timeouts & retries
— Handling responses
— Async with httpx
9 Authentication & Authorisation
— API keys
— Basic Auth
— Token auth
— JWT deep dive
— OAuth 2.0 flows
— Session cookies
10 API Security
— HTTPS & TLS
— CORS
— Rate limiting
— Input validation
— SQL injection prevention
— Security headers
11 Testing APIs
— Unit testing endpoints
— pytest + requests
— Mocking external APIs
— Integration tests
— Postman/Thunder Client
12 API Documentation
— OpenAPI / Swagger
— Writing good docs
— Auto-generated docs in FastAPI
— ReDoc
— Versioning docs
13 GraphQL with Python
— Schema definition
— Queries & mutations
— Strawberry library
— Comparison with REST
— When to choose GraphQL
14 WebSockets with Python
— What are WebSockets
— Use cases
— FastAPI WebSocket endpoint
— Client in Python
— Broadcasting
15 Webhooks
— Push vs pull
— Designing webhooks
— Receiving webhooks in Flask
The Complete API Guide — Concepts - Python - Testing - Security Page 3
— Signature verification
— Retry logic
16 Rate Limiting, Caching & Pagination
— Why rate limit
— flask-limiter
— Redis caching
— ETag caching
— Cursor-based pagination
17 Deployment & Production
— Gunicorn + Uvicorn
— Docker packaging
— Environment variables
— Reverse proxy (Nginx)
— Health checks
18 Real-World Project — Todo API
— Design
— Full Flask implementation
— Full FastAPI implementation
— Tests
— Docker Compose stack
19 Best Practices & Patterns
— API design principles
— Error handling patterns
— Idempotency
— HATEOAS
— Versioning strategies
20 Cheat Sheet & Quick Reference
— HTTP methods
— Status codes
— Python snippets
— curl commands
— Troubleshooting
The Complete API Guide — Concepts - Python - Testing - Security Page 4
CHAPTER 1
What Is an API?
The foundation — understanding interfaces before writing a single line of code
1.1 The Core Concept
An API — Application Programming Interface — is a contract between two pieces of software. It defines exactly
what requests one program can make to another, what format those requests must follow, and what responses to
expect in return. Think of it as a menu at a restaurant: you don't need to know how the kitchen works — you just
need to know what you can order, how to order it, and what you'll receive.
API — Application Programming Interface
A clearly defined set of rules, protocols, and tools that allows different software applications to
communicate with each other. The API specifies the inputs a system accepts, the outputs it produces, and
the operations it supports — without exposing its internal implementation.
1.2 The Restaurant Analogy (Explained in Depth)
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
■ THE RESTAURANT ANALOGY ■
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
■ ■
■ YOU (Customer) WAITER (API) KITCHEN (Server/DB) ■
■ ■■■■■■■■■■■■■■ ■■■■■■■■■■■■■ ■■■■■■■■■■■■■■■■■■■■■■■■ ■
■ ■ You look ■ ■ Waiter ■ ■ Chef prepares food ■ ■
■ ■ at the ■■■■■■■■ takes ■■■■■■■■■■ using internal ■ ■
■ ■ MENU (API ■ ■ your ■ ■ processes (database, ■ ■
■ ■ docs) and ■ ■ ORDER ■ ■ business logic, ■ ■
■ ■ place an ■ ■ (request) ■ ■ algorithms) ■ ■
■ ■ ORDER ■ ■ ■■■■■■■■■■ that you never see ■ ■
■ ■ (request) ■■■■■■■■ brings ■ ■■■■■■■■■■■■■■■■■■■■■■■■ ■
■ ■■■■■■■■■■■■■■ ■ your FOOD ■ ■
■ ■ (response)■ ■
■ ■■■■■■■■■■■■■ ■
■ ■
■ You don't need to know HOW the food is cooked — ■
■ only WHAT to order and WHAT you will receive. ■
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
Figure 1.1 — The Restaurant Analogy for APIs
In this analogy: The menu is the API documentation — it tells you what's available. Your order is the API request
— a specific, formatted ask. The waiter is the API layer — it relays requests and responses without you needing
to enter the kitchen. The kitchen is the server — complex internal logic that produces the result. The meal is the
API response — the data or action result you receive.
1.3 The Client-Server Model
The Complete API Guide — Concepts - Python - Testing - Security Page 5
Almost all web APIs follow a client-server architecture. The client is any program that makes requests — a
mobile app, a web browser, a Python script, or another server. The server is the program that receives requests,
processes them, and sends back responses.
CLIENT NETWORK SERVER
■■■■■■■■■■■■■■■■■■■ ■■■■■■■■■■■■■■■■■■■■■■■■
■ Mobile App ■■■■■■■■■■■ HTTP Request ■■■■■■■■■■■■ Web Server ■
■ Web Browser ■ (GET /users/42) ■ (Flask / FastAPI) ■
■ Python Script ■■■■■■■■■■■■ HTTP Response ■■■■■■■■■■ ■
■ Another Server ■ (200 OK + JSON) ■ ■■■■■■■■■■■■■■■■■■ ■
■■■■■■■■■■■■■■■■■■■ ■ ■ Database ■ ■
■ ■ Business Logic■ ■
■ ■■■■■■■■■■■■■■■■■■ ■
■■■■■■■■■■■■■■■■■■■■■■■■
Key principle: The client and server are DECOUPLED.
The client doesn't care what language the server is written in.
The server doesn't care what kind of client is calling it.
Figure 1.2 — Client-Server Architecture
1.4 Why APIs Matter — Real-World Impact
■ Reusability — Build a feature once as an API and let many different clients (web, mobile, desktop, partner
systems) use it without rebuilding the logic each time.
■ Separation of concerns — Frontend and backend teams work independently. The frontend team only
needs to know the API contract, not how the backend stores data.
■ Integration — APIs let completely different systems communicate. Your Python app can send emails via
the SendGrid API, accept payments via Stripe's API, and store files in AWS S3 — all without building any of
those systems yourself.
■ Scalability — Each API service can be scaled independently based on demand.
■ The modern economy — Twilio's entire business IS an API. Stripe processes billions via API. Every app
you use daily consumes dozens of APIs behind the scenes.
1.5 How APIs Are Used in Practice — Python Example
Here's the simplest possible API interaction — fetching weather data from a public API using Python. This shows
the complete request-response cycle:
The Complete API Guide — Concepts - Python - Testing - Security Page 6
Python - Consuming an API — Weather Example
import requests
# Step 1: Define the API endpoint (URL) and your credentials (API key)
API_KEY = 'your_openweathermap_api_key'
CITY = 'Seattle'
URL = f'[Link]
# Step 2: Build the request parameters
params = {
'q': CITY,
'appid': API_KEY,
'units': 'metric', # celsius
}
# Step 3: Send the HTTP GET request
response = [Link](URL, params=params)
# Step 4: Check if the request was successful
if response.status_code == 200:
data = [Link]() # Parse the JSON response
temp = data['main']['temp']
desc = data['weather'][0]['description']
humidity = data['main']['humidity']
print(f'Weather in {CITY}:')
print(f' Temperature : {temp}°C')
print(f' Conditions : {desc}')
print(f' Humidity : {humidity}%')
else:
print(f'Error {response.status_code}: {[Link]()}')
# Output:
# Weather in Seattle:
# Temperature : 12.4°C
# Conditions : light rain
# Humidity : 78%
The Complete API Guide — Concepts - Python - Testing - Security Page 7
CHAPTER 2
Types of APIs
REST, GraphQL, SOAP, gRPC, WebSocket, Webhooks — when to use which
2.1 Overview
Not all APIs are created equal. Different types of APIs use different protocols, data formats, and communication
patterns. Choosing the right API type is one of the most important architectural decisions you'll make. This
chapter explains each major type, its strengths, its weaknesses, and the scenarios where it shines.
2.2 REST — Representational State Transfer
REST
An architectural style (not a protocol) for building APIs over HTTP. REST treats every piece of data as a
'resource' identified by a URL, and uses standard HTTP methods (GET, POST, PUT, DELETE) to perform
operations on those resources. The server sends back representations of resources (usually JSON).
Invented by Roy Fielding in his 2000 PhD dissertation.
The 6 REST Constraints — an API is truly RESTful only when it follows these:
■ 1. Client-Server — The UI and data storage are separated. Each can evolve independently.
■ 2. Stateless — Each request contains ALL information needed to process it. The server stores no session
state between requests.
■ 3. Cacheable — Responses must declare whether they are cacheable. Caching improves performance.
■ 4. Uniform Interface — Consistent URL conventions, HTTP methods, and response formats across the
entire API.
■ 5. Layered System — The client doesn't know if it's talking to the server directly or through a proxy/load
balancer.
■ 6. Code on Demand — (Optional) Servers can send executable code to clients (e.g., JavaScript).
The Complete API Guide — Concepts - Python - Testing - Security Page 8
Python - REST API — GitHub Example
# REST API example — interacting with GitHub's REST API
import requests
BASE = '[Link]
headers = {'Accept': 'application/[Link].v3+json'}
# GET — Read a resource (a user's profile)
r = [Link](f'{BASE}/users/torvalds', headers=headers)
user = [Link]()
print(user['name'], '—', user['public_repos'], 'repos')
# GET — Read a collection (list repos)
r = [Link](f'{BASE}/users/torvalds/repos', headers=headers)
repos = [Link]()
for repo in repos[:3]:
print(f' {repo["name"]}: ★{repo["stargazers_count"]}')
# POST — Create a resource (requires auth — gist example)
TOKEN = 'ghp_your_token_here'
auth_headers = {**headers, 'Authorization': f'token {TOKEN}'}
new_gist = {
'description': 'Hello from Python',
'public': True,
'files': {'[Link]': {'content': 'print("Hello, World!")'}}
}
r = [Link](f'{BASE}/gists', json=new_gist, headers=auth_headers)
print('Created gist:', [Link]()['html_url']) # 201 Created
2.3 GraphQL
GraphQL
A query language and runtime for APIs, developed by Facebook (2012, open-sourced 2015). Instead of
fixed endpoints (one URL per resource), GraphQL exposes a SINGLE endpoint. The client sends a precise
query describing exactly which fields it wants — no more, no less. This solves REST's 'over-fetching' and
'under-fetching' problems.
REST (multiple endpoints): GraphQL (single endpoint):
GET /users/42 GET /posts POST /graphql
→ {id, name, email, → [{id, query {
address, avatar, title, user(id: 42) {
created_at, ...} body, name
(over-fetching!) author_id, posts {
...} title
GET /users/42/posts (need user }
→ must make name too → }
separate request! under- }
(under-fetching!) fetching!)
GraphQL returns EXACTLY what you asked for — nothing more, nothing less.
Figure 2.1 — REST vs GraphQL: Over-fetching & Under-fetching
The Complete API Guide — Concepts - Python - Testing - Security Page 9
Python - GraphQL Query — GitHub API
# GraphQL query sent via Python requests
import requests
GRAPHQL_URL = '[Link]
TOKEN = 'ghp_your_token'
# Define the query — ask for EXACTLY the fields you need
query = '''
query GetUser($login: String!) {
user(login: $login) {
name
bio
followers { totalCount }
repositories(first: 3, orderBy: {field: STARGAZERS, direction: DESC}) {
nodes { name stargazerCount primaryLanguage { name } }
}
}
}
'''
response = [Link](
GRAPHQL_URL,
json={'query': query, 'variables': {'login': 'torvalds'}},
headers={'Authorization': f'bearer {TOKEN}'}
)
data = [Link]()['data']['user']
print(data['name'], '—', data['bio'])
for repo in data['repositories']['nodes']:
print(f' {repo["name"]} ({repo["primaryLanguage"]["name"]}) ★{repo["stargazerCount"]}')
2.4 SOAP — Simple Object Access Protocol
SOAP
A protocol (not just a style) for exchanging structured information using XML over HTTP, SMTP, or other
protocols. SOAP is older (1998), verbose, and strictly typed. It uses WSDL (Web Services Description
Language) to describe the API. Still prevalent in enterprise, banking, and government systems. Python
uses the 'zeep' library to consume SOAP services.
■ Strengths ■ Weaknesses
SOAP Strengths SOAP Weaknesses
■ Strict contract via WSDL — hard to misuse ■ Extremely verbose XML payloads
■ Built-in WS-Security standard ■ Complex to implement and debug
■ Supports transactions and ACID compliance ■ No browser support — needs special tooling
■ Works over any protocol, not just HTTP ■ Much slower than REST/JSON
■ Widely used in banking/finance/government ■ Being replaced by REST in most new systems
2.5 gRPC — Google Remote Procedure Call
The Complete API Guide — Concepts - Python - Testing - Security Page 10
gRPC
A high-performance, open-source framework from Google (2015). Instead of JSON over HTTP/1.1, gRPC
uses Protocol Buffers (binary format) over HTTP/2. It's 5-10x more efficient than REST/JSON for the same
data. Best suited for internal microservice communication where performance matters. Uses a .proto file to
define the API schema.
Proto / Python - gRPC Example
# [Link] — gRPC service definition
syntax = 'proto3';
service UserService {
rpc GetUser (UserRequest) returns (UserResponse);
rpc CreateUser (CreateRequest) returns (UserResponse);
rpc ListUsers (Empty) returns (stream UserResponse);
}
message UserRequest { int32 id = 1; }
message CreateRequest{ string name = 1; string email = 2; }
message UserResponse { int32 id = 1; string name = 2; string email = 3; }
message Empty {}
# Python client (generated from .proto)
import grpc
import user_pb2, user_pb2_grpc
channel = grpc.insecure_channel('localhost:50051')
stub = user_pb2_grpc.UserServiceStub(channel)
# Call like a local function — gRPC handles the network
user = [Link](user_pb2.UserRequest(id=42))
print([Link], [Link]) # Very fast — binary transport
2.6 WebSocket APIs
WebSocket
A communication protocol providing a persistent, full-duplex connection between client and server over a
single TCP connection. Unlike HTTP (request → response → connection closes), WebSockets keep the
connection open, allowing the SERVER to push data to the client at any time without the client asking.
Perfect for real-time apps: chat, live prices, collaborative editing, gaming.
2.7 Webhooks
Webhook
A 'reverse API' — instead of YOUR code calling someone else's API, THEIR server calls YOUR URL when
something happens. You register a URL with a service, and when an event occurs (payment succeeds,
email bounced, commit pushed), they send an HTTP POST to your URL with event data. Think of it as
'don't call us, we'll call you.'
2.8 Comparison Table — Choosing the Right API Type
The Complete API Guide — Concepts - Python - Testing - Security Page 11
Type Protocol Data Format Best For Not Ideal For
REST HTTP/1.1 JSON / XML Public APIs, web/mobile apps, Complex queries, real-time
CRUD operations data
GraphQ HTTP JSON Complex data relationships, mobile Simple APIs, teams new to it
L (bandwidth)
SOAP HTTP/SMT XML Enterprise, banking, strict contracts Modern web/mobile apps
P
gRPC HTTP/2 Protobuf (binary) Internal microservices, high Public APIs, browser clients
throughput
WebSo TCP/WS Any Real-time: chat, live feeds, gaming Request/response patterns
cket (JSON/binary)
Webho HTTP JSON / XML Event notifications, integrations, When you need to query on
ok CI/CD demand
The Complete API Guide — Concepts - Python - Testing - Security Page 12
CHAPTER 3
HTTP Deep Dive
The protocol that powers the web — and almost all modern APIs
3.1 What is HTTP?
HTTP — HyperText Transfer Protocol — is the foundation of data communication on the web. It defines the rules
for how clients (browsers, apps, scripts) send requests to servers, and how servers send responses back. Almost
all REST APIs, and many other API types, run on HTTP.
HTTP is stateless — each request is completely independent. The server doesn't remember previous requests.
This simplicity is what makes HTTP so scalable.
3.2 Anatomy of an HTTP Request
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
■ HTTP REQUEST ■
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
■ REQUEST LINE: ■
■ POST /api/v1/users HTTP/1.1 ■
■ ^ ^ ^ ■
■ ■ ■ ■■■ Protocol version ■
■ ■ ■■■■■■■■■■■■■■■ Path (resource being accessed) ■
■ ■■■■■■■■■■■■■■■■■■■■■■ HTTP Method (what action to perform) ■
■ ■
■ HEADERS (key: value pairs — metadata about the request): ■
■ Host: [Link] ■
■ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... ■
■ Content-Type: application/json ■
■ Accept: application/json ■
■ User-Agent: python-requests/2.31.0 ■
■ ■
■ BLANK LINE (separates headers from body) ■
■ ■
■ BODY (payload — optional, used in POST/PUT/PATCH): ■
■ { ■
■ "name": "Alice", ■
■ "email": "alice@[Link]" ■
■ } ■
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
Figure 3.1 — HTTP Request Structure
3.3 Anatomy of an HTTP Response
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
■ HTTP RESPONSE ■
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
■ STATUS LINE: ■
■ HTTP/1.1 201 Created ■
■ ^ ^ ■
The Complete API Guide — Concepts - Python - Testing - Security Page 13
■ ■ ■■■ Reason phrase (human-readable) ■
■ ■■■■■■■■ Status code (machine-readable) ■
■ ■
■ RESPONSE HEADERS: ■
■ Content-Type: application/json; charset=utf-8 ■
■ Location: /api/v1/users/42 ← URL of new resource ■
■ X-Request-ID: d4e8f2a1-9b3c-4d5e ← for tracing ■
■ Cache-Control: no-store ■
■ ■
■ BODY: ■
■ { ■
■ "id": 42, ■
■ "name": "Alice", ■
■ "email": "alice@[Link]", ■
■ "created_at": "2026-03-15T14:22:00Z" ■
■ } ■
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
Figure 3.2 — HTTP Response Structure
3.4 HTTP Methods — The Verbs
Method Safe? Idempote Has B Purpose Example URL
nt? ody?
GET ■ Yes ■ Yes ■ No Retrieve a resource or collection GET /users/42
POST ■ No ■ No ■ Yes Create a new resource POST /users
PUT ■ No ■ Yes ■ Yes Replace a resource entirely PUT /users/42
PATCH ■ No ■ No ■ Yes Partially update a resource PATCH /users/42
DELET ■ No ■ Yes ■ No Delete a resource DELETE /users/42
E
HEAD ■ Yes ■ Yes ■ No Like GET but returns headers only HEAD /users/42
OPTIO ■ Yes ■ Yes ■ No Discover what methods are allowed OPTIONS /users
NS (CORS)
Safe = the request does not change server state (read-only). Idempotent = making the same request N times
has the same effect as making it once. DELETE /users/42 twice: second call returns 404 but the state (user
deleted) is the same. POST /users twice: creates TWO users — not idempotent!
3.5 HTTP Status Codes
Status codes are 3-digit numbers that tell the client what happened. They are grouped into 5 categories:
Code Meaning When to Use
1xx — Inf
ormation
al
The Complete API Guide — Concepts - Python - Testing - Security Page 14
100 Continue Server received request headers, client should continue sending body
2xx —
Success
200 OK Standard success for GET, PUT, PATCH — response body contains data
201 Created Resource was created (POST) — include Location header with new URL
204 No Content Success but no body (DELETE, or PUT with no response needed)
3xx — Re
direction
301 Moved Permanently Resource URL has changed permanently — update your bookmarks
304 Not Modified Cached version is still valid — client can use its local copy
4xx —
Client
Error
400 Bad Request Malformed request — invalid JSON, missing required field
401 Unauthorized No credentials or invalid credentials provided
403 Forbidden Credentials valid but user lacks permission for this resource
404 Not Found Resource doesn't exist at this URL
405 Method Not Allowed HTTP method not supported for this endpoint
409 Conflict State conflict — e.g., duplicate email on registration
422 Unprocessable Entity Syntactically correct but semantically invalid (validation error)
429 Too Many Requests Rate limit exceeded
5xx —
Server
Error
500 Internal Server Error Unexpected server crash — never show stack trace to client
502 Bad Gateway Upstream server (database, external API) returned invalid response
503 Service Unavailable Server temporarily down — deployments, maintenance
504 Gateway Timeout Upstream server took too long to respond
3.6 Common HTTP Headers
Header Direction Purpose Example Value
Content-Type Both Format of the message body application/json
The Complete API Guide — Concepts - Python - Testing - Security Page 15
Accept Request Formats the client can handle application/json, text/html
Authorization Request Credentials for authentication Bearer eyJhbGci...
Cache-Control Both Caching directives max-age=3600, no-store
Location Response URL of newly created or moved resource /api/users/42
X-Request-ID Both Unique ID for request tracing d4e8f2a1-9b3c-4d5e
ETag Response Version identifier for caching "33a64df5"
If-None-Match Request Sends ETag back for cache validation "33a64df5"
Retry-After Response Seconds to wait before retrying 60
X-Rate-Limit-Remaining Response Remaining requests in window 47
CORS headers Response Allow cross-origin access Access-Control-Allow-Origin:
*
3.7 Working with HTTP in Python
Shell - Inspecting HTTP with Python requests
import requests
# Inspecting request and response in detail
response = [Link](
'[Link]
headers={'Accept': 'application/json', 'X-Custom': 'my-value'},
params={'page': 1, 'per_page': 10},
)
# Inspect the response
print('Status Code :', response.status_code) # 200
print('Reason :', [Link]) # 'OK'
print('Content-Type :', [Link]['Content-Type'])
print('Elapsed :', [Link].total_seconds(), 's')
# Response body in different formats
json_data = [Link]() # Parse JSON → Python dict
raw_text = [Link] # Raw string
raw_bytes = [Link] # Raw bytes (for files/images)
# Inspect the REQUEST that was actually sent
req = [Link]
print('Request URL :', [Link]) # includes query params
print('Request Headers :', dict([Link]))
print('Request Body :', [Link])
The Complete API Guide — Concepts - Python - Testing - Security Page 16
CHAPTER 4
REST API Design
How to design clean, intuitive, and consistent APIs that developers love
4.1 Resources and URLs
In REST, everything is a resource — a noun representing a thing (user, product, order, post). Resources are
identified by URLs. The golden rule: URLs identify resources (nouns), HTTP methods express actions
(verbs).
■ Bad URL (verb in URL) ■ Good URL (noun + HTTP Why
method)
GET /getUser/42 GET /users/42 Method already implies 'get'
POST /createUser POST /users 'create' is implied by POST
GET /deleteUser/42 DELETE /users/42 GET should never delete things
POST /users/42/updateEmail PATCH /users/42 Action goes in HTTP method
GET /user_list GET /users Collections use plural nouns
GET /Users/42/Posts GET /users/42/posts URLs should be lowercase
4.2 URL Hierarchy — Relationships
Text - URL Design Patterns
# URL patterns communicate resource relationships clearly
# Collections (list of things)
GET /users # List all users
GET /products # List all products
# Individual resources
GET /users/42 # Get user with ID 42
GET /products/abc-123 # Get product (slug or UUID)
# Sub-resources (relationships)
GET /users/42/orders # Get all orders belonging to user 42
GET /users/42/orders/7 # Get order 7 from user 42
GET /courses/5/students # All students enrolled in course 5
# Avoid going too deep (max 3 levels)
# ■ Too deep: GET /users/42/orders/7/items/3/reviews/9
# ■ Better: GET /order-items/3/reviews/9
# Actions that don't fit CRUD (use sub-resources or query params)
POST /users/42/password-reset # Reset password
POST /users/42/deactivate # Deactivate account
POST /orders/7/cancel # Cancel an order
4.3 CRUD Mapping — The Complete Pattern
The Complete API Guide — Concepts - Python - Testing - Security Page 17
USERS RESOURCE — Complete CRUD API
HTTP Method URL Action Request Body Response
■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
■■
GET /users List users None 200 + [{...}]
POST /users Create user {name, email, pass} 201 + {id,...}
GET /users/{id} Get one user None 200 + {...}
PUT /users/{id} Replace user {name, email, ...} 200 + {...}
PATCH /users/{id} Update fields {email} 200 + {...}
DELETE /users/{id} Delete user None 204 (no body)
PUT vs PATCH:
PUT /users/42 {name:'Bob', email:'bob@[Link]'} → Replaces ENTIRE user object
PATCH /users/42 {email:'bob@[Link]'} → Updates ONLY the email field
Figure 4.1 — Complete CRUD Mapping for a REST Resource
4.4 API Versioning
APIs change over time. Versioning ensures existing clients don't break when you release breaking changes.
There are several versioning strategies:
Python - API Versioning with Flask Blueprints
# Strategy 1: URL versioning (most common, most visible)
GET /api/v1/users
GET /api/v2/users # v2 might return different fields
# Strategy 2: Header versioning
GET /api/users
Accept: application/[Link].v2+json
# Strategy 3: Query parameter
GET /api/users?version=2
# Python Flask — URL versioning example
from flask import Flask, Blueprint
app = Flask(__name__)
v1 = Blueprint('v1', __name__, url_prefix='/api/v1')
v2 = Blueprint('v2', __name__, url_prefix='/api/v2')
@[Link]('/users')
def get_users_v1():
return {'users': [{'id': 1, 'name': 'Alice'}]}
@[Link]('/users')
def get_users_v2():
# v2 adds pagination and richer user objects
return {'data': [{'id': 1, 'name': 'Alice', 'avatar_url': '...'}],
'meta': {'page': 1, 'total': 100}}
app.register_blueprint(v1)
app.register_blueprint(v2)
4.5 Pagination
Never return an unbounded list. Always paginate collections. Two main strategies:
The Complete API Guide — Concepts - Python - Testing - Security Page 18
JSON - Pagination Response Formats
# Strategy 1: Offset-based pagination (simple, but slower at scale)
# Client sends: GET /users?page=3&per_page=20
# Server returns:
{
'data': [...20 users...],
'meta': {
'page': 3,
'per_page': 20,
'total_items': 243,
'total_pages': 13
},
'links': {
'first': '/users?page=1&per_page=20',
'prev': '/users?page=2&per_page=20',
'next': '/users?page=4&per_page=20',
'last': '/users?page=13&per_page=20'
}
}
# Strategy 2: Cursor-based pagination (fast at scale, for feeds/timelines)
# Client: GET /posts?limit=20&cursor=eyJpZCI6MTAwfQ==
# Server returns:
{
'data': [...20 posts...],
'next_cursor': 'eyJpZCI6ODJ9', # opaque cursor — send back to get next page
'has_next': True
}
4.6 Filtering, Searching & Sorting
Python - Flask Filtering, Sorting & Pagination
# All via query parameters
GET /users?status=active # Filter by field
GET /users?role=admin&status=active # Multiple filters (AND)
GET /products?min_price=10&max_price=100 # Range filter
GET /users?q=alice # Full-text search
GET /users?sort=created_at&order=desc # Sort descending
GET /users?fields=id,name,email # Sparse fieldsets
# Flask implementation
from flask import request
@[Link]('/api/v1/users')
def list_users():
# Extract query params with defaults
page = [Link]('page', 1, type=int)
per_page = [Link]('per_page', 20, type=int)
status = [Link]('status', None)
sort_by = [Link]('sort', 'id')
order = [Link]('order', 'asc')
search = [Link]('q', None)
query = [Link]
if status: query = query.filter_by(status=status)
if search: query = [Link]([Link](f'%{search}%'))
if order == 'desc': query = query.order_by(getattr(User, sort_by).desc())
else: query = query.order_by(getattr(User, sort_by))
paginated = [Link](page=page, per_page=per_page)
return {'data': [u.to_dict() for u in [Link]],
'meta': {'total': [Link], 'page': page}}
The Complete API Guide — Concepts - Python - Testing - Security Page 19
The Complete API Guide — Concepts - Python - Testing - Security Page 20
CHAPTER 5
Building APIs with Flask
The lightweight, pragmatic way to build Python APIs
5.1 Why Flask?
Flask is a micro web framework for Python. 'Micro' means it provides the essentials (routing, request/response
handling) without making decisions for you — no forced ORM, no required project structure. It's perfect for
small-to-medium APIs, microservices, and learning API development. With the right extensions it scales to
production.
Python - Flask Hello World API
# Install Flask and common API extensions
pip install flask flask-sqlalchemy flask-marshmallow marshmallow flask-jwt-extended
# The simplest possible Flask API
from flask import Flask, jsonify, request
app = Flask(__name__)
@[Link]('/api/hello', methods=['GET'])
def hello():
name = [Link]('name', 'World')
return jsonify({'message': f'Hello, {name}!'}), 200
if __name__ == '__main__':
[Link](debug=True, port=5000)
# Test it:
# curl [Link]
# → {"message": "Hello, Alice!"}
5.2 A Complete Flask REST API
The Complete API Guide — Concepts - Python - Testing - Security Page 21
Python - Flask REST API — Part 1 (List & Create)
from flask import Flask, jsonify, request, abort
from datetime import datetime
import uuid
app = Flask(__name__)
# In-memory store (replace with a real database in production)
users = {}
def user_or_404(user_id):
"""Helper: return user or raise 404"""
user = [Link](user_id)
if not user:
abort(404, description=f'User {user_id} not found')
return user
# ■■ GET /users — List all users ■■■■■■■■■■■■■■■■■■■■■
@[Link]('/api/v1/users', methods=['GET'])
def list_users():
return jsonify({'users': list([Link]()), 'total': len(users)}), 200
# ■■ POST /users — Create a new user ■■■■■■■■■■■■■■■■■
@[Link]('/api/v1/users', methods=['POST'])
def create_user():
data = request.get_json()
if not data:
abort(400, description='Request body must be JSON')
# Validate required fields
for field in ['name', 'email']:
if field not in data:
abort(422, description=f'Missing required field: {field}')
user_id = str(uuid.uuid4())
user = {'id': user_id, 'name': data['name'],
'email': data['email'], 'created_at': [Link]().isoformat()}
users[user_id] = user
return jsonify(user), 201
The Complete API Guide — Concepts - Python - Testing - Security Page 22
Python - Flask REST API — Part 2 (Read, Update, Delete, Errors)
# ■■ GET /users/<id> — Get one user ■■■■■■■■■■■■■■■■■■
@[Link]('/api/v1/users/<user_id>', methods=['GET'])
def get_user(user_id):
return jsonify(user_or_404(user_id)), 200
# ■■ PATCH /users/<id> — Update user fields ■■■■■■■■■■■
@[Link]('/api/v1/users/<user_id>', methods=['PATCH'])
def update_user(user_id):
user = user_or_404(user_id)
data = request.get_json() or {}
# Only update fields that were sent
allowed = {'name', 'email'}
for key in allowed:
if key in data:
user[key] = data[key]
user['updated_at'] = [Link]().isoformat()
return jsonify(user), 200
# ■■ DELETE /users/<id> — Delete user ■■■■■■■■■■■■■■■■■
@[Link]('/api/v1/users/<user_id>', methods=['DELETE'])
def delete_user(user_id):
user_or_404(user_id) # raises 404 if not found
del users[user_id]
return '', 204 # 204 No Content — success, no body
# ■■ Error handlers — consistent error format ■■■■■■■■■
@[Link](400)
@[Link](404)
@[Link](422)
def handle_error(error):
return jsonify({
'error': [Link],
'message': [Link],
'status': [Link]
}), [Link]
@[Link](500)
def handle_500(error):
return jsonify({'error': 'Internal Server Error', 'status': 500}), 500
5.3 Request Parsing & Validation
The Complete API Guide — Concepts - Python - Testing - Security Page 23
Python - Flask Request Validation
from flask import request, abort
from functools import wraps
def require_json(f):
"""Decorator: ensure request has JSON body"""
@wraps(f)
def decorated(*args, **kwargs):
if not request.is_json:
abort(415, description='Content-Type must be application/json')
return f(*args, **kwargs)
return decorated
def validate_fields(data, required, optional=None):
"""Validate required and optional fields, return errors"""
errors = {}
for field in required:
if field not in data or data[field] is None:
errors[field] = 'This field is required'
elif isinstance(data[field], str) and not data[field].strip():
errors[field] = 'Cannot be empty'
return errors
@[Link]('/api/v1/users', methods=['POST'])
@require_json
def create_user():
data = request.get_json()
errors = validate_fields(data, required=['name', 'email'])
if errors:
return jsonify({'errors': errors}), 422
# ... proceed with creation
5.4 Blueprints — Organising a Large Flask API
The Complete API Guide — Concepts - Python - Testing - Security Page 24
Python - Flask Blueprint Project Structure
# Project structure:
# myapi/
# ■■■ [Link] ← application factory
# ■■■ [Link] ← configuration
# ■■■ api/
# ■■■ __init__.py
# ■■■ users/
# ■ ■■■ __init__.py
# ■ ■■■ [Link] ← @users_bp.route(...)
# ■ ■■■ [Link]
# ■■■ products/
# ■■■ [Link]
# api/users/[Link]
from flask import Blueprint, jsonify, request
users_bp = Blueprint('users', __name__, url_prefix='/api/v1/users')
@users_bp.route('/', methods=['GET'])
def list_users(): ...
@users_bp.route('/<int:user_id>', methods=['GET'])
def get_user(user_id): ...
# [Link] — application factory
from flask import Flask
from [Link] import users_bp
from [Link] import products_bp
def create_app(config='[Link]'):
app = Flask(__name__)
[Link].from_object(config)
app.register_blueprint(users_bp)
app.register_blueprint(products_bp)
return app
The Complete API Guide — Concepts - Python - Testing - Security Page 25
CHAPTER 6
Building APIs with FastAPI
Modern, fast, async — with automatic validation and documentation
6.1 Why FastAPI?
FastAPI is a modern Python web framework designed specifically for building APIs. Created by Sebastián
Ramírez, it combines Python type hints with Pydantic for automatic validation and Starlette for async
performance. It auto-generates interactive API documentation (Swagger UI and ReDoc) from your code — no
extra config needed.
FastAPI Advantages Flask vs FastAPI
FastAPI Strengths Flask vs FastAPI
■ Fastest Python framework (on par with ■ Flask: mature, huge ecosystem, synchronous
[Link]) by default
■ Automatic request validation via Pydantic ■ FastAPI: newer, built for async, more
opinionated
■ Auto-generated Swagger UI + ReDoc docs
■ Use Flask for: simple apps, legacy codebases
■ Full async/await support
■ Use FastAPI for: high throughput, new
■ Type hints = editor autocomplete everywhere
projects, type safety
■ Native dependency injection system
■ Both are production-grade and widely used
The Complete API Guide — Concepts - Python - Testing - Security Page 26
Python - FastAPI Setup and Pydantic Models
pip install fastapi uvicorn[standard] pydantic
# [Link] — Complete FastAPI application
from fastapi import FastAPI, HTTPException, Path, Query, Body
from pydantic import BaseModel, EmailStr, Field, validator
from typing import Optional, List
from datetime import datetime
import uuid
app = FastAPI(
title='My Users API',
description='A full-featured REST API for user management',
version='1.0.0',
docs_url='/docs', # Swagger UI at /docs
redoc_url='/redoc', # ReDoc at /redoc
)
# ■■ Pydantic Models (schemas) ■■■■■■■■■■■■■■■■■■■■■■■■
class UserCreate(BaseModel): # For request body
name: str = Field(..., min_length=2, max_length=100, example='Alice')
email: EmailStr = Field(..., example='alice@[Link]')
age: Optional[int] = Field(None, ge=0, le=150)
@validator('name')
def name_must_not_be_empty(cls, v):
if not [Link]():
raise ValueError('Name cannot be empty or whitespace')
return [Link]()
class UserResponse(BaseModel): # For response
id: str
name: str
email: str
age: Optional[int]
created_at: datetime
class Config:
from_attributes = True # Allow ORM models as input
The Complete API Guide — Concepts - Python - Testing - Security Page 27
Python - FastAPI — Complete CRUD API
# In-memory store
db: dict[str, dict] = {}
# ■■ GET /users — list users with pagination & filtering ■
@[Link]('/users', response_model=List[UserResponse])
async def list_users(
page: int = Query(1, ge=1, description='Page number'),
per_page: int = Query(20, ge=1, le=100, description='Items per page'),
search: str = Query(None, description='Search by name'),
):
items = list([Link]())
if search:
items = [u for u in items if [Link]() in u['name'].lower()]
start = (page - 1) * per_page
return items[start : start + per_page]
# ■■ POST /users — create user ■■■■■■■■■■■■■■■■■■■■■■■■■■■
@[Link]('/users', response_model=UserResponse, status_code=201)
async def create_user(user: UserCreate):
# Pydantic already validated the request body!
# If name is too short or email is invalid, 422 is returned automatically.
new_user = {
'id': str(uuid.uuid4()),
'name': [Link],
'email': [Link],
'age': [Link],
'created_at': [Link](),
}
db[new_user['id']] = new_user
return new_user
# ■■ GET /users/{user_id} ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
@[Link]('/users/{user_id}', response_model=UserResponse)
async def get_user(user_id: str = Path(..., description='UUID of the user')):
if user_id not in db:
raise HTTPException(status_code=404, detail=f'User {user_id} not found')
return db[user_id]
# ■■ PATCH /users/{user_id} ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
class UserUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=2)
age: Optional[int] = Field(None, ge=0, le=150)
@[Link]('/users/{user_id}', response_model=UserResponse)
async def update_user(user_id: str, updates: UserUpdate):
if user_id not in db:
raise HTTPException(status_code=404, detail='User not found')
user = db[user_id]
# Only update fields that were explicitly sent
update_data = [Link](exclude_unset=True)
[Link](update_data)
return user
# ■■ DELETE /users/{user_id} ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
@[Link]('/users/{user_id}', status_code=204)
async def delete_user(user_id: str):
if user_id not in db:
raise HTTPException(status_code=404, detail='User not found')
del db[user_id]
# Run with: uvicorn main:app --reload
# Then open: [Link] ← interactive Swagger UI
The Complete API Guide — Concepts - Python - Testing - Security Page 28
6.2 Dependency Injection in FastAPI
Python - FastAPI Dependency Injection
from fastapi import Depends, Header, HTTPException
# A dependency is just a function
async def verify_api_key(x_api_key: str = Header(...)):
"""Require a valid API key in the X-Api-Key header"""
if x_api_key != 'secret-key-123':
raise HTTPException(status_code=401, detail='Invalid API key')
return x_api_key
async def get_current_user(token: str = Header(...)):
"""Decode token and return user — shared across endpoints"""
user = decode_jwt(token) # hypothetical function
if not user:
raise HTTPException(status_code=401, detail='Invalid token')
return user
# Inject into routes with Depends()
@[Link]('/protected')
async def protected_route(
current_user = Depends(get_current_user), # runs first
_ = Depends(verify_api_key), # also runs
):
return {'message': f'Hello {current_user["name"]}'}
# Dependencies can be nested — FastAPI builds a dependency graph
# and handles caching, errors, and async properly.
The Complete API Guide — Concepts - Python - Testing - Security Page 29
CHAPTER 7
Consuming APIs with Python
The requests library and async httpx — your tools for calling any API
7.1 The requests Library
The requests library is the de-facto standard for making HTTP requests in Python. It wraps Python's low-level
urllib with a clean, human-friendly API. Nearly every Python developer uses it for consuming REST APIs.
Python · requests Library CRUD
pip install requests
import requests
BASE = '[Link]
# ■■ GET — Read data ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
r = [Link](f'{BASE}/posts/1')
r.raise_for_status() # raises exception on 4xx/5xx
post = [Link]() # parse JSON to dict
# ■■ POST — Send JSON body ■■■■■■■■■■■■■■■■■■■■■■■■■■■■
new_post = {'title': 'My Post', 'body': 'Content here', 'userId': 1}
r = [Link](f'{BASE}/posts', json=new_post) # json= auto-sets Content-Type
print(r.status_code) # 201
print([Link]()['id']) # 101 (new id from server)
# ■■ PUT — Replace entire resource ■■■■■■■■■■■■■■■■■■■
updated = {'id': 1, 'title': 'Updated', 'body': 'New body', 'userId': 1}
r = [Link](f'{BASE}/posts/1', json=updated)
# ■■ PATCH — Update specific fields ■■■■■■■■■■■■■■■■■■
r = [Link](f'{BASE}/posts/1', json={'title': 'Just the title changed'})
# ■■ DELETE — Remove resource ■■■■■■■■■■■■■■■■■■■■■■■■■
r = [Link](f'{BASE}/posts/1')
print(r.status_code) # 200 (jsonplaceholder quirk, usually 204)
7.2 Sessions, Headers & Authentication
The Complete API Guide — Concepts - Python - Testing - Security Page 30
Python · requests Sessions & Headers
import requests
# Sessions reuse the underlying TCP connection → faster for many requests
# They also persist headers, cookies, and auth across all requests
session = [Link]()
# Set headers once — applied to every request
[Link]({
'Authorization': 'Bearer YOUR_TOKEN',
'Accept': 'application/json',
'X-App-Version': '2.0.0',
})
# All these requests send the headers automatically
users = [Link]('[Link]
posts = [Link]('[Link]
[Link]('[Link] json={'text': 'Great!'})
[Link]() # or use as context manager:
with [Link]() as s:
[Link]['Authorization'] = 'Bearer TOKEN'
data = [Link]('[Link]
# Auth helpers built into requests
r = [Link](url, auth=('username', 'password')) # HTTP Basic Auth
r = [Link](url, auth=('api_key', '')) # API key as username
7.3 Timeouts, Retries & Error Handling
The Complete API Guide — Concepts - Python - Testing - Security Page 31
Python · Retries, Timeouts & Error Handling
import requests
from [Link] import HTTPAdapter
from [Link] import Retry
def make_session_with_retries(total=3, backoff_factor=1.0):
"""Create a session that auto-retries on failures"""
session = [Link]()
retry_strategy = Retry(
total=total, # max retry attempts
backoff_factor=backoff_factor, # wait 1s, 2s, 4s between retries
status_forcelist=[429, 500, 502, 503, 504], # retry on these codes
allowed_methods=['GET', 'POST', 'PUT'],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
[Link]('[Link] adapter)
[Link]('[Link] adapter)
return session
# Always set timeouts! Never let a request hang forever.
# timeout=(connect_timeout, read_timeout)
try:
session = make_session_with_retries()
response = [Link](
'[Link]
timeout=(3.05, 27) # 3s to connect, 27s to read
)
response.raise_for_status() # raise for 4xx / 5xx
data = [Link]()
except [Link]:
print('Request timed out')
except [Link]:
print('Network error — could not connect')
except [Link] as e:
print(f'HTTP error: {[Link].status_code} — {[Link]}')
except [Link] as e:
print(f'Unexpected error: {e}')
7.4 Async HTTP with httpx
The Complete API Guide — Concepts - Python - Testing - Security Page 32
Python · Async HTTP with httpx
pip install httpx
import asyncio, httpx
# httpx is the async alternative to requests
# Its API is nearly identical but supports async/await
async def fetch_user(client: [Link], user_id: int) -> dict:
response = await [Link](f'[Link]
response.raise_for_status()
return [Link]()
async def fetch_all_users_concurrently():
"""Fetch 10 users concurrently — much faster than sequential requests"""
async with [Link](
headers={'Authorization': 'Bearer TOKEN'},
timeout=[Link](10.0),
limits=[Link](max_connections=20),
) as client:
# Create tasks for all requests — they run concurrently
tasks = [fetch_user(client, i) for i in range(1, 11)]
users = await [Link](*tasks)
return users
# Run it
users = [Link](fetch_all_users_concurrently())
print(f'Fetched {len(users)} users concurrently!')
# Sequential (slow): 10 requests × 0.5s each = 5 seconds
# Concurrent (fast): all 10 requests in ~0.5s
The Complete API Guide — Concepts - Python - Testing - Security Page 33
CHAPTER 8
Authentication & Authorisation
Who are you? What are you allowed to do? — The two pillars of API security
Authentication vs Authorisation
Authentication (AuthN) — Verifying WHO you are. 'Prove you are Alice.' → Login, API key, token.
Authorisation (AuthZ) — Determining WHAT you are allowed to do. 'Alice can read posts but not delete
users.' → Roles, permissions, scopes. Authentication must happen BEFORE authorisation.
8.1 API Keys — Simplest Form of Auth
An API key is a long random string that identifies the calling application. Simple to implement, but provides no
user-level identity — anyone with the key can use it. Best for server-to-server communication or public APIs with
rate limiting.
Python · API Key Authentication
# Sending an API key — 3 common patterns:
# Pattern 1: Query parameter (■ avoid — keys appear in logs)
GET [Link]
# Pattern 2: Request header (■ preferred)
GET [Link]
X-API-Key: abc123xyz
# Pattern 3: Bearer token in Authorization header
GET [Link]
Authorization: Bearer abc123xyz
# Flask API — validating API key from header
from flask import Flask, request, jsonify, abort
from functools import wraps
import secrets
VALID_KEYS = {'abc123xyz': 'client-app-1', 'def456uvw': 'client-app-2'}
def require_api_key(f):
@wraps(f)
def decorated(*args, **kwargs):
key = [Link]('X-API-Key')
if not key or key not in VALID_KEYS:
return jsonify({'error': 'Invalid or missing API key'}), 401
request.api_client = VALID_KEYS[key] # attach client name to request
return f(*args, **kwargs)
return decorated
@[Link]('/api/data')
@require_api_key
def get_data():
return jsonify({'data': '...', 'client': request.api_client})
8.2 JWT — JSON Web Tokens (Deep Dive)
JWT is the most widely used authentication mechanism for REST APIs. A JWT is a self-contained,
cryptographically signed token that carries claims (user ID, role, expiry) inside it. The server does NOT need to
store sessions — it just validates the token's signature.
The Complete API Guide — Concepts - Python - Testing - Security Page 34
JWT STRUCTURE — Three base64-encoded parts separated by dots
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← HEADER
.eyJzdWIiOiI0MiIsIm5hbWUiOiJBbGljZSIs ← PAYLOAD (claims)
ImV4cCI6MTcwOTc0MDgwMH0
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV ← SIGNATURE
HEADER (decoded): {"alg": "HS256", "typ": "JWT"}
PAYLOAD (decoded): {
"sub": "42", ← subject (user ID)
"name": "Alice",
"role": "admin",
"exp": 1709740800, ← expiry timestamp
"iat": 1709654400 ← issued-at timestamp
}
SIGNATURE: HMAC-SHA256(base64(header) + '.' + base64(payload), SECRET_KEY)
■ Anyone can READ the payload (it's base64, not encrypted)
■ Only the server can CREATE valid tokens (needs SECRET_KEY)
■ NEVER store sensitive data (password, credit card) in JWT payload
Figure 8.1 — JWT Structure Explained
The Complete API Guide — Concepts - Python - Testing - Security Page 35
Python · JWT Authentication — Flask Implementation
pip install PyJWT
import jwt, datetime
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET_KEY = 'your-very-secret-key-change-in-production'
def generate_token(user_id: int, role: str) -> str:
"""Create a JWT that expires in 1 hour"""
payload = {
'sub': str(user_id),
'role': role,
'iat': [Link](),
'exp': [Link]() + [Link](hours=1),
}
return [Link](payload, SECRET_KEY, algorithm='HS256')
def decode_token(token: str) -> dict:
"""Validate signature and decode — raises exception on failure"""
return [Link](token, SECRET_KEY, algorithms=['HS256'])
# ■■ Login endpoint — issue token ■■■■■■■■■■■■■■■■■■■■■■
@[Link]('/api/auth/login', methods=['POST'])
def login():
data = request.get_json()
# In real apps: look up user in DB and verify hashed password
if [Link]('email') == 'alice@[Link]' and [Link]('password') == 'secret':
token = generate_token(user_id=42, role='admin')
return jsonify({'access_token': token, 'token_type': 'bearer'})
return jsonify({'error': 'Invalid credentials'}), 401
# ■■ Protected endpoint — verify token ■■■■■■■■■■■■■■■■■
def jwt_required(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = [Link]('Authorization', '')
if not [Link]('Bearer '):
return jsonify({'error': 'Missing token'}), 401
token = [Link](' ')[1]
try:
payload = decode_token(token)
request.current_user = payload
except [Link]:
return jsonify({'error': 'Token expired'}), 401
except [Link]:
return jsonify({'error': 'Invalid token'}), 401
return f(*args, **kwargs)
return decorated
@[Link]('/api/me')
@jwt_required
def get_profile():
return jsonify({'user_id': request.current_user['sub'],
'role': request.current_user['role']})
8.3 OAuth 2.0 — The Delegated Auth Standard
OAuth 2.0 allows users to grant third-party apps access to their data WITHOUT sharing their password. 'Sign in
with Google' uses OAuth 2.0. The app never sees your Google password — Google issues an access token after
the user consents.
OAuth 2.0 Authorization Code Flow:
The Complete API Guide — Concepts - Python - Testing - Security Page 36
User clicks 'Login with Google'
■
▼
Your App redirects to: [Link]
?client_id=YOUR_ID&redirect_uri=[Link]
&scope=email+profile&response_type=code
■
▼
User sees Google consent screen → clicks ALLOW
■
▼
Google redirects to: [Link]
■
▼
Your server exchanges code for tokens (server-to-server, no user involved):
POST [Link]
{code, client_id, client_secret, redirect_uri}
→ {access_token, refresh_token, expires_in}
■
▼
Your server uses access_token to call Google APIs on behalf of the user
Figure 8.2 — OAuth 2.0 Authorization Code Flow
Python · OAuth 2.0 with Google (Authlib)
# OAuth 2.0 with Authlib (the best Python OAuth library)
pip install authlib flask
from flask import Flask, redirect, url_for, session
from [Link].flask_client import OAuth
app = Flask(__name__)
app.secret_key = 'FLASK_SECRET'
oauth = OAuth(app)
google = [Link](
name='google',
client_id='YOUR_GOOGLE_CLIENT_ID',
client_secret='YOUR_GOOGLE_CLIENT_SECRET',
server_metadata_url='[Link]
client_kwargs={'scope': 'openid email profile'},
)
@[Link]('/login')
def login():
redirect_uri = url_for('callback', _external=True)
return google.authorize_redirect(redirect_uri)
@[Link]('/callback')
def callback():
token = google.authorize_access_token() # Exchange code for token
user = [Link]() # Call Google API
session['user'] = user # Store in session
return f'Logged in as {user["email"]}'
The Complete API Guide — Concepts - Python - Testing - Security Page 37
CHAPTER 9
API Security
Protecting your API from attacks, abuse, and data breaches
9.1 HTTPS — The Non-Negotiable Foundation
Always Use HTTPS
HTTPS (HTTP + TLS) encrypts all traffic between client and server. Without it, API keys, tokens, and user
data travel as plaintext — readable by anyone on the network. In 2026, there is no valid reason to run an
API over plain HTTP in production.
■ Use Let's Encrypt for free TLS certificates
■ Redirect all HTTP traffic to HTTPS
■ Set HTTP Strict Transport Security (HSTS) header
■ Use TLS 1.2 minimum, prefer TLS 1.3
9.2 CORS — Cross-Origin Resource Sharing
Browsers block JavaScript from making requests to a different domain than the current page. CORS headers tell
the browser which origins are permitted. This only applies to browser clients — Python scripts, mobile apps, and
servers are not restricted by CORS.
Python · CORS Configuration
pip install flask-cors
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
# Allow ALL origins (OK for public APIs, risky for private ones)
CORS(app)
# Allow only specific origins (production recommendation)
CORS(app, origins=['[Link] '[Link]
# Fine-grained per-route control
CORS(app, resources={
r'/api/public/*': {'origins': '*'},
r'/api/private/*': {'origins': '[Link]
'methods': ['GET', 'POST'],
'allow_headers': ['Authorization', 'Content-Type']},
})
# FastAPI CORS middleware
from [Link] import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=['[Link]
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
)
The Complete API Guide — Concepts - Python - Testing - Security Page 38
9.3 Rate Limiting
Python · Rate Limiting with Flask-Limiter
pip install flask-limiter redis
from flask import Flask, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
limiter = Limiter(
get_remote_address, # identify by client IP
app=app,
default_limits=['200/day', '50/hour', '10/minute'],
storage_uri='redis://localhost:6379', # persistent across restarts
)
@[Link]('/api/users')
@[Link]('100/hour') # override default for this endpoint
def list_users():
return jsonify({'users': []})
@[Link]('/api/auth/login', methods=['POST'])
@[Link]('5/minute') # very strict for login (prevent brute force)
def login():
...
# When rate limit exceeded → 429 Too Many Requests
# Response includes: Retry-After header with seconds to wait
@[Link](429)
def rate_limit_exceeded(e):
return jsonify({
'error': 'Rate limit exceeded',
'retry_after': [Link]
}), 429
9.4 Input Validation & SQL Injection Prevention
Python · Input Validation & SQL Injection Prevention
# SQL Injection — NEVER do this:
user_id = [Link]('id')
# ■ DANGEROUS — attacker sends id='1 OR 1=1' and gets ALL users
query = f'SELECT * FROM users WHERE id = {user_id}'
# ■ Safe — always use parameterised queries
[Link]('SELECT * FROM users WHERE id = %s', (user_id,))
# ■ Even better — use an ORM like SQLAlchemy
user = [Link](user_id) # automatically parameterised
# Input validation with Pydantic (FastAPI) or marshmallow (Flask)
from pydantic import BaseModel, validator, constr, conint
import re
class UserCreate(BaseModel):
# constr: constrained string — validates length and pattern
name: constr(min_length=2, max_length=100, strip_whitespace=True)
email: constr(regex=r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
age: conint(ge=0, le=150) # constrained int: 0 ≤ age ≤ 150
@validator('name')
def no_html_in_name(cls, v):
if [Link](r'<[^>]+>', v): # prevent XSS
raise ValueError('HTML not allowed in name')
return v
The Complete API Guide — Concepts - Python - Testing - Security Page 39
9.5 Security Headers Checklist
Python · Security Headers Middleware
# Add security headers to every Flask response
from flask import Flask
app = Flask(__name__)
@app.after_request
def add_security_headers(response):
# Prevent browsers from sniffing content type
[Link]['X-Content-Type-Options'] = 'nosniff'
# Prevent clickjacking
[Link]['X-Frame-Options'] = 'DENY'
# Force HTTPS
[Link]['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
# Restrict what resources can be loaded
[Link]['Content-Security-Policy'] = "default-src 'self'"
# Disable referrer header for privacy
[Link]['Referrer-Policy'] = 'no-referrer'
# Remove server identity
[Link]('Server', None)
return response
The Complete API Guide — Concepts - Python - Testing - Security Page 40
CHAPTER 10
Testing APIs
Automated testing — the safety net that lets you change code confidently
10.1 Why Test APIs?
Testing is how you prove your API works correctly — not just today, but after every future change. Without tests,
every code change is a gamble. With a good test suite, you can refactor freely, knowing the tests will catch
regressions immediately.
Test Type What it Tests Speed Tools
Unit Test One function in isolation, mocking ■ Very fast pytest, unittest
dependencies (ms)
Integration Test Multiple components working together ■ Fast pytest + real DB
(e.g., API + DB) (seconds)
End-to-End Test Full user flow from HTTP request to ■ Slower pytest + requests + running
database (seconds) server
Contract Test API contract matches documentation ■ Fast schemathesis, pact
Load Test Performance under concurrent traffic ■ Slow locust, k6
10.2 Testing Flask APIs with pytest
Python · pytest [Link]
pip install pytest pytest-cov
# tests/[Link] — shared test fixtures
import pytest
from app import create_app
@[Link](scope='session')
def app():
"""Create application configured for testing"""
app = create_app(config='[Link]')
return app
@[Link]()
def client(app):
"""Test client — makes HTTP requests without a real server"""
return app.test_client()
@[Link]()
def auth_headers():
"""Return headers with a valid JWT for protected endpoints"""
from [Link] import generate_token
token = generate_token(user_id=1, role='admin')
return {'Authorization': f'Bearer {token}'}
The Complete API Guide — Concepts - Python - Testing - Security Page 41
Python · Full pytest Test Suite for Users API
# tests/test_users.py — test the Users API
import pytest, json
class TestListUsers:
def test_returns_200(self, client):
r = [Link]('/api/v1/users')
assert r.status_code == 200
def test_returns_list(self, client):
r = [Link]('/api/v1/users')
data = r.get_json()
assert 'users' in data
assert isinstance(data['users'], list)
class TestCreateUser:
def test_creates_user_successfully(self, client):
payload = {'name': 'Alice', 'email': 'alice@[Link]'}
r = [Link]('/api/v1/users', json=payload)
assert r.status_code == 201
data = r.get_json()
assert data['name'] == 'Alice'
assert data['email'] == 'alice@[Link]'
assert 'id' in data
assert 'created_at' in data
def test_missing_name_returns_422(self, client):
r = [Link]('/api/v1/users', json={'email': 'alice@[Link]'})
assert r.status_code == 422
def test_invalid_email_returns_422(self, client):
r = [Link]('/api/v1/users', json={'name': 'Alice', 'email': 'not-an-email'})
assert r.status_code == 422
def test_missing_body_returns_400(self, client):
r = [Link]('/api/v1/users',
data='not json',
content_type='text/plain')
assert r.status_code == 400
class TestGetUser:
def test_get_existing_user(self, client):
# First create a user
r = [Link]('/api/v1/users', json={'name': 'Bob', 'email': 'bob@[Link]'})
user_id = r.get_json()['id']
# Then retrieve it
r = [Link](f'/api/v1/users/{user_id}')
assert r.status_code == 200
assert r.get_json()['name'] == 'Bob'
def test_get_nonexistent_user_returns_404(self, client):
r = [Link]('/api/v1/users/nonexistent-uuid')
assert r.status_code == 404
# Run tests:
# pytest tests/ -v --cov=app --cov-report=term-missing
10.3 Mocking External APIs
The Complete API Guide — Concepts - Python - Testing - Security Page 42
Python · Mocking External APIs in Tests
# When your code calls an external API, mock it in tests
# so tests are fast, reliable, and don't make real network calls
import pytest
from [Link] import patch, MagicMock
# Code under test — weather service that calls an external API
import requests
def get_weather(city: str) -> dict:
r = [Link](f'[Link]
params={'key': 'API_KEY'})
r.raise_for_status()
return [Link]()
# Test — mock the [Link] call
def test_get_weather_success():
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
'city': 'Seattle',
'temp': 12.4,
'condition': 'rainy',
}
with patch('[Link]', return_value=mock_response) as mock_get:
result = get_weather('Seattle')
mock_get.assert_called_once() # verify it was called
assert result['city'] == 'Seattle'
assert result['temp'] == 12.4
def test_get_weather_api_down():
"""Test that our code handles API errors gracefully"""
with patch('[Link]', side_effect=[Link]):
with [Link]([Link]):
get_weather('Seattle')
# For more complex scenarios, use the 'responses' library:
# pip install responses
import responses as resp
@[Link]
def test_with_responses_library():
[Link]([Link], '[Link]
json={'temp': 12.4}, status=200)
result = get_weather('Seattle')
assert result['temp'] == 12.4
The Complete API Guide — Concepts - Python - Testing - Security Page 43
CHAPTER 11
API Documentation
Good docs are the difference between an API people love and one they avoid
11.1 OpenAPI / Swagger Specification
The OpenAPI Specification (formerly Swagger) is an industry-standard format for describing REST APIs in a
machine-readable YAML or JSON file. From this single file, tools can auto-generate interactive documentation,
client SDKs, server stubs, and test cases.
YAML · OpenAPI Specification
# [Link] — OpenAPI 3.0 spec for the Users API
openapi: '3.0.3'
info:
title: Users API
description: |
Manage user accounts.
## Authentication
Use a Bearer token in the Authorization header.
version: '1.0.0'
contact:
name: API Support
email: api@[Link]
servers:
- url: [Link]
description: Production
- url: [Link]
description: Local development
paths:
/users:
get:
summary: List all users
operationId: listUsers
parameters:
- name: page
in: query
schema: { type: integer, default: 1 }
- name: per_page
in: query
schema: { type: integer, default: 20, maximum: 100 }
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
users: { type: array, items: { '$ref': '#/components/schemas/User' } }
total: { type: integer }
'401':
'$ref': '#/components/responses/Unauthorized'
The Complete API Guide — Concepts - Python - Testing - Security Page 44
11.2 Auto-Generated Docs in FastAPI
FastAPI generates OpenAPI documentation automatically from your code — type hints, Pydantic models,
docstrings, and Field descriptions all contribute to the docs. No separate spec file needed.
Python · Self-Documenting FastAPI
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Optional
app = FastAPI(
title='My API',
description='## Overview\nThis API manages users and products.',
version='2.0.0',
terms_of_service='[Link]
contact={'name': 'Dev Team', 'email': 'dev@[Link]'},
license_info={'name': 'MIT'},
)
class UserCreate(BaseModel):
name: str = Field(..., description='Full name', example='Alice Smith')
email: str = Field(..., description='Valid email', example='alice@[Link]')
role: str = Field('user', description='User role', enum=['user', 'admin'])
@[Link](
'/users',
response_model=UserResponse,
status_code=201,
summary='Create a new user',
description='Creates a new user account. Email must be unique.',
responses={
409: {'description': 'Email already exists'},
422: {'description': 'Validation error'},
}
)
async def create_user(user: UserCreate):
'''
Create a new user with the following information:
- **name**: Full name, 2-100 characters
- **email**: Valid email address, must be unique
- **role**: Either 'user' or 'admin'
'''
...
# Visit /docs → Swagger UI (try it interactively!)
# Visit /redoc → ReDoc (beautiful read-only docs)
# Visit /[Link] → raw OpenAPI spec
The Complete API Guide — Concepts - Python - Testing - Security Page 45
CHAPTER 12
GraphQL with Python
Flexible, client-driven queries — fetch exactly what you need
12.1 GraphQL Fundamentals
In GraphQL, the client writes a query that describes the exact shape of the data it wants. The server validates the
query against a schema and returns precisely that data. No under-fetching (missing data), no over-fetching (extra
unused data).
The Complete API Guide — Concepts - Python - Testing - Security Page 46
Python · GraphQL API with Strawberry
pip install strawberry-graphql[fastapi]
# A complete GraphQL API with Strawberry
import strawberry
from [Link] import GraphQLRouter
from fastapi import FastAPI
from typing import Optional, List
# ■■ Types (the GraphQL schema) ■■■■■■■■■■■■■■■■■■■■■■■■■
@[Link]
class Author:
id: int
name: str
@[Link]
class Book:
id: int
title: str
year: int
author: Author
# ■■ Fake data store ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
authors = {1: Author(id=1, name='Guido van Rossum')}
books = [
Book(id=1, title='Python Cookbook', year=2013, author=authors[1]),
Book(id=2, title='Fluent Python', year=2022, author=authors[1]),
]
# ■■ Queries (reads) ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
@[Link]
class Query:
@[Link]
def books(self) -> List[Book]:
return books
@[Link]
def book(self, id: int) -> Optional[Book]:
return next((b for b in books if [Link] == id), None)
# ■■ Mutations (writes) ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
@[Link]
class Mutation:
@[Link]
def add_book(self, title: str, year: int, author_id: int) -> Book:
author = [Link](author_id)
if not author:
raise ValueError(f'Author {author_id} not found')
book = Book(id=len(books)+1, title=title, year=year, author=author)
[Link](book)
return book
# ■■ FastAPI integration ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
schema = [Link](query=Query, mutation=Mutation)
graphql_app = GraphQLRouter(schema)
app = FastAPI()
app.include_router(graphql_app, prefix='/graphql')
# Visit /graphql for the GraphiQL interactive editor
The Complete API Guide — Concepts - Python - Testing - Security Page 47
Python · GraphQL Queries & Mutations
# GraphQL queries — sent as POST to /graphql
# Query 1: Get all books with only title and author name
query = '''
query {
books {
title
author { name }
}
}
'''
# → [{"title": "Python Cookbook", "author": {"name": "Guido van Rossum"}}]
# Query 2: Get a specific book by ID
query = '''
query GetBook($id: Int!) {
book(id: $id) {
id title year
author { id name }
}
}
'''
variables = {'id': 1}
# Mutation: Add a new book
mutation = '''
mutation AddBook($title: String!, $year: Int!, $authorId: Int!) {
addBook(title: $title, year: $year, authorId: $authorId) {
id title year
}
}
'''
import requests
r = [Link]('[Link]
json={'query': mutation, 'variables': {'title': 'New Book', 'year': 2026, 'authorId': 1}
})
print([Link]())
The Complete API Guide — Concepts - Python - Testing - Security Page 48
CHAPTER 13
WebSockets with Python
Real-time, bidirectional communication — push data the moment it changes
13.1 How WebSockets Work
HTTP is request-response: the client asks, the server answers, and the connection closes. WebSockets start as
an HTTP request, then upgrade to a persistent, bidirectional connection. Either side can send data at any time
without the other initiating.
HTTP (polling — client asks repeatedly): WebSocket (push — server sends when ready):
Client → 'Any new messages?' Server Client ← Server: 'New message!'
Server → 'No' Client ← Server: 'User joined'
Client → 'Any new messages?' Server Client → Server: 'Send: Hello!'
Server → 'No' Client ← Server: 'Alice: Hello!'
Client → 'Any new messages?' Server (one connection, always open)
Server → 'Yes! Here it is'
(wasted bandwidth, high latency)
WebSocket handshake:
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
→ HTTP 101 Switching Protocols (connection now upgraded)
Figure 13.1 — HTTP Polling vs WebSocket Push
The Complete API Guide — Concepts - Python - Testing - Security Page 49
Python · WebSocket Chat with FastAPI
pip install fastapi uvicorn websockets
# WebSocket chat server with FastAPI
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import List
app = FastAPI()
class ConnectionManager:
"""Manages all active WebSocket connections"""
def __init__(self):
[Link]: List[WebSocket] = []
async def connect(self, ws: WebSocket):
await [Link]()
[Link](ws)
def disconnect(self, ws: WebSocket):
[Link](ws)
async def broadcast(self, message: str):
"""Send a message to ALL connected clients"""
for ws in [Link]:
await ws.send_text(message)
manager = ConnectionManager()
@[Link]('/ws/{username}')
async def websocket_endpoint(ws: WebSocket, username: str):
await [Link](ws)
await [Link](f'■ {username} joined the chat')
try:
while True: # Keep connection alive
msg = await ws.receive_text() # Wait for client message
await [Link](f'{username}: {msg}') # Broadcast to all
except WebSocketDisconnect:
[Link](ws)
await [Link](f'■ {username} left the chat')
# Python WebSocket client
import asyncio, websockets
async def chat_client():
uri = '[Link]
async with [Link](uri) as ws:
await [Link]('Hello everyone!')
response = await [Link]()
print(response) # 'Alice: Hello everyone!'
[Link](chat_client())
The Complete API Guide — Concepts - Python - Testing - Security Page 50
CHAPTER 14
Webhooks
Event-driven integration — let services come to you
14.1 Receiving Webhooks in Flask
Python · Receiving & Verifying Webhooks
import hmac, hashlib, json
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
WEBHOOK_SECRET = 'your-webhook-secret'
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
"""Verify the webhook came from the expected sender"""
expected = [Link](
[Link](), payload, hashlib.sha256
).hexdigest()
# Use hmac.compare_digest to prevent timing attacks
return hmac.compare_digest(f'sha256={expected}', signature)
@[Link]('/webhooks/github', methods=['POST'])
def github_webhook():
# 1. Verify the signature (ALWAYS do this!)
signature = [Link]('X-Hub-Signature-256', '')
if not verify_signature([Link], signature, WEBHOOK_SECRET):
abort(401, 'Invalid webhook signature')
# 2. Parse the event type
event = [Link]('X-GitHub-Event')
payload = request.get_json()
# 3. Handle the event
if event == 'push':
branch = payload['ref'].split('/')[-1]
committer = payload['pusher']['name']
commits = len(payload['commits'])
print(f'{committer} pushed {commits} commit(s) to {branch}')
# Trigger your CI/CD, send notifications, etc.
elif event == 'pull_request':
action = payload['action'] # opened, closed, merged
pr_title = payload['pull_request']['title']
print(f'PR {action}: {pr_title}')
# 4. Respond quickly — process async if needed
return jsonify({'status': 'received'}), 200 # Must respond within 10 seconds
The Complete API Guide — Concepts - Python - Testing - Security Page 51
CHAPTER 15
Rate Limiting, Caching & Pagination
Building APIs that are fast, fair, and efficient at scale
15.1 Response Caching with ETags
Python · ETag Caching
import hashlib, json
from flask import request, jsonify, make_response
def etag_for(data: dict) -> str:
"""Generate a unique fingerprint for a response body"""
content = [Link](data, sort_keys=True)
return hashlib.md5([Link]()).hexdigest()
@[Link]('/api/v1/users/<user_id>')
def get_user(user_id):
user = fetch_user_from_db(user_id) # database call
if not user:
return jsonify({'error': 'Not found'}), 404
etag = etag_for(user)
# Client sends If-None-Match with the ETag from previous response
if [Link]('If-None-Match') == etag:
return '', 304 # Not Modified — client uses cached version
response = make_response(jsonify(user))
[Link]['ETag'] = etag
[Link]['Cache-Control'] = 'private, max-age=300' # 5 min
return response
# First request: GET /users/42
# Response: 200 + user data + ETag: '33a64df5'
# Second request: GET /users/42 + If-None-Match: '33a64df5'
# If unchanged: 304 Not Modified (no body! saves bandwidth)
# If changed: 200 + new user data + new ETag
15.2 Redis Caching
The Complete API Guide — Concepts - Python - Testing - Security Page 52
Python · Redis Response Caching Decorator
pip install redis
import redis, json, functools
cache = [Link](host='localhost', port=6379, decode_responses=True)
def cached(ttl_seconds=300):
"""Decorator: cache function results in Redis"""
def decorator(f):
@[Link](f)
def wrapper(*args, **kwargs):
key = f'cache:{f.__name__}:{args}:{kwargs}'
# Try to get from cache
cached_result = [Link](key)
if cached_result:
return [Link](cached_result)
# Cache miss — call the function
result = f(*args, **kwargs)
[Link](key, ttl_seconds, [Link](result))
return result
return wrapper
return decorator
@cached(ttl_seconds=60) # cache for 1 minute
def get_user_stats(user_id: int) -> dict:
# Expensive database aggregation — only runs on cache miss
return [Link]('SELECT COUNT(*) ... expensive query ...').fetchone()
# Invalidate cache when data changes
def invalidate_user_cache(user_id: int):
pattern = f'cache:get_user_stats:({user_id},)*'
for key in cache.scan_iter(pattern):
[Link](key)
The Complete API Guide — Concepts - Python - Testing - Security Page 53
CHAPTER 16
Deployment & Production
Getting your API from your laptop to the world
16.1 Production Server — Gunicorn + Uvicorn
Shell · Production Server Commands
pip install gunicorn uvicorn[standard]
# Flask — production server with Gunicorn
gunicorn 'app:create_app()' \
--workers 4 \
--bind [Link]:5000 \
--timeout 30 \
--access-logfile - \
--error-logfile -
# FastAPI — production server with Uvicorn + Gunicorn workers
gunicorn main:app \
--worker-class [Link] \
--workers 4 \
--bind [Link]:8000
# How many workers? Rule of thumb: 2 × CPU_cores + 1
# 4-core machine → 9 workers
# For IO-bound APIs: more workers are fine
# For CPU-bound: don't exceed core count
16.2 Dockerizing a Python API
Dockerfile · Production FastAPI Container
# Dockerfile — production-ready FastAPI container
FROM python:3.12-slim AS base
# Security: run as non-root user
RUN groupadd -r app && useradd -r -g app app
WORKDIR /app
# Dependencies layer (cached unless [Link] changes)
COPY [Link] .
RUN pip install --no-cache-dir -r [Link]
# Application code
COPY . .
RUN chown -R app:app /app
USER app
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -f [Link] || exit 1
CMD ["gunicorn", "main:app",
"--worker-class", "[Link]",
"--workers", "4",
"--bind", "[Link]:8000",
"--timeout", "30"]
The Complete API Guide — Concepts - Python - Testing - Security Page 54
YAML · Docker Compose Full Stack
# [Link] — full stack: API + DB + Redis + Nginx
version: '3.9'
services:
api:
build: .
environment:
DATABASE_URL: postgresql://user:pass@db:5432/myapi
REDIS_URL: redis://cache:6379
SECRET_KEY: ${SECRET_KEY}
ENVIRONMENT: production
depends_on: [db, cache]
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapi
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
volumes: [pgdata:/var/lib/postgresql/data]
cache:
image: redis:7-alpine
nginx:
image: nginx:alpine
ports: ['80:80', '443:443']
volumes:
- ./[Link]:/etc/nginx/[Link]
- certs:/etc/letsencrypt
depends_on: [api]
volumes: [pgdata:, certs:]
The Complete API Guide — Concepts - Python - Testing - Security Page 55
CHAPTER 17
Real-World Project — Complete Todo
API
A fully-featured API: design, implement, test, and run with Docker
17.1 API Design
Method URL Description Auth Required
POST /auth/register Create a new account ■ No
POST /auth/login Get access token ■ No
GET /todos List your todos (paginated) ■ Yes
POST /todos Create a new todo ■ Yes
GET /todos/{id} Get a specific todo ■ Yes
PATCH /todos/{id} Update a todo ■ Yes
DELETE /todos/{id} Delete a todo ■ Yes
GET /todos?done=true Filter by completion status ■ Yes
GET /health Health check (for monitoring) ■ No
17.2 Complete FastAPI Todo App
The Complete API Guide — Concepts - Python - Testing - Security Page 56
Python · FastAPI Todo — Database Models
# [Link] — Complete Todo API (FastAPI + SQLite via SQLAlchemy)
from fastapi import FastAPI, Depends, HTTPException, Query
from [Link] import CORSMiddleware
from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime
from [Link] import declarative_base, sessionmaker, Session
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional, List
import jwt, hashlib, secrets
# ■■ Database setup ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
engine = create_engine('sqlite:///./[Link]', connect_args={'check_same_thread': False})
SessionLocal = sessionmaker(bind=engine)
Base = declarative_base()
class UserModel(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
email = Column(String, unique=True, index=True)
password = Column(String) # stored as SHA-256 hash
created_at = Column(DateTime, default=[Link])
class TodoModel(Base):
__tablename__ = 'todos'
id = Column(Integer, primary_key=True)
title = Column(String)
done = Column(Boolean, default=False)
user_id = Column(Integer)
created_at = Column(DateTime, default=[Link])
[Link].create_all(bind=engine) # Create tables on startup
The Complete API Guide — Concepts - Python - Testing - Security Page 57
Python · FastAPI Todo — Schemas & Dependencies
# ■■ Pydantic schemas ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
class RegisterRequest(BaseModel):
email: str = Field(..., example='alice@[Link]')
password: str = Field(..., min_length=8)
class TodoCreate(BaseModel):
title: str = Field(..., min_length=1, max_length=500)
class TodoUpdate(BaseModel):
title: Optional[str] = Field(None, max_length=500)
done: Optional[bool] = None
class TodoResponse(BaseModel):
id: int; title: str; done: bool; created_at: datetime
class Config: from_attributes = True
# ■■ App & dependencies ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
SECRET = 'change-me-in-production'
app = FastAPI(title='Todo API', version='1.0.0')
app.add_middleware(CORSMiddleware, allow_origins=['*'],
allow_methods=['*'], allow_headers=['*'])
def get_db():
db = SessionLocal()
try: yield db
finally: [Link]()
def current_user(token: str = '', db: Session = Depends(get_db)) -> UserModel:
try:
payload = [Link](token, SECRET, algorithms=['HS256'])
user = [Link](UserModel).get(int(payload['sub']))
if not user: raise HTTPException(401, 'User not found')
return user
except [Link]:
raise HTTPException(401, 'Invalid token')
The Complete API Guide — Concepts - Python - Testing - Security Page 58
Python · FastAPI Todo — Complete Routes
# ■■ Routes ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
def hash_pw(pw): return hashlib.sha256([Link]()).hexdigest()
@[Link]('/health')
def health(): return {'status': 'ok', 'time': [Link]()}
@[Link]('/auth/register', status_code=201)
def register(req: RegisterRequest, db: Session = Depends(get_db)):
if [Link](UserModel).filter_by(email=[Link]).first():
raise HTTPException(409, 'Email already registered')
user = UserModel(email=[Link], password=hash_pw([Link]))
[Link](user); [Link](); [Link](user)
return {'id': [Link], 'email': [Link]}
@[Link]('/auth/login')
def login(req: RegisterRequest, db: Session = Depends(get_db)):
user = [Link](UserModel).filter_by(
email=[Link], password=hash_pw([Link])).first()
if not user: raise HTTPException(401, 'Invalid credentials')
token = [Link]({'sub': str([Link])}, SECRET, algorithm='HS256')
return {'access_token': token, 'token_type': 'bearer'}
@[Link]('/todos', response_model=List[TodoResponse])
def list_todos(done: Optional[bool]=None, page: int=Query(1,ge=1),
db: Session=Depends(get_db), user: UserModel=Depends(current_user)):
q = [Link](TodoModel).filter_by(user_id=[Link])
if done is not None: q = q.filter_by(done=done)
return [Link]((page-1)*20).limit(20).all()
@[Link]('/todos', response_model=TodoResponse, status_code=201)
def create_todo(todo: TodoCreate, db=Depends(get_db), user=Depends(current_user)):
t = TodoModel(title=[Link], user_id=[Link])
[Link](t); [Link](); [Link](t); return t
@[Link]('/todos/{todo_id}', response_model=TodoResponse)
def update_todo(todo_id: int, updates: TodoUpdate,
db=Depends(get_db), user=Depends(current_user)):
t = [Link](TodoModel).filter_by(id=todo_id, user_id=[Link]).first()
if not t: raise HTTPException(404, 'Todo not found')
if [Link] is not None: [Link] = [Link]
if [Link] is not None: [Link] = [Link]
[Link](); [Link](t); return t
@[Link]('/todos/{todo_id}', status_code=204)
def delete_todo(todo_id: int, db=Depends(get_db), user=Depends(current_user)):
t = [Link](TodoModel).filter_by(id=todo_id, user_id=[Link]).first()
if not t: raise HTTPException(404, 'Todo not found')
[Link](t); [Link]()
The Complete API Guide — Concepts - Python - Testing - Security Page 59
CHAPTER 18
Best Practices & Patterns
Design principles that make APIs maintainable, predictable, and developer-friendly
18.1 Error Handling — Consistent Format
Every error your API returns should follow the same JSON structure. This lets clients handle errors with a single
code path, regardless of what went wrong.
Python · Consistent Error Response Format
# Standard error response format
{
"error": {
"code": "VALIDATION_ERROR", # machine-readable error code
"message": "Validation failed", # human-readable summary
"details": [ # optional — field-level errors
{"field": "email", "issue": "Invalid email format"},
{"field": "age", "issue": "Must be between 0 and 150"}
],
"request_id": "d4e8f2a1-9b3c-4d5e" # for support/tracing
}
}
# FastAPI — custom exception handler
from fastapi import Request
from [Link] import JSONResponse
from [Link] import RequestValidationError
@app.exception_handler(RequestValidationError)
async def validation_error_handler(request: Request, exc: RequestValidationError):
return JSONResponse(status_code=422, content={
'error': {
'code': 'VALIDATION_ERROR',
'message': 'Request validation failed',
'details': [{'field': '.'.join(str(l) for l in e['loc']),
'issue': e['msg']} for e in [Link]()],
}
})
18.2 Idempotency Keys
For operations that must not be executed twice (payment, email send), clients can send an Idempotency-Key
header. If the same key is seen again, the server returns the original response without re-executing the operation.
Stripe uses this pattern extensively.
The Complete API Guide — Concepts - Python - Testing - Security Page 60
Python · Idempotency Keys
import redis
cache = [Link](decode_responses=True)
@[Link]('/api/payments', methods=['POST'])
def create_payment():
idempotency_key = [Link]('Idempotency-Key')
if idempotency_key:
# Check if we've seen this key before
cached = [Link](f'idem:{idempotency_key}')
if cached:
import json
return jsonify([Link](cached)), 200 # Return original response
# Process the payment (expensive, must not run twice)
result = process_payment(request.get_json())
if idempotency_key:
# Store result for 24 hours
[Link](f'idem:{idempotency_key}', 86400, [Link](result))
return jsonify(result), 201
18.3 The 15 Rules of Good API Design
■ 1. Use nouns not verbs in URLs — GET /users not GET /getUsers
■ 2. Use plural nouns for collections — /users not /user
■ 3. Version your API from day one — /api/v1/ — you will need it eventually
■ 4. Return consistent error objects — Same JSON structure for every error
■ 5. Use HTTP status codes correctly — 201 Created, 204 No Content, 422 Validation Error
■ 6. Never expose internal details — No stack traces, no DB schema info, no internal IDs in errors
■ 7. Paginate all list endpoints — Never return unbounded collections
■ 8. Filter/sort via query params — GET /users?status=active&sort;=name
■ 9. Use HTTPS always — No exceptions. Ever.
■ 10. Document everything — Every endpoint, field, and error code
■ 11. Validate all input — Never trust client data
■ 12. Use tokens, not sessions — JWT/OAuth for stateless scalable auth
■ 13. Rate limit all endpoints — Protect against abuse and DoS
■ 14. Log requests with a request ID — Essential for debugging production issues
■ 15. Design for your client's needs — Ask 'what does the client need?' before designing endpoints
The Complete API Guide — Concepts - Python - Testing - Security Page 61
CHAPTER 19
Django REST Framework
The batteries-included approach to building APIs in Django
19.1 DRF Fundamentals
Python · Django REST Framework Complete Example
pip install djangorestframework djangorestframework-simplejwt
# [Link]
INSTALLED_APPS = ['rest_framework', 'api', ...]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.[Link]',
],
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.[Link]'],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.[Link]',
'PAGE_SIZE': 20,
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
}
# [Link]
from [Link] import models
class Todo([Link]):
title = [Link](max_length=500)
done = [Link](default=False)
user = [Link]('[Link]', on_delete=[Link])
created = [Link](auto_now_add=True)
# [Link]
from rest_framework import serializers
class TodoSerializer([Link]):
class Meta:
model = Todo
fields = ['id', 'title', 'done', 'created']
read_only_fields = ['id', 'created']
# [Link] — ModelViewSet gives full CRUD in ~10 lines!
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticated
class TodoViewSet(ModelViewSet):
serializer_class = TodoSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return [Link](user=[Link])
def perform_create(self, serializer):
[Link](user=[Link]) # auto-set user
# [Link]
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
[Link]('todos', TodoViewSet, basename='todo')
# Generates automatically: GET/POST /todos/, GET/PUT/PATCH/DELETE /todos/{id}/
The Complete API Guide — Concepts - Python - Testing - Security Page 62
CHAPTER 20
Cheat Sheet & Quick Reference
Everything you need at a glance
20.1 HTTP Methods Quick Reference
Method Safe Idempote Use For Example
nt
GET ■ ■ Read resource or list GET /users/42
POST ■ ■ Create resource POST /users
PUT ■ ■ Replace whole resource PUT /users/42
PATCH ■ ■ Update specific fields PATCH /users/42
DELETE ■ ■ Delete resource DELETE /users/42
HEAD ■ ■ Check resource exists HEAD /users/42
OPTIONS ■ ■ List allowed methods / CORS OPTIONS /users
20.2 Status Code Reference
Code Name When to Use
200 OK Successful GET, PATCH, PUT — body contains result
201 Created POST succeeded — include Location header with URL of new resource
204 No Content DELETE succeeded — no body
400 Bad Request Malformed request syntax, invalid JSON
401 Unauthorized Missing or invalid authentication credentials
403 Forbidden Authenticated but lacks permission
404 Not Found Resource doesn't exist
409 Conflict Duplicate resource (email already registered)
422 Unprocessable Entity Valid JSON but fails validation rules
429 Too Many Requests Rate limit exceeded — send Retry-After header
500 Internal Server Error Unexpected server crash
502 Bad Gateway Upstream service (DB, 3rd party API) failed
503 Service Unavailable Server temporarily down for maintenance
The Complete API Guide — Concepts - Python - Testing - Security Page 63
20.3 Python API Snippets
Python · Essential API Snippets
import requests, json
BASE = '[Link]
HEADERS = {'Authorization': 'Bearer TOKEN', 'Content-Type': 'application/json'}
# GET with query params
r = [Link](f'{BASE}/users', headers=HEADERS, params={'page': 2})
# POST JSON body
r = [Link](f'{BASE}/users', headers=HEADERS,
json={'name': 'Alice', 'email': 'a@[Link]'})
# PATCH partial update
r = [Link](f'{BASE}/users/42', headers=HEADERS, json={'name': 'Bob'})
# DELETE
r = [Link](f'{BASE}/users/42', headers=HEADERS)
# Handle response safely
try:
r.raise_for_status()
data = [Link]()
except [Link] as e:
print(f'{[Link].status_code}: {[Link]()}')
except [Link]:
print('Request timed out')
# Async (httpx)
import asyncio, httpx
async def main():
async with [Link](headers=HEADERS) as c:
r = await [Link](f'{BASE}/users')
return [Link]()
20.4 curl Quick Reference
Shell · curl API Testing Commands
# GET request
curl [Link]
# GET with headers
curl -H 'Authorization: Bearer TOKEN' [Link]
# POST JSON
curl -X POST [Link] \
-H 'Content-Type: application/json' \
-d '{"name": "Alice", "email": "alice@[Link]"}'
# PATCH
curl -X PATCH [Link] \
-H 'Content-Type: application/json' \
-d '{"name": "Bob"}'
# DELETE
curl -X DELETE -H 'Authorization: Bearer TOKEN' [Link]
# Useful flags:
# -v verbose (show request + response headers)
# -i include response headers in output
# -o [Link] save response body to file
# -w '%{http_code}' print status code at end
# -s silent (no progress bar)
# --max-time 10 set 10-second timeout
The Complete API Guide — Concepts - Python - Testing - Security Page 64
20.5 Common Troubleshooting Guide
Problem Likely Cause Solution
401 on every request Token missing, expired, or Check: 'Authorization: Bearer TOKEN' — note the space
wrong format and 'Bearer' prefix
403 Forbidden Wrong permissions/role Check the user's role or scope in your token payload
404 on valid resource Wrong URL, wrong ID, wrong Print the full URL you're calling. Check v1 vs v2 in path
version
422 Unprocessable Validation failure Read the 'details' array in the error response — it lists
exactly which field failed
CORS error in browser Missing Add flask-cors or CORS middleware. CORS is not a
Access-Control-Allow-Origin Python/requests issue
header
Timeout Server too slow or network Set explicit timeout in [Link](timeout=10). Check
issue server logs
JSON decode error Server returned HTML error Print [Link] before calling .json() to see raw
page not JSON response
SSL certificate error Self-signed cert in Add verify=False to requests (dev only!) or install the cert
development
429 Too Many Requests Rate limit hit Check Retry-After header, add exponential backoff in retry
logic
500 Internal Server Server code crashed Check server logs — the server has more details. Never
Error trust client-side 500 messages
End of The Complete API Guide
You now have everything you need to build, consume, secure,
test, document, and deploy production-ready APIs in Python. ■
Keep building — every great API started with a single endpoint.
The Complete API Guide — Concepts - Python - Testing - Security Page 65