API Design Best Practices

Creating effective, user-friendly, and maintainable APIs

Introduction to API Design

Designing a good API is similar to designing a good user interface—but for developers rather than end users. While REST provides the architectural foundation, effective API design requires additional consideration of user experience, consistency, and practicality.

A well-designed API is:

In this lecture, we'll explore concrete best practices for designing APIs that embody these qualities, focusing on both RESTful principles and practical considerations from real-world implementations.

mindmap root((API Design)) Resources Naming Structure Relationships Operations Methods Parameters Responses Developer Experience Documentation Consistency Error Handling Evolution Versioning Deprecation Backward Compatibility

Resource Design

Effective resource design is the foundation of a good API. It involves identifying the right resources, naming them clearly, and structuring their relationships appropriately.

Resource Identification

Start by identifying the key entities in your domain model. Good candidates for resources typically include:

When identifying resources, consider:

For an e-commerce system, core resources might include:


/products
/customers
/orders
/payments
/reviews
/categories
/inventory
            

Resource Naming Best Practices

Clear, consistent resource naming is crucial for a usable API:

Good Resource Names Problematic Resource Names
/users /getUsers (verb)
/products /product (inconsistent plurality)
/shopping-carts /sc (unclear abbreviation)
/order-items /orderStuff (vague)

Resource Hierarchies

Organize resources hierarchically when they have strong parent-child relationships. This helps express containment and ownership clearly.


# Parent-child relationships
/users/123/orders                # All orders for user 123
/orders/456/items                # All items in order 456
/products/789/reviews            # All reviews for product 789

# Avoid excessive nesting
/users/123/orders/456/items/789  # Too deep, prefer /orders/456/items/789
            

As a rule of thumb, limit nesting to a maximum of 2-3 levels. Beyond that, consider flattening with query parameters or separate endpoints.

Collection and Singleton Resources

APIs typically have two types of resources:

Some resources might also be singletons within a parent context:


/users/current                   # Current authenticated user
/users/123/profile               # Profile of user 123 (only one per user)
/system/status                   # System status information
            

Custom Resource Types

Some operations don't fit neatly into the standard CRUD model. In these cases, consider creating specialized resource types:


# Controller resources (for operations that don't map to standard CRUD)
/search                          # Search across multiple resource types
/calculator                      # Perform calculations

# Process resources (for long-running operations)
/jobs                            # Background processing jobs
/imports                         # Data import processes

# Virtual resources (don't map directly to backend entities)
/dashboard                       # Combined stats and insights
/suggestions                     # Personalized recommendations
            

These resources still follow RESTful principles, even if they represent operations or virtual concepts rather than traditional database entities.

HTTP Methods & Operations

HTTP methods define the operations that can be performed on resources. Using them consistently is key to a clear and predictable API.

Standard CRUD Operations

For most resources, implement these standard operations:

Operation HTTP Method URL Description
List all resources GET /resources Returns a collection of resources, typically paginated
Create new resource POST /resources Creates a new resource and returns its details
Read single resource GET /resources/{id} Returns details for a specific resource
Update resource (full) PUT /resources/{id} Replaces the entire resource with new data
Update resource (partial) PATCH /resources/{id} Updates only specified fields of the resource
Delete resource DELETE /resources/{id} Deletes the resource

PUT vs. PATCH

Understanding the distinction between PUT and PATCH is important:


# Original resource
{
  "name": "John Smith",
  "email": "john@example.com",
  "role": "user",
  "status": "active"
}

# PUT request (complete replacement)
PUT /users/123
{
  "name": "John Smith",
  "email": "new.email@example.com",
  "role": "user"
  # Note: missing 'status' field might be set to null or default value
}

# PATCH request (partial update)
PATCH /users/123
{
  "email": "new.email@example.com"
  # Note: other fields remain unchanged
}
            

Bulk Operations

APIs often need to support operations on multiple resources at once:


# Bulk create
POST /users/bulk
{
  "users": [
    { "name": "User 1", "email": "user1@example.com" },
    { "name": "User 2", "email": "user2@example.com" }
  ]
}

# Bulk update
PATCH /users/bulk
{
  "ids": [123, 456, 789],
  "changes": { "status": "inactive" }
}

# Bulk delete
DELETE /users?ids=123,456,789
            

For bulk operations, prioritize consistency, clarity, and error handling. Decide whether the operation should be atomic (all succeed or all fail) or partial (return which succeeded and which failed).

Complex Operations

Some operations don't map cleanly to CRUD. Here are patterns for handling them:

Pattern Example Best For
Resource-Action POST /orders/123/cancel Actions on specific resources
Controller Resource POST /password-reset Cross-cutting actions not tied to a specific resource
State Transfer PUT /orders/123
{ "status": "cancelled" }
When the action is essentially a state change
Sub-Resource PUT /users/123/activation
{ "active": true }
When the operation affects a specific aspect of a resource

Each pattern has its place. Choose based on:

Query Parameters and Filtering

Query parameters allow clients to modify the behavior of resource requests, such as filtering, sorting, and pagination.

Common Query Parameter Uses

Purpose Example
Filtering /users?status=active&role=admin
Sorting /users?sort=created_at:desc
Pagination /users?page=2&per_page=20
Field selection /users?fields=id,name,email
Searching /users?search=smith
Expanding relations /orders?expand=customer,items

Filtering Strategies

Consider these approaches to filtering, from simple to complex:


# Basic field equality
/products?category=electronics&in_stock=true

# Operators for numeric fields
/products?price_min=100&price_max=500

# Dates and time ranges
/orders?created_after=2025-01-01&created_before=2025-02-01

# Complex filtering with a query language
/products?filter=category:electronics AND (price<500 OR price>1000)
            

For advanced filtering, consider:

Pagination Approaches

Proper pagination is essential for handling large data sets efficiently. Common approaches include:

Offset-Based Pagination


/users?page=2&per_page=20
/users?offset=40&limit=20
            

Advantages: Simple to implement, familiar to developers
Disadvantages: Performance issues with large offsets, inconsistent results with data changes

Cursor-Based Pagination


/users?after=eyJpZCI6MTIzNDV9&limit=20
/users?cursor=eyJpZCI6MTIzNDV9&limit=20
            

Advantages: Better performance, consistent results with data changes
Disadvantages: More complex to implement, cannot jump to arbitrary pages

Keyset Pagination


/users?after_id=12345&limit=20
/orders?after_date=2025-03-15T14:30:00Z&limit=20
            

Advantages: Good performance, simple to implement and use
Disadvantages: Requires a stable sorting key, doesn't work well with complex sorts

Include pagination metadata in responses to help clients navigate:


{
  "data": [...],
  "pagination": {
    "total_count": 573,
    "page": 2,
    "per_page": 20,
    "total_pages": 29,
    "links": {
      "first": "/users?page=1&per_page=20",
      "prev": "/users?page=1&per_page=20",
      "next": "/users?page=3&per_page=20",
      "last": "/users?page=29&per_page=20"
    }
  }
}
            

Sorting

Provide flexible sorting options to meet varied client needs:


# Basic single-field sorting
/users?sort=last_name

# Multiple fields with directions
/users?sort=status:asc,created_at:desc

# Default sorting direction
/products?sort=price  # Assumes ascending

# Alias common sort patterns
/articles?sort=newest  # Equivalent to created_at:desc
            

Document your sorting options clearly, including:

Response Design and Status Codes

Well-designed responses make your API easy to use and understand. They should be consistent, informative, and follow established patterns.

Response Format Consistency

Maintain a consistent response structure across your API:


# Single resource response
{
  "id": 123,
  "name": "Product Name",
  "price": 99.99,
  "created_at": "2025-03-15T14:30:00Z",
  "updated_at": "2025-03-20T09:15:00Z"
}

# Collection response
{
  "data": [
    { "id": 123, "name": "Product 1", ... },
    { "id": 124, "name": "Product 2", ... }
  ],
  "pagination": {
    "total": 57,
    "page": 1,
    "per_page": 20
  }
}

# Error response
{
  "error": {
    "code": "validation_error",
    "message": "Invalid input data",
    "details": [
      { "field": "email", "message": "Must be a valid email address" },
      { "field": "age", "message": "Must be at least 18" }
    ]
  }
}
            

Consider wrapping success responses in a common envelope for consistency:


# Success response envelope
{
  "data": {
    "id": 123,
    "name": "Product Name",
    ...
  },
  "meta": {
    "request_id": "a1b2c3d4",
    "timestamp": "2025-03-25T12:34:56Z"
  }
}
            

However, be careful not to add unnecessary nesting. Some APIs prefer to keep single resource responses flat for simplicity.

HTTP Status Codes

Use HTTP status codes consistently and appropriately. Focus on a core set of codes for common scenarios:

Code Name Use Cases
200 OK Successful GET, PUT, PATCH, or DELETE that returns content
201 Created Successful POST that created a new resource
204 No Content Successful operation that returns no content (often DELETE)
400 Bad Request Client error (validation failure, malformed request)
401 Unauthorized Authentication required or failed
403 Forbidden Authentication succeeded but permission denied
404 Not Found Resource doesn't exist
422 Unprocessable Entity Request format is correct but semantically invalid
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Server error, something went wrong

For a more detailed approach, consider using additional status codes for specific scenarios:

Error Response Design

Error responses should be consistent, informative, and actionable:


# Basic error response
{
  "error": {
    "code": "resource_not_found",
    "message": "The requested resource was not found",
    "details": "No user exists with ID 12345"
  }
}

# Validation error with field-level details
{
  "error": {
    "code": "validation_error",
    "message": "The request contains invalid parameters",
    "details": [
      {
        "field": "email",
        "code": "invalid_format",
        "message": "Must be a valid email address"
      },
      {
        "field": "password",
        "code": "too_short",
        "message": "Must be at least 8 characters long"
      }
    ]
  }
}
            

Include these elements in error responses:

Success Response Patterns

For different operations, return appropriate content in success responses:

Operation Status Code Response Body
GET resource 200 The resource representation
GET collection 200 Array of resources with pagination data
POST new resource 201 The created resource, including server-generated fields
PUT/PATCH update 200 The updated resource
DELETE resource 204 No content
Action operation 200 The modified resource or operation result
sequenceDiagram participant Client participant API Client->>API: POST /users Note right of API: Create a user API-->>Client: 201 Created + User resource Client->>API: GET /users/123 Note right of API: Get user details API-->>Client: 200 OK + User resource Client->>API: PUT /users/123 Note right of API: Update user API-->>Client: 200 OK + Updated user Client->>API: DELETE /users/123 Note right of API: Delete user API-->>Client: 204 No Content Client->>API: GET /users/123 Note right of API: Get deleted user API-->>Client: 404 Not Found + Error details

Versioning and Evolution

APIs evolve over time. Effective versioning strategies help maintain backward compatibility while allowing for improvements.

Versioning Strategies

Several approaches to API versioning exist, each with advantages and disadvantages:

Approach Example Pros Cons
URL Path /v1/users Very explicit, easy to understand Resources can't be moved between versions easily
Query Parameter /users?version=1 Doesn't change resource URLs Easy to miss, inconsistent application
Header Accept: application/vnd.example.v1+json Cleanest URLs, proper use of HTTP Less visible, harder to test
Content Negotiation Accept: application/vnd.example+json;version=1.0 Most HTTP-compliant Most complex to implement and use

URL path versioning is the most common approach due to its simplicity and visibility, despite not being the most technically pure RESTful solution.

Versioning Guidelines

Non-Breaking Changes

Many changes can be made without breaking backward compatibility:

Breaking Changes

These changes typically require a new version:

API Evolution Best Practices

To minimize the need for versioning and make your API more resilient to change:

Deprecation Process

When you need to phase out part of your API:

  1. Announce deprecation well in advance (typically 6-12 months)
  2. Document alternatives for deprecated functionality
  3. Include deprecation notices in responses using a custom header:
    Deprecation: true
    Sunset: Sat, 31 Dec 2025 23:59:59 GMT
    Link: <https://api.example.com/v2/users>; rel="successor-version"
  4. Track usage of deprecated features to identify affected clients
  5. Contact users of deprecated features directly if possible
  6. Maintain the deprecated version until the sunset date

Security Best Practices

Security is a critical aspect of API design that can't be bolted on as an afterthought.

Authentication & Authorization

Implement robust authentication and authorization mechanisms:

Always use HTTPS for all API endpoints, with no exceptions for "public" data.

Input Validation

Thoroughly validate all client input to prevent injection attacks and other security issues:

Rate Limiting

Implement rate limiting to protect your API from abuse, DoS attacks, and excessive use:


# Rate limit headers in response
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1616723058

# When limit is exceeded
HTTP/1.1 429 Too Many Requests
Retry-After: 60
            

Consider different rate limit tiers based on:

Error Disclosure

Be careful about what information you disclose in error responses:

Additional Security Headers

Include security-related HTTP headers in API responses:


Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'none'
X-Content-Type-Options: nosniff
Cache-Control: no-store
Pragma: no-cache
            

Documentation

Great API documentation is essential for developer adoption and effective use of your API.

Documentation Components

Comprehensive API documentation should include:

Documentation Formats

Consider these documentation approaches:

OpenAPI has become the de facto standard for RESTful API documentation due to its widespread adoption and rich tooling ecosystem.

Interactive Documentation

Interactive API explorers like Swagger UI allow developers to:

This "try it out" functionality dramatically improves developer experience and reduces onboarding time.

Documentation as Code

Treat API documentation as code by:

This approach ensures documentation stays accurate and up-to-date as the API evolves.

Performance Considerations

Performance directly impacts the usability and adoption of your API.

Request Efficiency

Design your API to minimize the number of requests needed to accomplish common tasks:


# Field selection
GET /users?fields=id,name,email

# Embedding related resources
GET /orders?embed=customer,items

# Bulk operations
POST /users/bulk
PATCH /products?ids=1,2,3
            

Caching

Implement proper HTTP caching to reduce load and improve response times:


# Example cache headers
Cache-Control: max-age=3600, public
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 15 Mar 2025 14:30:00 GMT
            

Consider these caching strategies:

Pagination Performance

Choose pagination strategies that scale well with large datasets:

Asynchronous Operations

For long-running operations, consider asynchronous processing patterns:


# Create a job for a long operation
POST /import-jobs
{
  "file_url": "https://example.com/data.csv",
  "options": { "update_existing": true }
}

# Response with job resource
HTTP/1.1 202 Accepted
Location: /import-jobs/12345

# Check job status
GET /import-jobs/12345

# Job status response
{
  "id": "12345",
  "status": "processing",
  "progress": 60,
  "created_at": "2025-03-15T14:30:00Z",
  "estimated_completion_time": "2025-03-15T14:35:00Z"
}
            

This pattern allows clients to start operations and check progress without keeping connections open for long periods.

Case Study: Building a RESTful E-commerce API

Let's apply these best practices to design an e-commerce API.

Resource Design

Core resources for an e-commerce API:


# Primary resources
/products            # Product catalog
/categories          # Product categories
/customers           # Customer accounts
/orders              # Customer orders
/cart                # Shopping cart
/payments            # Payment transactions
/shipping-methods    # Available shipping options
/reviews             # Product reviews

# Sub-resources
/products/{id}/variants          # Product variations
/orders/{id}/items               # Line items in an order
/customers/{id}/addresses        # Customer addresses
/customers/{id}/payment-methods  # Saved payment methods

# Specialized resources
/search              # Product search
/recommendations     # Personalized product recommendations
/inventory           # Stock levels
/webhooks            # Event notifications
            

Common Operations

Operation Method + URL Description
List products GET /products Retrieve products with filtering options
Get product details GET /products/{id} Get complete product information
Add to cart POST /cart/items Add a product to the shopping cart
Update cart quantity PATCH /cart/items/{id} Change quantity of an item in cart
Create an order POST /orders Convert cart to an order
Process payment POST /payments Make a payment for an order
Cancel order POST /orders/{id}/cancel Cancel an existing order
Search products GET /search?q=keyword Search for products by keyword

Example Requests and Responses


# Get product with related data
GET /products/123?fields=id,name,price,stock,variants&embed=reviews,categories

# Response
{
  "id": 123,
  "name": "Ergonomic Office Chair",
  "price": 299.99,
  "stock": 15,
  "variants": [
    {
      "id": 456,
      "name": "Black",
      "price_modifier": 0
    },
    {
      "id": 457,
      "name": "Red",
      "price_modifier": 20
    }
  ],
  "reviews": [
    {
      "id": 789,
      "rating": 5,
      "comment": "Very comfortable!",
      "author": "John D."
    }
  ],
  "categories": [
    {
      "id": 22,
      "name": "Office Furniture"
    }
  ]
}

# Add item to cart
POST /cart/items
{
  "product_id": 123,
  "variant_id": 456,
  "quantity": 2
}

# Response
{
  "cart": {
    "id": "c789",
    "items": [
      {
        "id": "ci456",
        "product_id": 123,
        "variant_id": 456,
        "name": "Ergonomic Office Chair - Black",
        "quantity": 2,
        "unit_price": 299.99,
        "total_price": 599.98
      }
    ],
    "subtotal": 599.98,
    "tax": 48.00,
    "shipping": 0,
    "total": 647.98,
    "_links": {
      "self": { "href": "/cart" },
      "checkout": { "href": "/checkout" }
    }
  }
}
            

Error Scenarios


# Product not found
GET /products/999
HTTP/1.1 404 Not Found
{
  "error": {
    "code": "resource_not_found",
    "message": "The requested product could not be found",
    "request_id": "req_abcd1234"
  }
}

# Add to cart with insufficient stock
POST /cart/items
{
  "product_id": 123,
  "quantity": 100
}

HTTP/1.1 422 Unprocessable Entity
{
  "error": {
    "code": "insufficient_stock",
    "message": "The requested quantity exceeds available stock",
    "details": {
      "available": 15,
      "requested": 100
    }
  }
}
            

Complex Operations

For the checkout process, we might implement a workflow using multiple endpoints:

  1. Review cart: GET /cart
  2. Set shipping address: PUT /cart/shipping-address
  3. Get shipping options: GET /shipping-methods?destination={shipping_address_id}
  4. Select shipping method: PUT /cart/shipping-method
  5. Create order (reserves inventory): POST /orders
  6. Process payment: POST /payments
  7. Confirm order: POST /orders/{id}/confirm

This multi-step process allows for flexibility while maintaining RESTful principles.

Practice Activities

Activity 1: Resource Design

Design resources for a blog API that supports these features:

For each resource, define:

Activity 2: Response Design

Create response formats for the following API scenarios:

  1. Successfully creating a new user account
  2. Validation error when creating a user account
  3. Listing orders with pagination
  4. Getting a single product with related products
  5. Authentication failure
  6. Rate limit exceeded

Include appropriate status codes, headers, and response bodies for each scenario.

Activity 3: API Evolution

Consider an existing API endpoint for user profiles:


GET /users/123
{
  "id": 123,
  "username": "johndoe",
  "email": "john@example.com",
  "name": "John Doe",
  "created_at": "2025-01-15T12:00:00Z"
}
            

Design how you would implement these changes:

  1. Adding user preferences (non-breaking change)
  2. Splitting "name" into "first_name" and "last_name" (breaking change)
  3. Adding user roles and permissions (mixed changes)
  4. Supporting both old and new formats during a transition period

Explain your versioning strategy and how you would communicate these changes to API consumers.

Summary

In this lecture, we've explored comprehensive best practices for designing effective RESTful APIs:

By following these practices, you can create APIs that are intuitive, stable, and maintainable—providing an excellent developer experience while meeting your system's technical requirements.

Further Reading