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

πŸ“… Published: August 28, 2025 ✏️ Updated: September 25, 2025 By Ojaswi Athghara
#api-design #rest #authentication #versioning #system-design

API Design Best Practices: RESTful APIs, Versioning, Authentication

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:

MethodPurposeIdempotent?Example
GETRetrieve resourceYesGet user profile
POSTCreate resourceNoCreate new user
PUTUpdate resource (full)YesReplace user data
PATCHUpdate resource (partial)NoUpdate user email
DELETEDelete resourceYesDelete 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 allowed
  • X-RateLimit-Remaining: Requests left
  • X-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:

  1. Purpose: What does it do?
  2. HTTP method and URL: GET /users/{id}
  3. Request parameters: Path params, query params, headers, body
  4. Response format: Success and error responses
  5. Example requests/responses
  6. Authentication requirements
  7. 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:

FeatureRESTGraphQLgRPC
Request FormatHTTPQuery languageProtocol Buffers
ResponseJSONJSONBinary
Over/Under-fetchingYesNo βœ…No βœ…
Learning CurveLow βœ…MediumHigh
PerformanceGoodGoodExcellent βœ…
CachingEasy βœ…HardHard
Use CaseGeneralFrontend-drivenMicroservices

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:

  1. Use RESTful principles (resources, HTTP methods, status codes)
  2. Version your API (URL versioning most common)
  3. Paginate large responses (cursor-based for feeds)
  4. Implement rate limiting
  5. Use JWT or OAuth for authentication
  6. Provide clear error messages
  7. Document everything

Master these, and developers will love your APIβ€”and by extension, your product.


Cover image by Douglas Lopes on Unsplash

Support My Work

If this guide helped you learn something new, solve a problem, or ace your interviews, I'd really appreciate your support! Creating comprehensive, free content like this takes significant time and effort. Your support helps me continue sharing knowledge and creating more helpful resources for developers and students.

Buy me a Coffee

Every contribution, big or small, means the world to me and keeps me motivated to create more content!

Related Blogs

Ojaswi Athghara

SDE, 4+ Years

Β© ojaswiat.com 2025-2027