Home/Learn/System Design/API Design Best Practices

API Design Best Practices

Beginner
Communication Patterns

Good API design uses consistent naming, proper HTTP methods, pagination, versioning, idempotency keys, and clear error responses. A well-designed API is the contract that holds distributed systems together.

Overview

APIs are the contracts between services and between your system and external clients. A well-designed API is intuitive, consistent, and evolvable. Key principles include: use nouns for resources and HTTP verbs for actions (GET /orders, POST /orders, DELETE /orders/123); use proper HTTP status codes (200, 201, 400, 404, 500); implement pagination for list endpoints (cursor-based for large datasets); version your API (URL path /v1/ or header-based); use idempotency keys for non-idempotent operations (POST with Idempotency-Key header); return structured error responses with error codes and messages; rate-limit to protect your service; document with OpenAPI/Swagger. API design mistakes are expensive — once published, breaking changes affect all consumers.

RESTful Resource Design

Use nouns for URLs, HTTP verbs for actions, and proper status codes. Keep URLs hierarchical and predictable.

HTTP — RESTful resource design
// Good REST API design
// Resources as nouns, HTTP verbs for actions
GET    /api/v1/users              → 200 (list users)
GET    /api/v1/users/42           → 200 (get user) / 404
POST   /api/v1/users              → 201 (create user)
PUT    /api/v1/users/42           → 200 (full update)
PATCH  /api/v1/users/42           → 200 (partial update)
DELETE /api/v1/users/42           → 204 (no content)

// Nested resources for relationships
GET    /api/v1/users/42/orders    → user's orders
POST   /api/v1/users/42/orders    → create order for user

// Filtering, sorting, pagination via query params
GET /api/v1/orders?status=SHIPPED&sort=-created_at&page=2&limit=20

// HTTP status codes
// 200 OK, 201 Created, 204 No Content
// 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found
// 409 Conflict, 422 Unprocessable Entity, 429 Too Many Requests
// 500 Internal Server Error, 503 Service Unavailable

Pagination, Versioning & Idempotency

Cursor-based pagination scales better than offset-based. Version APIs to evolve without breaking clients. Idempotency keys prevent duplicate operations for non-idempotent methods.

HTTP + JSON — pagination, versioning, idempotency
// Cursor-based pagination (better for large datasets)
GET /api/v1/orders?limit=20&cursor=eyJpZCI6MTAwfQ==
// Response:
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTIwfQ==",
    "has_more": true
  }
}
// Why: offset-based (OFFSET 10000) gets slower as offset grows

// API versioning strategies
// 1. URL path (most common):  /api/v1/users, /api/v2/users
// 2. Header: Accept: application/vnd.myapi.v2+json
// 3. Query param: /api/users?version=2

// Idempotency key — prevent duplicate charges
POST /api/v1/payments
Headers:
  Idempotency-Key: "pay-req-abc-123"
Body: { "amount": 99.99, "currency": "INR" }
// Server: if key seen before, return cached response (no double charge)
// Stripe, Razorpay, and most payment APIs support this

Error Handling & Documentation

Return structured error responses with machine-readable codes and human-readable messages. Document with OpenAPI (Swagger) for auto-generated client SDKs and interactive docs.

JSON + YAML — error handling and OpenAPI
// Structured error response
{
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Account balance is below the required amount.",
    "details": {
      "required": 99.99,
      "available": 50.00,
      "currency": "INR"
    },
    "request_id": "req-abc-123",
    "documentation_url": "https://docs.example.com/errors/INSUFFICIENT_BALANCE"
  }
}

// OpenAPI spec (auto-generates docs + client SDKs)
openapi: 3.0.3
paths:
  /api/v1/orders:
    get:
      summary: List orders
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [PENDING, SHIPPED, DELIVERED]
        - name: cursor
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Paginated list of orders
        '401':
          description: Unauthorized

Key Points to Remember

  • 1Use nouns for resources, HTTP verbs for actions, and proper status codes.
  • 2Cursor-based pagination scales better than offset-based for large datasets.
  • 3Version APIs from day one — URL path versioning (/v1/) is the most common approach.
  • 4Idempotency keys prevent duplicate operations for non-idempotent methods (POST payments).
  • 5Always return structured error responses with machine-readable codes and request IDs.

Interview Questions

Sign in to ask Aria
1

What makes a REST API "well-designed"?

EasyInfosys
2

Why is cursor-based pagination better than offset-based?

MediumAmazon
3

How do you handle API versioning without breaking existing clients?

MediumGoogle
4

What is an idempotency key and why is it important for payment APIs?

MediumFlipkart
5

Design the API layer for a ride-sharing application like Uber.

HardUber

Ask Aria about API Design Best Practices

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…