API Design Best Practices: RESTful APIs, Versioning, Authentication
Master API design for system design interviews. Learn REST principles, versioning strategies, authentication (JWT, OAuth), rate limiting with real examples from Stripe, Twitter, GitHub APIs

Your API Is Your Product
Not your database. Not your backend code. Your API.
When Stripe launched in 2011, their payment processing wasn't revolutionary. Competitors existed. What made Stripe a $95 billion company? Their API was beautiful.
// Stripe API: Charge a credit card in 3 lines
const charge = await stripe.charges.create({
amount: 2000,
currency: 'usd',
source: 'tok_visa',
});
Clean. Intuitive. Obvious.
Compare to competitors at the time: XML, SOAP, dozens of configuration fields, unclear error messages. Developers chose Stripe because their API didn't suck.
Your API is how developers experience your product. A bad API? Developers leave. A great API? They evangelize for free.
In this guide, I'll show you how to design APIs like Stripe, GitHub, and TwitterβAPIs that developers love. Let's dive in.
What Is an API?
API = Application Programming Interface
In simple terms: Contract that defines how applications talk to each other
Example:
Your mobile app β Sends HTTP request β Your server
β
Server processes request
β
Sends JSON response β Your app displays data
APIs we'll cover:
- REST (most common)
- GraphQL (Facebook's alternative)
- gRPC (Google's high-performance RPC)
Focus: REST APIs (90% of APIs)
RESTful API Principles
REST = Representational State Transfer
6 principles:
1. Resource-Based URLs
Resources = Entities (users, products, orders)
Good (Resource-based):
GET /users # Get list of users
GET /users/123 # Get user with ID 123
POST /users # Create new user
PUT /users/123 # Update user 123
DELETE /users/123 # Delete user 123
Bad (Action-based):
GET /getUsers
GET /getUserById?id=123
POST /createUser
POST /updateUser
POST /deleteUser
Why resource-based is better: β Predictable (developers know URL pattern) β Standard HTTP methods (GET, POST, PUT, DELETE) β RESTful convention
2. HTTP Methods (Verbs)
Use HTTP methods to indicate action:
| Method | Purpose | Idempotent? | Example |
|---|---|---|---|
| GET | Retrieve resource | Yes | Get user profile |
| POST | Create resource | No | Create new user |
| PUT | Update resource (full) | Yes | Replace user data |
| PATCH | Update resource (partial) | No | Update user email |
| DELETE | Delete resource | Yes | Delete user |
Idempotent = Same request multiple times β same result
Example:
# GET is idempotent (safe to call multiple times)
GET /users/123
GET /users/123
GET /users/123
# All return same user data
# POST is NOT idempotent (creates multiple resources)
POST /users {"name": "John"}
POST /users {"name": "John"}
POST /users {"name": "John"}
# Creates 3 different users!
# DELETE is idempotent
DELETE /users/123
DELETE /users/123
DELETE /users/123
# First call: Deletes user
# Subsequent calls: User already deleted (same result)
3. HTTP Status Codes
Use appropriate status codes:
2xx (Success)
200 OK # Standard success
201 Created # Resource created (POST)
204 No Content # Success, no body (DELETE)
3xx (Redirection)
301 Moved Permanently # URL changed permanently
302 Found # Temporary redirect
304 Not Modified # Cached version is fresh
4xx (Client Error)
400 Bad Request # Invalid input
401 Unauthorized # Not authenticated
403 Forbidden # Authenticated but no permission
404 Not Found # Resource doesn't exist
429 Too Many Requests # Rate limit exceeded
5xx (Server Error)
500 Internal Server Error # Generic server error
502 Bad Gateway # Upstream service failed
503 Service Unavailable # Server overloaded
Example:
// User creates post
POST /posts
{
"title": "My first post",
"content": "Hello world"
}
// Response
HTTP/1.1 201 Created
Location: /posts/123
{
"id": 123,
"title": "My first post",
"content": "Hello world",
"created_at": "2025-08-28T10:00:00Z"
}
4. JSON Responses
Use JSON (not XML, not plain text)
Good:
{
"id": 123,
"username": "john_doe",
"email": "john@example.com",
"created_at": "2025-08-15T10:30:00Z"
}
Bad:
<user>
<id>123</id>
<username>john_doe</username>
<email>john@example.com</email>
</user>
Why JSON? β Human-readable β Natively supported in JavaScript β Smaller than XML β Standard for modern APIs
5. Stateless
Each request contains all information needed (no session state on server)
Bad (Stateful):
Step 1: POST /auth/login β Server stores session
Step 2: GET /users/me β Server checks session
Problem: Doesn't scale (session tied to specific server)
Good (Stateless):
Step 1: POST /auth/login β Return JWT token
Step 2: GET /users/me
Headers: Authorization: Bearer <token>
Server validates token (no session storage)
Benefits: β Scalable (any server can handle any request) β No server-side session storage β Load balancing works seamlessly
6. HATEOAS (Hypermedia)
Include links to related resources
Example:
GET /users/123
{
"id": 123,
"username": "john_doe",
"email": "john@example.com",
"links": {
"self": "/users/123",
"posts": "/users/123/posts",
"followers": "/users/123/followers",
"following": "/users/123/following"
}
}
Benefits: β Discoverable API β Clients know what actions are available
Reality: Most APIs skip this (added complexity)
API Design Best Practices
1. Versioning
Problem: API changes break existing clients
Solution: Version your API
Strategy 1: URL Versioning (Most Common)
GET /v1/users
GET /v2/users
Pros: β Simple, obvious β Easy to route to different codebases
Cons: β URL changes (breaks bookmarks)
Used by: Twitter, Stripe, GitHub
Example: Stripe
https://api.stripe.com/v1/charges
Strategy 2: Header Versioning
GET /users
Headers:
Accept: application/vnd.myapi.v2+json
Pros: β Clean URLs
Cons: β Less obvious β Harder to test (can't type in browser)
Used by: GitHub (also supports URL versioning)
Strategy 3: Query Parameter Versioning
GET /users?version=2
Pros: β Simple
Cons: β Query params usually for filtering, not versioning β Easy to forget
Rarely used
When to Increment Version?
Breaking changes:
- Remove field
- Rename field
- Change data type
- Change response structure
Non-breaking changes (don't increment):
- Add new field
- Add new endpoint
- Fix bug
Example:
// v1
{
"id": 123,
"name": "John Doe"
}
// v2 (breaking: split name field)
{
"id": 123,
"first_name": "John",
"last_name": "Doe"
}
Clients expecting name field β break. Version change needed.
2. Pagination
Problem: Returning 1 million users in single response β slow, crashes client
Solution: Paginate
Offset-Based Pagination
GET /users?limit=20&offset=0 # First 20 users
GET /users?limit=20&offset=20 # Next 20 users
GET /users?limit=20&offset=40 # Next 20 users
Pros: β Simple β Can jump to any page
Cons: β Performance degrades with large offsets β Inconsistent results if data changes (new user inserted β shifts results)
Used by: Most APIs
Cursor-Based Pagination
GET /users?limit=20
Response:
{
"data": [...],
"next_cursor": "eyJpZCI6MTIzfQ=="
}
GET /users?limit=20&cursor=eyJpZCI6MTIzfQ==
Pros: β Consistent results (even if data changes) β Better performance (no offset calculation)
Cons: β Can't jump to specific page
Used by: Twitter, Facebook
Example: Twitter API
GET /2/tweets/search/recent?query=cat&max_results=10&next_token=abc123
3. Filtering, Sorting, Searching
Filtering:
GET /products?category=electronics
GET /products?category=electronics&price_min=100&price_max=500
Sorting:
GET /products?sort=price_asc
GET /products?sort=created_at_desc
Searching:
GET /products?search=laptop
Example: GitHub API
GET /search/repositories?q=language:python&sort=stars&order=desc
4. Rate Limiting
Problem: Abusive users sending millions of requests β overload server
Solution: Rate limiting
Common limits:
1,000 requests per hour per user
10 requests per second per user
Implementation:
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1698753600
{
"error": "Rate limit exceeded. Try again in 1 hour."
}
Headers:
X-RateLimit-Limit: Total requests allowedX-RateLimit-Remaining: Requests leftX-RateLimit-Reset: Unix timestamp when limit resets
Real Example: GitHub API
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Reset: 1698753600
5. Authentication
Common methods:
API Keys (Simple)
GET /users
Headers:
X-API-Key: sk_live_abc123def456
Pros: β Simple β Easy to implement
Cons: β Less secure (key can be stolen) β No expiration
Used for: Server-to-server APIs
JWT (JSON Web Tokens)
POST /auth/login
{
"email": "john@example.com",
"password": "secret"
}
Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
# Use token in subsequent requests
GET /users/me
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT Structure:
Header.Payload.Signature
Payload (decoded):
{
"user_id": 123,
"email": "john@example.com",
"exp": 1698753600 # Expiration timestamp
}
Pros: β Stateless (no server-side session storage) β Secure (signed, can't be tampered) β Expiration built-in
Cons: β Can't revoke (until expiration)
Used by: Most modern APIs
OAuth 2.0 (Third-Party Access)
Used when: User wants to grant third-party app access to their data
Example: "Sign in with Google", "Connect to Twitter"
Flow:
1. User clicks "Login with Google"
2. Redirected to Google login page
3. User grants permission
4. Google redirects back with authorization code
5. Your app exchanges code for access token
6. Use access token to call Google APIs on user's behalf
Pros: β Secure (user doesn't share password) β Granular permissions (scopes) β Token can be revoked
Cons: β Complex to implement
Used by: Google, Facebook, GitHub, Twitter
6. Error Handling
Good error responses:
Bad:
{
"error": "Something went wrong"
}
Good:
{
"error": {
"code": "INVALID_EMAIL",
"message": "Email format is invalid",
"field": "email",
"value": "notanemail",
"documentation": "https://api.example.com/docs/errors#INVALID_EMAIL"
}
}
Include: β Error code (machine-readable) β Human-readable message β Which field caused error β Link to documentation
Example: Stripe
{
"error": {
"type": "card_error",
"code": "card_declined",
"message": "Your card was declined.",
"param": "source",
"doc_url": "https://stripe.com/docs/error-codes/card-declined"
}
}
API Documentation
Your API is useless if developers don't understand it.
What to Document
For each endpoint:
- Purpose: What does it do?
- HTTP method and URL:
GET /users/{id} - Request parameters: Path params, query params, headers, body
- Response format: Success and error responses
- Example requests/responses
- Authentication requirements
- Rate limits
Tools
Swagger/OpenAPI:
paths:
/users/{id}:
get:
summary: Get user by ID
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: User not found
Generates interactive docs automatically.
Used by: Stripe, AWS, many others
Real-World API Examples
1. Stripe API
Why it's great:
// Clear, simple, intuitive
const customer = await stripe.customers.create({
email: 'customer@example.com',
source: 'tok_visa',
});
const charge = await stripe.charges.create({
amount: 2000,
currency: 'usd',
customer: customer.id,
});
Best practices:
β
RESTful
β
Clear error messages
β
Excellent documentation
β
Consistent naming conventions
β
Versioned (/v1/)
2. GitHub API
Why it's great:
GET /repos/facebook/react
Response:
{
"id": 10270250,
"name": "react",
"full_name": "facebook/react",
"description": "A declarative, efficient JavaScript library",
"stargazers_count": 210000,
"forks_count": 43000,
"url": "https://api.github.com/repos/facebook/react"
}
Best practices: β Hypermedia (includes URLs to related resources) β Rich filtering/searching β Clear rate limits β OAuth for authentication
3. Twitter API
Why it's great:
GET /2/tweets/search/recent?query=cat&max_results=10
Response:
{
"data": [...],
"meta": {
"next_token": "abc123"
}
}
Best practices:
β
Cursor-based pagination
β
Versioned (/2/)
β
Clear rate limits
System Design Interview Tips
Common Question: "Design an API for system"
Approach:
1. Define resources:
For Twitter:
- Users
- Tweets
- Followers
- Likes
2. Design endpoints:
GET /users/{id}
POST /users
GET /users/{id}/tweets
POST /tweets
DELETE /tweets/{id}
POST /tweets/{id}/like
GET /users/{id}/followers
POST /users/{id}/follow
3. Discuss:
- Authentication: JWT or OAuth
- Rate limiting: 1,000 requests/hour
- Versioning: URL versioning (
/v1/) - Pagination: Cursor-based (for timelines)
- Error handling: Structured error responses
What to Mention
β RESTful principles (resources, HTTP methods) β Versioning strategy β Pagination (offset vs cursor) β Authentication (JWT, OAuth) β Rate limiting β Error responses β Documentation
Avoid These Mistakes
β Non-RESTful URLs (/getUserById)
β Inconsistent naming (/users vs /get-tweets)
β Not discussing versioning
β Ignoring rate limiting
β Vague error messages
REST vs GraphQL vs gRPC
Quick comparison:
| Feature | REST | GraphQL | gRPC |
|---|---|---|---|
| Request Format | HTTP | Query language | Protocol Buffers |
| Response | JSON | JSON | Binary |
| Over/Under-fetching | Yes | No β | No β |
| Learning Curve | Low β | Medium | High |
| Performance | Good | Good | Excellent β |
| Caching | Easy β | Hard | Hard |
| Use Case | General | Frontend-driven | Microservices |
When to use REST: Most cases (simple, widely understood) When to use GraphQL: Frontend needs flexible queries, avoid over-fetching When to use gRPC: High-performance microservices (internal APIs)
Conclusion
Great APIs are:
- Intuitive (developers understand without reading docs)
- Consistent (naming, structure, error handling)
- Well-documented (examples, error codes)
- Versioned (don't break existing clients)
- Secure (authentication, rate limiting)
Remember: Your API is your product. Stripe didn't win because of superior payment processingβthey won because their API was a joy to use.
Design your API like developers are your customers. Because they are.
Key takeaways:
- Use RESTful principles (resources, HTTP methods, status codes)
- Version your API (URL versioning most common)
- Paginate large responses (cursor-based for feeds)
- Implement rate limiting
- Use JWT or OAuth for authentication
- Provide clear error messages
- Document everything
Master these, and developers will love your APIβand by extension, your product.