7 API Design Patterns Every Developer Should Know (With Real Code Examples)
I've been building and consuming APIs for years, and I keep seeing the same mistakes repeated. Meanwhile, the developers who ship fast all use a handful of patterns that make their lives 10x easier.
Here are the 7 patterns I use on every project, with copy-paste code examples.
1. Paginated Collections (Cursor-Based)
Forget offset pagination for anything real. Cursor-based pagination is the standard for modern APIs.
@app.get("/api/items")
async def list_items(cursor: str = None, limit: int = 50):
query = db.query(Item).order_by(Item.created_at.desc())
if cursor:
cursor_time = base64.urlsafe_decode(cursor)
query = query.filter(Item.created_at < cursor_time)
items = query.limit(limit + 1).all()
next_cursor = None
if len(items) > limit:
next_cursor = base64.urlsafe_encode(str(items[-1].created_at))
items = items[:limit]
return {"items": items, "next_cursor": next_cursor}
Why: Offset pagination breaks when items are inserted/deleted between requests. Cursor-based is stable and performant.
2. Rate Limiting with Sliding Window
Fixed-window rate limits create a thundering herd at window boundaries. Sliding window is smoother.
import time
from collections import defaultdict
rate_limits = defaultdict(list)
def check_rate_limit(user_id: str, max_requests: int = 100, window: int = 3600):
now = time.time()
# Clean old entries
rate_limits[user_id] = [t for t in rate_limits[user_id] if now - t < window]
if len(rate_limits[user_id]) >= max_requests:
return False
rate_limits[user_id].append(now)
return True
Production tip: Use Redis sorted sets for this instead of in-memory storage.
3. Optimistic Concurrency with ETags
Prevent lost updates without locking.
@app.put("/api/items/{item_id}")
async def update_item(item_id: int, data: ItemUpdate, if_match: str):
item = db.get(Item, item_id)
current_etag = f'"{hash(item.to_dict())}"'
if if_match != current_etag:
raise HTTPException(409, "Item was modified by another request")
# Apply update
for key, value in data.dict(exclude_unset=True).items():
setattr(item, key, value)
db.commit()
return {"etag": current_etag, "item": item}
4. Bulk Operations with Batching
Individual API calls for bulk work are painfully slow.
@app.post("/api/items/bulk")
async def bulk_create(items: list[ItemCreate], batch_size: int = 100):
results = []
errors = []
for i in range(0, len(items), batch_size):
batch = items[i:i + batch_size]
try:
created = db.bulk_insert(Item, [item.dict() for item in batch])
results.extend(created)
except Exception as e:
errors.append({"batch": i // batch_size, "error": str(e)})
return {"created": len(results), "errors": errors}
5. Webhook Delivery with Retry
If you're sending webhooks, you need retries with exponential backoff.
import httpx
import asyncio
async def deliver_webhook(url: str, payload: dict, max_retries: int = 3):
for attempt in range(max_retries):
try:
response = httpx.post(url, json=payload, timeout=10)
if response.status_code < 500:
return {"status": "delivered", "code": response.status_code}
except httpx.TimeoutException:
pass
if attempt < max_retries - 1:
await asyncio.sleep(2 ** attempt) # 1s, 2s, 4s
return {"status": "failed", "attempts": max_retries}
6. Field Selection (Sparse Fieldsets)
Let clients request only the fields they need.
@app.get("/api/users/{user_id}")
async def get_user(user_id: int, fields: str = None):
user = db.get(User, user_id)
all_fields = user.to_dict()
if fields:
requested = set(fields.split(","))
return {k: v for k, v in all_fields.items() if k in requested}
return all_fields
Call: GET /api/users/42?fields=id,name,email → only returns those 3 fields.
7. Health Check with Dependency Status
A real health check tests actual dependencies, not just "I'm alive."
@app.get("/health")
async def health_check():
checks = {}
# Check database
try:
db.execute("SELECT 1")
checks["database"] = "ok"
except Exception as e:
checks["database"] = f"error: {str(e)[:50]}"
# Check Redis
try:
redis.ping()
checks["cache"] = "ok"
except Exception:
checks["cache"] = "unavailable"
status = 200 if all(v == "ok" for v in checks.values()) else 503
return JSONResponse(status_code=status, content={
"status": "healthy" if status == 200 else "degraded",
"checks": checks,
"version": "1.2.0"
})
The Full Toolkit
These patterns are part of a larger collection I put together. If you work with APIs daily, here's what I've built:
📚 The Developer's API Cheat Sheet Collection — Volume 2 ($9.99) — 200+ API patterns, auth flows, error handling, and code snippets for REST + GraphQL
📋 Developer API Cheat Sheet Collection — 50+ APIs with Code Examples ($4.99) — Quick reference for 50+ popular APIs (Stripe, Twilio, SendGrid, etc.)
🔧 The Complete Git & GitHub Power User Toolkit ($9.99) — 200+ Git commands, hooks, CI/CD templates for API project workflows
🐳 50 Docker Commands Every Developer Should Know ($4.99) — Containerize your APIs with these 50 production-ready Docker commands
🎁 10 AI Prompts That Save Me 5 Hours/Week (FREE) — AI prompts for code review, debugging, and API documentation
One More Thing
If you're building side projects and want to monetize them, I put together a guide on automating income streams:
💡 Complete Guide to Building Passive Income with AI Automation ($6.99)
What patterns do you use?
Did I miss any API patterns you can't live without? Drop them in the comments — I might add them to Volume 3 of the cheat sheet collection.
Originally published as part of the Developer Productivity series.
Top comments (0)