🔥 0
0
Lesson 8 of 10 15 min +125 XP

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

2xx
Success
4xx
Client error
5xx
Server error
Common codes:
  • 200 OK - Success
  • 201 Created - Resource created
  • 400 Bad Request - Invalid input
  • 401 Unauthorized - Authentication required
  • 404 Not Found - Resource doesn't exist
  • 429 Too Many Requests - Rate limited
  • 500 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 unique Idempotency-Key header. The server caches responses by this key.
Stripe's Idempotency Implementation

"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.

Stripe Engineering Blog

# 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.

Token bucket rate limiting: tokens refill over time, requests consume tokens
Pros: Allows bursts (up to bucket size), smooth limiting Used by: AWS, Stripe

#### 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

Stripe's Multi-Layer Approach

"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

Stripe Engineering Blog

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's Date-Based Versioning

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.

Stripe Engineering Blog

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.

🧠 Quick Quiz

Test your understanding of this lesson.

1

What is an idempotency key used for?

2

Which rate limiting algorithm is best for allowing bursts?

3

What HTTP status code should be returned when rate limited?