Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

API Design Principles

Why API Design Matters

APIs are the contract between services. Good API design enables scalability, maintainability, and developer productivity. Poor API design leads to tight coupling, versioning nightmares, and developer frustration.

API Paradigms

1. REST (Representational State Transfer)

The most common API paradigm for web services.

GET    /api/v1/users          → List users
GET    /api/v1/users/123      → Get user 123
POST   /api/v1/users          → Create user
PUT    /api/v1/users/123      → Update user 123 (full)
PATCH  /api/v1/users/123      → Update user 123 (partial)
DELETE /api/v1/users/123      → Delete user 123

REST Principles

PrincipleDescriptionExample
StatelessEach request contains all needed infoAuth token in header
Resource-basedURLs represent resources, not actions/users not /getUsers
HTTP methodsUse standard methods (GET, POST, etc.)GET for reads, POST for creates
HATEOASLinks to related resourcesResponse includes next URL
Uniform interfaceConsistent naming and structure/users/{id}/orders

REST Best Practices

✅ Good:
GET /api/v1/users/123/orders
POST /api/v1/users
DELETE /api/v1/users/123

❌ Bad:
GET /api/v1/getUser?id=123
POST /api/v1/createUser
POST /api/v1/deleteUser/123

Status Codes

200 OK              → Successful GET/PUT/PATCH
201 Created         → Successful POST
204 No Content      → Successful DELETE
400 Bad Request     → Invalid input
401 Unauthorized    → Missing/invalid auth
403 Forbidden       → Authenticated but not allowed
404 Not Found       → Resource doesn't exist
409 Conflict        → Duplicate or conflict
429 Too Many Reqs   → Rate limited
500 Server Error    → Internal error
503 Unavailable     → Service down

2. GraphQL

Query language for APIs, developed by Facebook.

# Schema
type User {
  id: ID!
  name: String!
  email: String!
  orders: [Order!]!
}

type Query {
  user(id: ID!): User
  users(limit: Int, offset: Int): [User!]!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
}

# Query (client specifies what it needs)
query {
  user(id: "123") {
    name
    orders {
      id
      total
    }
  }
}

GraphQL vs REST

AspectRESTGraphQL
EndpointsMultipleSingle
Data fetchingFixed structureClient specifies
Over-fetchingCommonEliminated
Under-fetchingCommon (multiple calls)Eliminated
CachingEasy (HTTP caching)Harder
ComplexitySimplerMore complex
File uploadsNativeRequires workaround
Real-timeWebSocketSubscriptions

When to Use GraphQL

  • Mobile apps (bandwidth sensitive)
  • Complex data relationships
  • Multiple client types with different needs
  • Rapid frontend iteration

When to Use REST

  • Simple CRUD APIs
  • Public APIs (easier to document/cache)
  • Microservices (simpler inter-service communication)
  • File-heavy operations

3. gRPC (Google Remote Procedure Call)

High-performance RPC framework using Protocol Buffers.

// user.proto
syntax = "proto3";

service UserService {
  rpc GetUser(GetUserRequest) returns (User);
  rpc ListUsers(ListUsersRequest) returns (stream User);
  rpc CreateUser(CreateUserRequest) returns (User);
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
}

gRPC vs REST

AspectgRPCREST
ProtocolHTTP/2HTTP/1.1 (usually)
FormatProtobuf (binary)JSON (text)
SpeedFaster (binary, multiplexing)Slower
StreamingNative (bidirectional)Limited (SSE, WebSocket)
Browser supportRequires proxyNative
Code generationBuilt-inManual or OpenAPI
Learning curveSteeperLower

When to Use gRPC

  • Microservice-to-microservice communication
  • Low-latency requirements
  • Streaming data
  • Polyglot environments (auto-generated clients)

API Paradigm Comparison

CriteriaRESTGraphQLgRPC
Learning curveLowMediumHigh
PerformanceGoodGoodExcellent
FlexibilityMediumHighMedium
CachingEasyHardN/A
ToolingExcellentGoodGood
Browser supportNativeNativeNeeds proxy
Best forPublic APIsClient-driven queriesInternal services

API Versioning

Versioning Strategies

URL Versioning

GET /api/v1/users
GET /api/v2/users
  • Pros: Clear, easy to route
  • Cons: URL changes, client must update

Header Versioning

GET /api/users
Accept: application/vnd.myapi.v2+json
  • Pros: Clean URLs
  • Cons: Less visible, harder to test

Query Parameter Versioning

GET /api/users?version=2
  • Pros: Easy to implement
  • Cons: Clutters URL, easy to forget

Best Practice

URL versioning for public APIs (clear, explicit)
Header versioning for internal APIs (clean URLs)

Pagination

Offset-Based Pagination

GET /api/v1/users?offset=20&limit=10

Response:
{
  "data": [...],
  "total": 1000,
  "offset": 20,
  "limit": 10
}

Pros: Simple, can jump to any page Cons: Inconsistent with concurrent inserts/deletes, slow at high offsets

Cursor-Based Pagination

GET /api/v1/users?cursor=eyJpZCI6MTAwfQ&limit=10

Response:
{
  "data": [...],
  "next_cursor": "eyJpZCI6MTEwfQ",
  "has_more": true
}

Pros: Consistent, efficient for large datasets Cons: Can’t jump to arbitrary page

Keyset Pagination

GET /api/v1/users?created_after=2024-01-01T00:00:00Z&limit=10

Pros: Efficient (uses index), consistent Cons: Requires sortable column

Pagination Comparison

StrategyConsistencyPerformanceJump to PageUse Case
OffsetPoorDegradesYesSmall datasets
CursorGoodConsistentNoLarge datasets, infinite scroll
KeysetGoodExcellentNoTime-series, feeds

Rate Limiting

Why Rate Limiting?

  • Prevent abuse and DDoS
  • Ensure fair resource usage
  • Protect backend services
  • Manage costs

Rate Limiting Algorithms

Token Bucket

Bucket: capacity=10, refill=2/sec
Request: Takes 1 token
Empty: Reject request

Time 0: [10 tokens] → Request ✓
Time 1: [9 tokens] → Request ✓
Time 5: [5+2=7 tokens] → Multiple requests ✓

Sliding Window

Window: 60 seconds, Limit: 100 requests
Track: timestamps of each request
Count: requests in last 60 seconds
Exceed: Reject

Fixed Window

Window: 1 minute (00:00-00:60), Limit: 100
Count: requests in current minute
Problem: Burst at window boundary
00:59: 100 requests (OK)
00:01: 100 requests (OK)
Total: 200 in 2 seconds (exceeds intended rate)

Sliding Window Log

Log: [T1, T2, T3, ...]
On request: Remove entries older than window
Count remaining: If < limit, allow

Algorithm Comparison

AlgorithmMemoryAccuracyBurst HandlingUse Case
Token BucketLowGoodAllows burstAPI gateways
Sliding WindowMediumGoodSmoothGeneral purpose
Fixed WindowLowFairBoundary burstSimple cases
Sliding LogHighExactSmoothStrict limiting

Rate Limit Headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640995200

HTTP/1.1 429 Too Many Requests
Retry-After: 30

API Security

Authentication Methods

MethodHowUse Case
API KeySimple string in headerPublic APIs, simple auth
OAuth 2.0Token-based delegationThird-party access
JWTSigned token with claimsStateless auth
mTLSMutual certificate authService-to-service

API Security Best Practices

✅ Always use HTTPS
✅ Validate all input
✅ Implement rate limiting
✅ Use authentication (OAuth/JWT)
✅ Use authorization (RBAC/ABAC)
✅ Log all API access
✅ Sanitize error messages (no stack traces)
✅ Use CORS properly
✅ Implement request size limits

Error Handling

Consistent Error Response

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format"
      }
    ],
    "request_id": "req_abc123"
  }
}

Error Code Categories

VALIDATION_ERROR    → 400 Bad Request
AUTHENTICATION_ERROR → 401 Unauthorized
AUTHORIZATION_ERROR → 403 Forbidden
NOT_FOUND          → 404 Not Found
CONFLICT           → 409 Conflict
RATE_LIMITED       → 429 Too Many Requests
INTERNAL_ERROR     → 500 Server Error

API Documentation

OpenAPI (Swagger)

openapi: 3.0.0
paths:
  /users/{id}:
    get:
      summary: Get user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: User found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

Interview Tips

  1. Choose paradigm based on use case — REST for public API, gRPC for internal, GraphQL for complex queries
  2. Always discuss versioning — “We’ll use URL versioning for backward compatibility”
  3. Include pagination — “Cursor-based pagination for the feed”
  4. Mention rate limiting — “Token bucket algorithm, 100 requests per minute”
  5. Consider error handling — “Consistent error format with request IDs”
  6. Think about security — “OAuth 2.0 for third-party, JWT for internal”
  7. Document the API — “OpenAPI spec for REST, proto files for gRPC”
  8. Discuss caching — “ETags for conditional requests, cache headers”

Common Mistakes

  • ❌ Using verbs in URLs (/getUsers)
  • ❌ Ignoring versioning
  • ❌ No pagination on list endpoints
  • ❌ Inconsistent error formats
  • ❌ No rate limiting
  • ❌ Returning 200 for errors
  • ❌ Exposing internal IDs or stack traces

Cross-References