Guide 10 min read API REST Design

API Design Best Practices

Principles for designing APIs that are secure, scalable, and developer-friendly.

API Design Best Practices

Principles and patterns for designing APIs that developers want to use and operations teams can support.


Foundational Principles

1. Design for the Consumer

APIs exist to serve their consumers. Every design decision should consider:

  • Who will use this API?
  • What are they trying to accomplish?
  • What is their technical sophistication?
  • How will they discover and learn the API?

2. Be Consistent

Consistency reduces cognitive load. Establish conventions and follow them:

  • Naming patterns
  • Error formats
  • Pagination approach
  • Authentication method

3. Be Explicit

Implicit behavior creates confusion. Make everything explicit:

  • Document all behaviors
  • Use clear, unambiguous names
  • Provide examples for every operation

Resource Design

URL Structure

Pattern: /{version}/{resource}/{identifier}/{sub-resource}

Good:

GET /v1/users/123/orders
POST /v1/orders
PUT /v1/orders/456
DELETE /v1/orders/456

Avoid:

GET /getUser?id=123
POST /createOrder
PUT /updateOrder/456
DELETE /deleteOrder/456

Resource Naming

Principle Example
Use nouns, not verbs /orders not /getOrders
Use plural forms /users not /user
Use lowercase /users not /Users
Use hyphens for readability /user-accounts not /userAccounts

HTTP Methods

Method Purpose Idempotent Safe
GET Retrieve resource(s) Yes Yes
POST Create resource No No
PUT Replace resource Yes No
PATCH Partial update No No
DELETE Remove resource Yes No

Request Design

Query Parameters

Use for filtering, sorting, and pagination:

GET /v1/orders?status=pending&sort=-created_at&page=2&limit=20

Conventions:

  • filter[field]=value for filtering
  • sort=field (prefix - for descending)
  • page and limit or offset and limit for pagination
  • fields=field1,field2 for sparse fieldsets

Request Bodies

Use JSON with consistent structure:

{
  "data": {
    "type": "order",
    "attributes": {
      "customer_id": "123",
      "items": [
        {
          "product_id": "456",
          "quantity": 2
        }
      ]
    }
  }
}

Conventions:

  • Use snake_case for property names (or camelCase—just be consistent)
  • Wrap payloads in data envelope if using JSON:API style
  • Include type for polymorphic resources

Response Design

Successful Responses

Single Resource:

{
  "data": {
    "id": "123",
    "type": "order",
    "attributes": {
      "status": "pending",
      "total": 99.99,
      "created_at": "2024-01-15T10:30:00Z"
    },
    "relationships": {
      "customer": {
        "data": { "id": "456", "type": "customer" }
      }
    }
  }
}

Collection:

{
  "data": [...],
  "meta": {
    "total": 100,
    "page": 2,
    "limit": 20
  },
  "links": {
    "self": "/v1/orders?page=2",
    "first": "/v1/orders?page=1",
    "prev": "/v1/orders?page=1",
    "next": "/v1/orders?page=3",
    "last": "/v1/orders?page=5"
  }
}

Status Codes

Code Meaning When to Use
200 OK Successful GET, PUT, PATCH
201 Created Successful POST creating resource
204 No Content Successful DELETE
400 Bad Request Invalid request syntax or parameters
401 Unauthorized Missing or invalid authentication
403 Forbidden Valid auth but insufficient permissions
404 Not Found Resource doesn’t exist
409 Conflict Request conflicts with current state
422 Unprocessable Entity Valid syntax but semantic errors
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Server-side failure

Error Responses

Provide actionable error information:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request contains invalid data",
    "details": [
      {
        "field": "email",
        "code": "INVALID_FORMAT",
        "message": "Must be a valid email address"
      },
      {
        "field": "quantity",
        "code": "OUT_OF_RANGE",
        "message": "Must be between 1 and 100"
      }
    ],
    "request_id": "req_abc123"
  }
}

Authentication & Authorization

Authentication Methods

Method Use Case
API Keys Server-to-server, simple integrations
OAuth 2.0 User-authorized access, third-party apps
JWT Stateless authentication, microservices

Best Practices

  1. Use HTTPS exclusively—no exceptions
  2. Never expose credentials in URLs—use headers
  3. Implement proper token expiration—short-lived access tokens
  4. Use scopes for authorization—principle of least privilege
  5. Rate limit by client—prevent abuse

Header Conventions

Authorization: Bearer <token>
X-API-Key: <key>
X-Request-ID: <uuid>

Versioning

Strategies

Strategy Example Pros Cons
URL Path /v1/users Explicit, easy routing URL changes
Header Accept: application/vnd.api+json;version=1 Clean URLs Hidden
Query Param /users?version=1 Simple Caching issues

Recommendation: URL path versioning for simplicity and explicitness.

Version Lifecycle

  1. Current: Actively developed and supported
  2. Deprecated: Still functional, no new features, sunset date announced
  3. Sunset: No longer available

Communicate version status in responses:

Deprecation: true
Sunset: Sat, 31 Dec 2024 23:59:59 GMT
Link: </v2/users>; rel="successor-version"

Pagination

Offset-Based

GET /v1/orders?page=2&limit=20

Response:

{
  "data": [...],
  "meta": {
    "total": 100,
    "page": 2,
    "limit": 20,
    "total_pages": 5
  }
}

Best for: Stable datasets, random access needed

Cursor-Based

GET /v1/orders?limit=20&cursor=eyJpZCI6MTIzfQ==

Response:

{
  "data": [...],
  "meta": {
    "has_more": true,
    "next_cursor": "eyJpZCI6MTQzfQ=="
  }
}

Best for: Large datasets, real-time data, infinite scroll


Rate Limiting

Headers

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1609459200

429 Response

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests",
    "retry_after": 60
  }
}

Include Retry-After header.


Documentation

What to Document

For each endpoint:

  • HTTP method and URL
  • Description of purpose
  • Authentication requirements
  • Request parameters (path, query, body)
  • Request examples
  • Response formats and examples
  • Error codes and meanings
  • Rate limits

OpenAPI Specification

Use OpenAPI (Swagger) for machine-readable documentation:

paths:
  /v1/orders:
    get:
      summary: List orders
      description: Returns a paginated list of orders
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, completed, cancelled]
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderList'

Security Checklist

  • HTTPS only (redirect HTTP to HTTPS)
  • Authentication on all non-public endpoints
  • Input validation on all parameters
  • Output encoding to prevent injection
  • Rate limiting implemented
  • CORS configured appropriately
  • Security headers set (CSP, X-Frame-Options, etc.)
  • Sensitive data not logged
  • Audit logging for sensitive operations
  • API keys/tokens rotatable

Performance Considerations

Caching

Use appropriate cache headers:

Cache-Control: max-age=3600, public
ETag: "abc123"
Last-Modified: Wed, 15 Jan 2024 10:30:00 GMT

Compression

Support gzip/deflate:

Accept-Encoding: gzip, deflate
Content-Encoding: gzip

Sparse Fieldsets

Allow clients to request only needed fields:

GET /v1/orders/123?fields=id,status,total

For API design review or architectural guidance, contact our team.

Need help implementing these practices?

Our team can help you apply these frameworks to your specific context.

Get in Touch