API Design & Rate Limiting
Your API is the contract between your service and the world. A good API is intuitive, reliable, and scales. A bad API creates support tickets, integration headaches, and technical debt.
REST API Fundamentals
REST isn't just about using HTTP. It's about resources and standard operations on those resources.
Resources, Not Actions
Bad (Action-based)
POST /createUser
POST /getUserById
POST /deleteUserAccount
POST /updateUserEmail
Good (Resource-based)
POST /users
GET /users/{id}
DELETE /users/{id}
PATCH /users/{id}
HTTP Methods
| Method | Purpose | Idempotent? |
|---|---|---|
| GET | Read a resource | Yes |
| POST | Create a resource | No |
| PUT | Replace a resource | Yes |
| PATCH | Partial update | Depends |
| DELETE | Remove a resource | Yes |
Status Codes Matter
200 OK- Success201 Created- Resource created400 Bad Request- Invalid input401 Unauthorized- Authentication required404 Not Found- Resource doesn't exist429 Too Many Requests- Rate limited500 Internal Server Error- Something broke
Idempotency: Safe Retries
Network failures happen. Clients will retry. Without idempotency, this causes problems:
User clicks "Pay $100"
→ Request sent
→ Network timeout (but server processed it!)
→ Client retries automatically
→ User charged $200 😱
Idempotency Keys
Stripe's approach: Clients include a uniqueIdempotency-Key header. The server caches responses by this key.
"We store idempotency keys for at least 24 hours. Keys can be up to 255 characters." On retry with the same key, Stripe returns the cached response without re-processing.
# Server-side idempotency handling
def process_payment(request):
key = request.headers.get('Idempotency-Key')
# Check if we've seen this key before
cached = cache.get(f"idempotency:{key}")
if cached:
return cached # Return cached response
# Process the payment
result = charge_card(request.body)
# Cache the response
cache.set(f"idempotency:{key}", result, ttl=86400) # 24 hours
return result
Rate Limiting
Your API can handle 1,000 requests/second. One misconfigured client starts sending 100,000 requests/second. Without rate limiting, your service crashes for everyone.
Why Rate Limit?
Prevent abuse
Stop DDoS attacks and runaway scripts
Fair usage
Ensure no single user monopolizes resources
Cost control
Limit expensive operations (AI APIs, SMS)
Rate Limiting Algorithms
#### 1. Token Bucket
Imagine a bucket that holds tokens. Each request costs a token. Tokens refill at a fixed rate.
#### 2. Sliding Window
Count requests in a sliding time window (e.g., last 60 seconds).
Window: Last 60 seconds
Limit: 100 requests
Time Requests Status
12:00 50 OK
12:01 40 OK
12:02 30 Blocked (50+40+30 > 100)
Pros: More accurate than fixed windows, prevents boundary issues
#### 3. Fixed Window
Simple counter per time window (e.g., 100 requests per minute).
Cons: Traffic spikes at window boundaries (99 requests at 11:59, 100 at 12:00 = 199 in 2 seconds)Real-World: Stripe's Rate Limiting
"We run several different rate limiters in production. We have found that carefully implementing a few rate limiting strategies helps keep the API available for everyone."
- Request rate limiter: Limits per-second requests per API key
- Concurrent request limiter: Limits in-flight requests
- Fleet usage load shedder: Drops low-priority traffic under load
- Worker utilization load shedder: Protects individual workers
Rate Limit Headers
Good APIs tell clients about their limits:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 67
X-RateLimit-Reset: 1640000000
When limited:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
API Versioning
APIs evolve. How do you make breaking changes without breaking existing clients?
URL Path
/v1/users
/v2/users
Header
API-Version: 2024-01-15
Query Param
/users?version=2
Stripe uses date-based API versions (2024-01-15). When you create an account, your version is locked to that date. You can upgrade at your own pace.
Pagination
Never return unbounded lists. Always paginate.
// Request
GET /users?limit=20&cursor=abc123
// Response
{
"data": [...],
"has_more": true,
"next_cursor": "def456"
}
Cursor-based (recommended) vs offset-based (simpler but inconsistent with mutations).
Key Takeaways
- Design around resources, not actions - REST is about nouns
- Idempotency keys make retries safe - essential for payments
- Rate limiting protects your service - token bucket allows bursts
- Version your API - breaking changes will happen
Next up: Case Study: URL Shortener - Putting it all together in a complete system design.