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

PrincipleExample
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

MethodPurposeIdempotentSafe
GETRetrieve resource(s)YesYes
POSTCreate resourceNoNo
PUTReplace resourceYesNo
PATCHPartial updateNoNo
DELETERemove resourceYesNo

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

CodeMeaningWhen to Use
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST creating resource
204No ContentSuccessful DELETE
400Bad RequestInvalid request syntax or parameters
401UnauthorizedMissing or invalid authentication
403ForbiddenValid auth but insufficient permissions
404Not FoundResource doesn’t exist
409ConflictRequest conflicts with current state
422Unprocessable EntityValid syntax but semantic errors
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer-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

MethodUse Case
API KeysServer-to-server, simple integrations
OAuth 2.0User-authorized access, third-party apps
JWTStateless 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

StrategyExampleProsCons
URL Path/v1/usersExplicit, easy routingURL changes
HeaderAccept: application/vnd.api+json;version=1Clean URLsHidden
Query Param/users?version=1SimpleCaching 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