Skip to content

API Design Best Practices

REST Fundamentals

REST Maturity Model (Richardson)

REST Maturity Levels


Resource Naming

Conventions

Resource Naming Rules

Resource Examples

# Collections
GET    /orders                 # List orders
POST   /orders                 # Create order
GET    /orders/123             # Get specific order
PUT    /orders/123             # Replace order
PATCH  /orders/123             # Partial update
DELETE /orders/123             # Delete order

# Nested resources
GET    /customers/123/orders   # Orders for customer 123
POST   /orders/456/items       # Add item to order 456

# Actions (when CRUD doesn't fit)
POST   /orders/123/cancel      # Cancel order
POST   /orders/123/refund      # Refund order
POST   /accounts/123/transfer  # Transfer money

# Filtering, Sorting, Pagination
GET    /orders?status=pending&sort=-created_at&page=2&limit=20

# Search
GET    /products/search?q=laptop&category=electronics

HTTP Methods

HTTP Methods

PUT vs PATCH

// PUT - Replace entire resource
PUT /users/123
{
  "name": "John Doe",
  "email": "[email protected]",
  "phone": "555-1234",
  "address": "123 Main St"
}

// PATCH - Partial update
PATCH /users/123
{
  "email": "[email protected]"
}

// PATCH with JSON Patch (RFC 6902)
PATCH /users/123
Content-Type: application/json-patch+json
[
  { "op": "replace", "path": "/email", "value": "[email protected]" },
  { "op": "add", "path": "/phone", "value": "555-9999" }
]

HTTP Status Codes

HTTP Status Codes


Request/Response Design

Request Format

// Create order
POST /orders
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

{
  "customer_id": "cust_123",
  "items": [
    {
      "product_id": "prod_456",
      "quantity": 2
    }
  ],
  "shipping_address": {
    "line1": "123 Main St",
    "city": "New York",
    "state": "NY",
    "postal_code": "10001",
    "country": "US"
  },
  "metadata": {
    "source": "mobile_app"
  }
}

Response Format

// Success response
HTTP/1.1 201 Created
Content-Type: application/json
Location: /orders/ord_789

{
  "id": "ord_789",
  "object": "order",
  "customer_id": "cust_123",
  "status": "pending",
  "items": [...],
  "subtotal": 5000,
  "tax": 450,
  "total": 5450,
  "currency": "usd",
  "created_at": "2024-01-15T10:30:00Z",
  "updated_at": "2024-01-15T10:30:00Z"
}

// Error response
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_invalid",
    "message": "The customer_id provided is invalid.",
    "param": "customer_id",
    "doc_url": "https://api.example.com/docs/errors#parameter_invalid"
  }
}

// Validation errors
HTTP/1.1 422 Unprocessable Entity
{
  "error": {
    "type": "validation_error",
    "code": "validation_failed",
    "message": "Validation failed",
    "errors": [
      {
        "field": "email",
        "code": "invalid_format",
        "message": "Email format is invalid"
      },
      {
        "field": "items",
        "code": "required",
        "message": "At least one item is required"
      }
    ]
  }
}

Pagination

Offset Pagination

GET /orders?offset=20&limit=10

{
  "data": [...],
  "pagination": {
    "offset": 20,
    "limit": 10,
    "total": 150,
    "has_more": true
  }
}

Pros: Simple, allows jumping to any page
Cons: Inconsistent with concurrent changes, slow for large offsets
GET /orders?limit=10&starting_after=ord_xyz

{
  "data": [...],
  "has_more": true,
  "cursors": {
    "next": "ord_abc",
    "previous": "ord_xyz"
  }
}

Pros: Consistent, efficient, works with real-time data
Cons: Can't jump to arbitrary page

Page-Based Pagination

GET /orders?page=3&per_page=20

{
  "data": [...],
  "meta": {
    "current_page": 3,
    "per_page": 20,
    "total_pages": 8,
    "total_count": 150
  },
  "links": {
    "first": "/orders?page=1",
    "prev": "/orders?page=2",
    "next": "/orders?page=4",
    "last": "/orders?page=8"
  }
}

Filtering, Sorting, and Field Selection

Filtering

# Simple equality
GET /products?category=electronics&status=active

# Comparison operators
GET /orders?created_at[gte]=2024-01-01&total[lt]=1000
GET /orders?created_at.gte=2024-01-01  # Alternative syntax

# Multiple values (OR)
GET /products?category=electronics,clothing

# Search
GET /products?q=laptop

# Nested filtering
GET /orders?customer.country=US

Sorting

# Single field (ascending)
GET /products?sort=price

# Descending
GET /products?sort=-price
GET /products?sort=price&order=desc  # Alternative

# Multiple fields
GET /products?sort=-created_at,name
GET /products?sort=category,-price

Field Selection (Sparse Fieldsets)

# Select specific fields
GET /orders?fields=id,status,total
GET /users/123?fields=name,email

# Expand nested objects
GET /orders/123?expand=customer,items.product

Versioning

GET /v1/orders
GET /v2/orders

Pros: Clear, explicit, easy to route
Cons: URL pollution

Header Versioning

GET /orders
Accept: application/vnd.api+json; version=2

Pros: Clean URLs
Cons: Hidden, harder to test

Query Parameter

GET /orders?version=2

Pros: Easy to use
Cons: Optional parameter feels wrong

Best Practices

Versioning Best Practices


Authentication & Security

Authentication Methods

Authentication Methods

Security Headers

# Response headers
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: default-src 'self'
X-Request-Id: req_abc123  # For debugging/tracing

Rate Limiting

# Response headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 998
X-RateLimit-Reset: 1640000000
Retry-After: 60  # When rate limited

# Rate limit exceeded response
HTTP/1.1 429 Too Many Requests
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Please retry after 60 seconds.",
    "retry_after": 60
  }
}

Rate Limiting Strategies

Rate Limiting Strategies


Idempotency

Idempotency


Error Handling

Error Response Structure

{
  "error": {
    "type": "invalid_request_error",
    "code": "resource_missing",
    "message": "No such customer: cust_xyz",
    "param": "customer_id",
    "request_id": "req_abc123",
    "doc_url": "https://api.example.com/docs/errors#resource_missing"
  }
}

Error Types

Error Types


HATEOAS (Hypermedia)

// Order response with links
{
  "id": "ord_123",
  "status": "pending",
  "total": 5000,
  "_links": {
    "self": {
      "href": "/orders/ord_123"
    },
    "customer": {
      "href": "/customers/cust_456"
    },
    "cancel": {
      "href": "/orders/ord_123/cancel",
      "method": "POST"
    },
    "pay": {
      "href": "/orders/ord_123/pay",
      "method": "POST"
    }
  },
  "_embedded": {
    "items": [
      {
        "product_id": "prod_789",
        "quantity": 2,
        "_links": {
          "product": { "href": "/products/prod_789" }
        }
      }
    ]
  }
}

API Documentation

OpenAPI (Swagger) Example

openapi: 3.0.0
info:
  title: Orders API
  version: 1.0.0
  description: API for managing orders

paths:
  /orders:
    get:
      summary: List orders
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, confirmed, shipped]
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        '200':
          description: List of orders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderList'

    post:
      summary: Create order
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
      responses:
        '201':
          description: Order created
        '400':
          description: Invalid request

components:
  schemas:
    Order:
      type: object
      properties:
        id:
          type: string
        status:
          type: string
        total:
          type: integer

Common Interview Questions

  1. PUT vs PATCH?
  2. PUT: Replace entire resource (idempotent)
  3. PATCH: Partial update (can be idempotent)

  4. How to handle API versioning?

  5. URL path (recommended), header, or query param
  6. Support multiple versions, deprecation policy

  7. Pagination approaches?

  8. Offset: Simple but inconsistent
  9. Cursor: Efficient, consistent (recommended)

  10. How to make APIs idempotent?

  11. Idempotency keys for POST requests
  12. Natural idempotency for PUT/DELETE

  13. REST vs RPC-style APIs?

  14. REST: Resource-oriented, HTTP semantics
  15. RPC: Action-oriented, single endpoint