ESC
Type to search guides, tutorials, and reference documentation.
Verified by Garnet Grid

API Design That Developers Actually Want to Use

Design REST and GraphQL APIs that are intuitive, consistent, and evolvable. Covers naming conventions, error handling, pagination, versioning, rate limiting, and the documentation practices that reduce support tickets.

A good API is one that developers can use correctly without reading the documentation. A great API is one where developers read the documentation anyway because it is that well-written. Most APIs are neither — they are collections of endpoints that grew organically, where every endpoint returns errors differently, naming conventions change halfway through, and the pagination format is a surprise every time.

This guide covers how to design APIs that are consistent, predictable, and pleasant to work with — because your API is your product’s developer experience.


REST API Naming Conventions

Resources are nouns (plural), actions are HTTP methods:

  GET    /users              → List users
  POST   /users              → Create a user
  GET    /users/123          → Get user 123
  PUT    /users/123          → Replace user 123
  PATCH  /users/123          → Update user 123 (partial)
  DELETE /users/123          → Delete user 123

Nested resources:
  GET    /users/123/orders   → List orders for user 123
  POST   /users/123/orders   → Create order for user 123

Avoid:
  ❌ GET /getUser/123         (verb in URL)
  ❌ POST /users/create       (action in URL)
  ❌ GET /user/123            (singular resource name)
  ❌ GET /Users/123           (capitalized)
  ❌ GET /user_orders/123     (mixed naming with underscores)

URL Design Rules

RuleExampleWhy
Plural nouns for collections/orders not /orderConsistency — collection vs item distinguished by presence/absence of ID
Kebab-case for multi-word/order-items not /orderItemsURLs are case-insensitive; hyphens are standard
No verbs in URLs/users/123/activatePATCH /users/123 {status: "active"}HTTP method IS the verb
Max 2 levels of nesting/users/123/orders not /users/123/orders/456/items/789Deep nesting is hard to use; flatten with top-level resources

Error Handling: Be Specific and Helpful

// ❌ Bad: tells you nothing
{
  "error": "Bad Request"
}

// ❌ Also bad: internal details leaked
{
  "error": "NullPointerException at OrderService.java:142"
}

// ✅ Good: specific, actionable, documented
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address",
        "value": "not-an-email"
      },
      {
        "field": "quantity",
        "message": "Must be between 1 and 100",
        "value": 0
      }
    ],
    "request_id": "req-abc-123"
  }
}

HTTP Status Codes: Use the Right One

StatusWhen to UseCommon Mistakes
200Success
201Resource created (POST)Using 200 for creation
204Success, no content (DELETE)Returning 200 with empty body
400Client sent invalid dataUsing for server errors
401Authentication required (no valid credentials)Confused with 403
403Authenticated but not authorizedConfused with 401
404Resource not foundUsing for authorization failures (leaks resource existence)
409Conflict (duplicate, state conflict)Using 400 for conflicts
422Semantically invalid (valid JSON, invalid business logic)Using 400 for everything
429Rate limit exceededNot implementing rate limiting
500Server error (unexpected)Returning for client errors

Pagination

PatternBest ForExample
OffsetSimple, small datasets, UI “page 3 of 40”?offset=40&limit=20
CursorLarge datasets, real-time data, infinite scroll?cursor=eyJpZCI6MTIzfQ&limit=20
KeysetSorted results, database-friendly?after_id=123&limit=20

Cursor-Based Pagination Response

{
  "data": [
    {"id": "order_124", "total": 49.99},
    {"id": "order_125", "total": 29.99}
  ],
  "pagination": {
    "has_next": true,
    "next_cursor": "eyJpZCI6MTI1fQ",
    "has_prev": true,
    "prev_cursor": "eyJpZCI6MTI0fQ"
  },
  "meta": {
    "total_count": 1847,
    "per_page": 20
  }
}

Versioning

StrategyURL ExampleHeader ExampleTradeoff
URL path/v2/usersSimple, visible, but pollutes URL
Header/usersAPI-Version: 2Clean URLs, harder to discover
Query param/users?version=2Easy to use, easy to forget

Recommendation: Use URL path versioning (/v1/, /v2/). It is the most visible and hardest to forget. Only increment the major version for breaking changes (removed fields, renamed endpoints, changed semantics).

What Counts as a Breaking Change

Breaking ❌Non-Breaking ✅
Removing a field from responseAdding a new field to response
Changing a field type (string → number)Adding a new optional query parameter
Renaming an endpointAdding a new endpoint
Changing error code formatAdding new error codes
Making an optional field requiredMaking a required field optional

Rate Limiting

Rate Limit Response Headers:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100           # Max requests per window
X-RateLimit-Remaining: 0         # Requests remaining
X-RateLimit-Reset: 1625766000    # Unix timestamp when limit resets
Retry-After: 30                  # Seconds to wait before retrying
StrategyHow It WorksBest For
Fixed window100 requests per minute, reset at :00Simple, easy to understand
Sliding window100 requests in any 60-second periodSmoother, prevents burst at window boundary
Token bucketRefill tokens at steady rate, burst up to bucket sizeAllows bursts, rate-limited over time

Documentation

Documentation ElementWhy It Matters
OpenAPI/Swagger specMachine-readable, generates client libraries
Authentication guideFirst thing developers need
Quick start (< 5 minutes)Proves the API works before deep dive
Error catalogEvery error code with cause and resolution
Rate limit documentationPrevents surprises
ChangelogDevelopers know what changed and when
Code examples (curl, Python, JS)Copy-paste-friendly

Implementation Checklist

  • Use plural nouns for resources, HTTP methods for actions
  • Return consistent error objects with code, message, details, and request_id
  • Use correct HTTP status codes (especially 401 vs 403, 400 vs 422)
  • Implement cursor-based pagination for list endpoints
  • Version your API with URL path versioning (/v1/)
  • Add rate limiting with proper response headers (Limit, Remaining, Reset)
  • Include request_id in every response for debugging
  • Generate OpenAPI spec and keep it in sync with implementation
  • Write a quick-start guide that takes < 5 minutes
  • Document every error code with cause and resolution steps
Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →