Why API Design Decisions Are Permanent

API design decisions are uniquely consequential because APIs are public contracts. Once clients depend on your endpoint structure, status codes, or response shapes, changing them breaks their code. The patterns in this guide are not preferences — they are conventions that exist because they prevent breaking changes, reduce client-side bugs, and make your API predictable to any developer who has worked with REST before.

Resource Naming: Nouns, Not Verbs

The URL identifies a resource. HTTP methods describe what to do with it. Therefore, URLs should be nouns — never verbs.

❌ Bad (verb-heavy, RPC-style):
GET  /getUser/123
POST /createPost
PUT  /updatePost/456
POST /deleteUser/123

✅ Good (noun-based, resource-oriented):
GET    /users/123
POST   /posts
PUT    /posts/456
DELETE /users/123

Always use plural nouns for collection endpoints: /users not /user. This makes the pattern consistent — /users is the collection, /users/123 is a specific user, and this holds true regardless of whether you're reading one or many.

Use hierarchical nesting to express relationships, but only one level deep in most cases:

GET  /users/123/posts          # Posts belonging to user 123
POST /users/123/posts          # Create a post for user 123
GET  /users/123/posts/456      # Specific post belonging to user 123

# Avoid deep nesting beyond two levels:
❌ /users/123/posts/456/comments/789/likes   # Too deep — use /comments/789/likes instead

HTTP Methods: What Each One Means

  • GET — Retrieve a resource or collection. Safe (no side effects) and idempotent (same result on repeated calls). Never use GET for operations that modify data.
  • POST — Create a new resource. Not idempotent — calling POST twice creates two records. Returns 201 Created with a Location header pointing to the new resource.
  • PUT — Replace a resource entirely. Idempotent — calling PUT multiple times produces the same result. The client sends the complete representation of the resource.
  • PATCH — Partial update. Only sends the fields to change. Not guaranteed idempotent (depends on implementation).
  • DELETE — Remove a resource. Idempotent — deleting an already-deleted resource should return 204 or 404, not an error that crashes the client.

HTTP Status Codes: The Complete Reference for API Developers

Status codes communicate the result of an operation at the protocol level — before the client even reads the response body. Using the wrong status code forces clients to parse the body just to determine if the request succeeded.

2xx — Success

  • 200 OK — Generic success for GET, PUT, PATCH. Response body contains the resource.
  • 201 Created — Resource was successfully created (POST). Include a Location: /users/123 header with the URL of the new resource.
  • 204 No Content — Success with no response body. Use for DELETE operations or PATCH/PUT when you do not need to return the updated resource.

4xx — Client Errors

  • 400 Bad Request — The request is malformed (invalid JSON, missing required fields that fail basic parsing).
  • 401 Unauthorized — No valid authentication credentials. The client should authenticate and retry.
  • 403 Forbidden — Authenticated but not authorized. The client is identified but lacks permission for this action.
  • 404 Not Found — The resource does not exist (or is being hidden for security).
  • 409 Conflict — The request conflicts with the current state (e.g., creating a user with an email that already exists).
  • 422 Unprocessable Entity — The request is syntactically valid but semantically wrong (field validation failures). This is the correct code for form validation errors, not 400.
  • 429 Too Many Requests — Rate limit exceeded. Include a Retry-After header with the number of seconds to wait.

5xx — Server Errors

  • 500 Internal Server Error — Unexpected server failure. Log the error, return a generic message to the client (never stack traces).
  • 503 Service Unavailable — Server is temporarily down for maintenance. Include Retry-After.

Consistent Request/Response Envelope

Define a consistent response structure that all your endpoints follow. Clients should be able to parse every response with the same code path:

// Success response
{
  "data": { "id": "123", "name": "Project Alpha", "status": "active" },
  "meta": { "timestamp": "2026-06-20T10:00:00Z", "request_id": "req_abc123" }
}

// Collection response
{
  "data": [...],
  "meta": {
    "total": 248,
    "page": 2,
    "per_page": 20,
    "next_cursor": "eyJpZCI6MTIzfQ"
  }
}

// Error response
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "field": "email", "message": "Must be a valid email address" },
      { "field": "name", "message": "Required" }
    ]
  }
}

The code field in errors should be a machine-readable string constant — not an HTTP status code, and not a human-readable message (those can change). Clients can branch on error.code without parsing the human message.

API Versioning Strategies

There is no perfect versioning strategy — only trade-offs. The three main approaches:

URL path versioning (/api/v1/users) is the most common and most visible. Easy to implement, easy to route, easy for developers to understand. The downside: it creates the illusion that the entire API changes with a version bump, when only specific endpoints might change. This is the pragmatic choice for most teams.

Accept header versioning (Accept: application/vnd.myapi.v2+json) keeps URLs clean and is technically the most REST-correct approach (version is a representation concern, not a resource concern). The downside: harder to test in a browser, less visible in logs, and requires clients to set headers explicitly.

Custom header versioning (X-API-Version: 2) is a middle ground — clean URLs, easier to test than Accept headers. But custom headers are not cached by default CDN configurations and require explicit client implementation.

Recommendation for SaaS APIs: use URL path versioning (/api/v1/), maintain backward compatibility within a version, and provide a deprecation timeline before removing a version.

Authentication Patterns

Bearer JWT tokens are standard for user-facing APIs. The client includes the token in every request:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

API Keys are better for server-to-server integrations. Typically sent in a custom header (X-API-Key: sk_live_abc123) rather than a query parameter — query params get logged in server logs and browser history. Store API keys hashed in your database (SHA-256), never in plaintext, just like passwords.

Never put authentication tokens or API keys in the URL path or query string (/users?api_key=secret). URLs appear in server logs, access logs, browser history, and CDN caches.

Rate Limiting Headers

Include rate limit information in every response so clients can implement adaptive throttling:

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000        # Requests allowed per window
X-RateLimit-Remaining: 847     # Requests remaining in current window
X-RateLimit-Reset: 1750420800  # Unix timestamp when window resets

# When limit is exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 60               # Seconds until client can retry

Pagination: Offset vs Cursor

Offset-based pagination (?page=2&limit=20 or ?offset=20&limit=20) is simple to implement and allows jumping to any page. The problem: it breaks on live data. If a new record is inserted while a user is paginating, records shift and the user may see duplicates or skip records. Appropriate for static datasets, admin panels, and search results where "jump to page 50" matters.

Cursor-based pagination is more complex but handles live data correctly. The response includes a next_cursor token (usually a Base64-encoded timestamp or ID of the last record). The next request sends ?cursor=eyJpZCI6MTIzfQ&limit=20, and the query becomes WHERE id < 123 ORDER BY id DESC LIMIT 20. There is no page jumping, but the list is stable. Use cursor pagination for feeds, activity logs, and any list where new data is frequently inserted.

OpenAPI 3.0: Document Your API

OpenAPI 3.0 (formerly Swagger) is the standard format for describing REST APIs. A well-maintained OpenAPI spec lets you auto-generate client SDKs, import into Postman, deploy interactive documentation, and run contract tests. At minimum, document your production endpoints:

# openapi.yaml (excerpt)
openapi: 3.0.3
info:
  title: My SaaS API
  version: 1.0.0
paths:
  /api/v1/projects:
    get:
      summary: List user's projects
      security:
        - bearerAuth: []
      parameters:
        - name: cursor
          in: query
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, default: 20, maximum: 100 }
      responses:
        '200':
          description: Paginated list of projects
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Project'
                  meta:
                    $ref: '#/components/schemas/PaginationMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'