How to Design APIs That Developers Actually Want to Use

A bad API doesn't just frustrate developers. It loses them. This guide covers the principles, patterns, and concrete decisions that separate the APIs developers love from the ones they dread — with real examples you can apply immediately.

19 min read
...
Software
How to Design APIs That Developers Actually Want to Use

A bad API doesn't just frustrate developers. It loses them.

Developers abandon APIs that take hours instead of minutes to integrate. The difference between a 5-minute and 5-hour first API call often comes down to design choices made months earlier. Poor API design creates fragile integrations, inconsistent experiences across endpoints, and mounting support costs as developers hit the same obstacles repeatedly.

The stakes are real. Stripe built a $91 billion business on the back of an API so well-designed that developers actively recommend it to each other. Twilio turned SMS and voice into a two-line code snippet. These companies didn't just build APIs — they built developer experiences that felt effortless. The technology was almost secondary to the design.

In 2026, the bar is higher than ever. Modern businesses treat APIs not just as technical utilities but as strategic assets. Robust APIs determine an organisation's speed and flexibility, enabling partners and teams to integrate seamlessly. And with AI agents increasingly consuming APIs autonomously, the old excuse of "developers will figure it out from the code" no longer holds. Your API needs to be legible to machines as well as humans.

This guide covers the principles, patterns, and concrete decisions that separate the APIs developers love from the ones they dread — with real examples you can apply immediately.


What makes an API genuinely good?

A well-designed API delights developers, enables integration, and scales with your product. A poorly designed API frustrates users, limits adoption, and creates technical debt.

Three qualities define an API developers actually want to use:

Predictability — predictability reduces cognitive load. Developers can make accurate assumptions about unfamiliar endpoints based on their experience with familiar ones, which speeds up API integration and reduces support requests. If your API is consistent, developers only need to learn its patterns once.

Honesty — a good API tells you clearly what went wrong, what it expects, and what it returned. It doesn't make you guess. It doesn't return a 200 with an error buried in the response body.

Stability — developers build on APIs. Breaking changes aren't just inconvenient — they break production systems in ways that create real costs and erode trust permanently.

Let's build all three.


1. Design your URL structure around resources, not actions

The most common API design mistake is building URLs around what the server does rather than what it represents. REST is resource-oriented: your URLs describe things, and HTTP methods describe what you're doing to them.

Bad — action-oriented URLs:

POST /createUser
GET  /getUser?id=123
POST /updateUser
POST /deleteUser
POST /getUserOrders

Good — resource-oriented URLs:

POST   /users              → Create a user
GET    /users/123          → Get a specific user
PATCH  /users/123          → Update a user
DELETE /users/123          → Delete a user
GET    /users/123/orders   → Get orders for a user

The second pattern is immediately learnable. A developer who's never seen your API before can make a reasonable guess at every endpoint. That's the goal.

Rules that make URL design consistent:

Use plural nouns for collections, always. /users, not /user. Consistency matters more than grammatical correctness. Always use /users rather than /user, even when retrieving a single user at /users/123. One rule is easier to remember than conditional rules.

Keep hierarchy shallow. Deep nesting like /users/123/orders/456/items/789/notes becomes unwieldy quickly. Prefer shallower hierarchies, and consider whether nested resources could work as top-level resources with filtering instead.

Use kebab-case for multi-word paths: /user-accounts, not /userAccounts or /user_accounts. URLs are case-insensitive and kebab-case is the web convention.

Filter with query parameters, not separate endpoints:

# ❌ Too many endpoints for the same resource
GET /active-users
GET /pending-orders
GET /completed-orders

# ✅ One endpoint, filter with parameters
GET /users?status=active
GET /orders?status=pending
GET /orders?status=completed&userId=123

Fewer, well-designed endpoints typically outperform many specialised ones. A single /orders endpoint with filtering parameters is often more maintainable than separate endpoints for /pending-orders, /completed-orders, and /cancelled-orders. Simpler APIs are easier to document, easier to test, and easier to evolve over time.


2. Use HTTP methods and status codes correctly — every time

HTTP methods have defined semantics. Ignoring them is one of the fastest ways to confuse developers integrating with your API.

Method Use it for Safe? Idempotent?
GET Retrieve a resource or collection ✅ Yes ✅ Yes
POST Create a new resource ❌ No ❌ No
PUT Replace a resource entirely ❌ No ✅ Yes
PATCH Partially update a resource ❌ No ❌ Not always
DELETE Remove a resource ❌ No ✅ Yes

Safe means the request doesn't modify state. Idempotent means sending the request multiple times has the same effect as sending it once. These properties matter for caching, retries, and client-side error handling.

Status codes tell the full story — use the right ones:

200 OK           → Successful GET, PUT, PATCH
201 Created      → Successful POST (resource created)
204 No Content   → Successful DELETE (nothing to return)
400 Bad Request  → Client sent invalid data
401 Unauthorized → Not authenticated
403 Forbidden    → Authenticated but not authorised
404 Not Found    → Resource doesn't exist
409 Conflict     → State conflict (duplicate email, stale update)
422 Unprocessable Entity → Validation failed
429 Too Many Requests    → Rate limit hit
500 Internal Server Error → Your fault, not theirs

The cardinal sin: returning 200 OK with {"error": "User not found"} in the body. This breaks every HTTP client, monitoring tool, and retry logic that depends on status codes to understand what happened.

Never do this:

HTTP/1.1 200 OK

{
  "success": false,
  "error": "Invalid email address"
}

Do this instead:

HTTP/1.1 422 Unprocessable Entity

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" },
      { "field": "name",  "message": "Name is required" }
    ]
  },
  "meta": {
    "requestId": "req_abc123",
    "timestamp": "2026-05-05T10:30:00Z"
  }
}

3. Write error responses developers can actually act on

Error messages are where most APIs fail their users most visibly. A cryptic error response at 2 AM in a production incident is not just unhelpful — it's hostile.

A great error response answers four questions instantly:

  1. What went wrong? — the code field, machine-readable, stable across versions
  2. Why did it go wrong? — the message field, human-readable, plain English
  3. Where did it go wrong? — field-level details for validation errors
  4. How do I fix it? — a docs_url link to the relevant documentation section
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "You have exceeded the rate limit of 1000 requests per hour.",
    "details": {
      "limit": 1000,
      "remaining": 0,
      "reset_at": "2026-05-05T11:00:00Z"
    },
    "docs_url": "https://docs.yourapi.com/errors/rate-limiting"
  },
  "meta": {
    "requestId": "req_xyz789",
    "timestamp": "2026-05-05T10:47:00Z"
  }
}

The requestId in the meta field is worth emphasising. Include a unique request ID in every response — success or error. It's the single most important field for debugging production issues: developers can send you the ID, and you can trace the exact request in your logs without a lengthy back-and-forth.

Error codes should be machine-readable strings, not numbers. VALIDATION_ERROR is self-documenting. 4012 requires a lookup table every time.


4. Version your API from day one

Not versioning your API is a decision you'll regret the moment you need to make your first breaking change. By then, you have customers in production that you can't break without destroying trust.

The three main versioning strategies:

URL versioning — the most explicit and widely used:

GET /v1/users/123
GET /v2/users/123

Pros: immediately visible, easy to route, easy to document separately. Cons: URLs change with major versions, which feels un-REST-y to purists.

Header versioning:

GET /users/123
Api-Version: 2026-05-01

Stripe uses date-based header versioning. Every API change is a permanent, documented event tied to a date. This forces you to treat every breaking change with the seriousness it deserves. Companies like Stripe use headers with dates like "2026-04-15." This forces you to treat every API change as a permanent, documented event, rather than just bumping a "v1" to "v2."

Query parameter versioning:

GET /users/123?version=2

Easiest to implement, hardest to cache and enforce. Generally the weakest option.

For most teams, URL versioning is the right default — it's the most transparent and the easiest to reason about. Start with /v1 and don't create /v2 until you have a genuinely breaking change that can't be handled any other way.

What counts as a breaking change:

  • Removing or renaming a field
  • Changing a field's data type
  • Changing authentication requirements
  • Removing an endpoint
  • Changing error response structure

What doesn't require a version bump:

  • Adding new optional fields to responses
  • Adding new optional request parameters
  • Adding new endpoints
  • Bug fixes that bring behaviour in line with documented behaviour

Keep old versions running long enough for clients to migrate. Announce deprecations with clear timelines. Send Deprecation and Sunset headers on deprecated endpoints so clients can detect the deadline programmatically:

Deprecation: true
Sunset: Sat, 01 Nov 2026 00:00:00 GMT
Link: <https://docs.yourapi.com/migration/v2>; rel="successor-version"

5. Handle pagination, filtering, and sorting consistently

Any endpoint that returns a collection needs pagination. Returning all records is a DoS attack waiting to happen and a client-side performance disaster on any non-trivial dataset.

Cursor-based pagination is the modern standard for large, frequently updated datasets:

GET /users?limit=20&cursor=eyJpZCI6MTAwfQ==

{
  "data": [...],
  "meta": {
    "limit": 20,
    "hasMore": true,
    "nextCursor": "eyJpZCI6MTIwfQ=="
  }
}

Cursors are stable — if items are inserted or deleted while a client is paginating, cursor-based pagination doesn't skip or duplicate records. Offset pagination (?page=3&limit=20) is simpler but breaks under concurrent writes.

Offset pagination is acceptable for smaller, stable datasets:

GET /users?page=1&limit=20

{
  "data": [...],
  "meta": {
    "total": 100,
    "page": 1,
    "limit": 20,
    "totalPages": 5
  },
  "links": {
    "self": "/users?page=1",
    "next": "/users?page=2",
    "last": "/users?page=5"
  }
}

Filtering and sorting conventions — be consistent:

GET /orders?status=pending&userId=123
GET /orders?createdAfter=2026-01-01&createdBefore=2026-04-01
GET /users?sort=createdAt&order=desc
GET /users?sort=-createdAt          # Prefix minus = descending (common shorthand)

Pick one convention for sort direction and use it everywhere. Never mix order=desc on some endpoints and sort=-field on others.


6. Design your authentication the right way

Authentication is where bad decisions become permanent. Choose the wrong approach early and you'll be supporting legacy auth methods for years while also running the new one.

The 2026 standard stack:

API Keys — for server-to-server communication, internal tools, and simple integrations. Fast to implement, easy to revoke. Pass in the header, not the query string (query strings appear in server logs):

Authorization: Bearer sk_live_abc123xyz
# Not: GET /users?api_key=sk_live_abc123xyz

OAuth 2.0 — for user-delegated access. When your API acts on behalf of a user, OAuth is the right choice. The Authorization Code flow for web apps, PKCE for mobile and single-page apps.

JWT (JSON Web Tokens) — for stateless authentication where the server doesn't need to look up session state on every request. The token carries the claims. Short expiry (15 minutes), refreshed with a longer-lived refresh token.

Security baseline every API must implement:

HTTPS only — no exceptions, no fallback to HTTP
Rate limiting — protect every endpoint, especially auth endpoints
Input validation — never trust client input
RBAC — users get only the permissions their role requires
Scopes — API keys and tokens should declare what they can access

Authentication, authorisation, and input validation are never afterthoughts in modern API design. Role-based access control (RBAC) has become standard practice — users receive permissions based on their role, and the API enforces those permissions consistently across every request.

On rate limiting — always include rate limit information in your response headers so clients can handle limits gracefully instead of discovering them in errors:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1746439200
Retry-After: 3600   # On 429 responses

7. Treat documentation as a design deliverable

Documentation is the user interface for your API. If developers can't figure out authentication from your docs, your API design failed, regardless of how elegant the underlying architecture is.

The APIs developers recommend to each other — Stripe, Twilio, Plaid — share one quality beyond their technical design: their documentation is exceptional. Companies like Stripe and Twilio set the industry standard, proving that exceptional documentation is a competitive advantage.

What great API documentation includes:

A getting started guide that takes a developer from zero to their first successful API call in under 5 minutes. Authentication setup, a simple request, a real response. Don't make them read reference docs before they've seen the API work.

Working code examples in every major language. Not pseudocode — real, copy-pasteable code that actually runs:

# Python
import requests

response = requests.get(
    'https://api.yourservice.com/v1/users/123',
    headers={'Authorization': 'Bearer YOUR_API_KEY'}
)
user = response.json()
print(user['data']['name'])

// Node.js
const response = await fetch('https://api.yourservice.com/v1/users/123', {
  headers: { 'Authorization': 'Bearer YOUR_API_KEY' }
});
const { data } = await response.json();
console.log(data.name);

An interactive API reference built from your OpenAPI spec — where developers can make real calls from the docs page without leaving their browser. Readme, Stoplight, and Redocly all generate this from an OpenAPI 3.1 file.

A changelog that documents every change — especially breaking changes — in plain English. Developers need to know what changed, when, and what they need to do about it.

An error code directory listing every possible error code, what causes it, and exactly how to fix it.

The OpenAPI 3.1 spec is the foundation. Write it first, generate documentation from it, validate requests and responses against it, and generate client SDKs from it. By 2026, OpenAPI 3.1 has become the absolute standard. Its biggest benefit is that it fully aligns with JSON Schema Draft 2020-12 — your documentation and your code validation use the exact same rules, eliminating a huge source of bugs.


8. Design for AI agent consumption

In 2026, a growing proportion of API consumers are not humans — they're AI agents making autonomous calls to complete multi-step tasks. This changes what "good API design" means in practice.

Design for AI agent consumption by serving machine-readable specs at /openapi.json endpoints and generating llms.txt files, reducing token consumption by over 90% compared to HTML documentation.

The llms.txt file is a plain-text description of your API optimised for LLM consumption — no HTML markup, no navigation chrome, just the essential information an AI agent needs to understand how to use your API. Serve it at https://yourapi.com/llms.txt.

What this means practically:

  • Serve your OpenAPI spec at /openapi.json — agents can discover and parse it automatically
  • Keep endpoint descriptions precise — agents read your descriptions literally
  • Make error responses self-explanatory — agents need to understand what went wrong and how to retry without a human in the loop
  • Design idempotency keys for write operations — agents retry on failure, so you need a way for them to safely retry POST requests without creating duplicates
POST /v1/payments
Idempotency-Key: order_789_attempt_1

{
  "amount": 5000,
  "currency": "usd",
  "customerId": "cust_abc123"
}

An idempotency key lets a client safely retry a request — the server returns the same response for duplicate requests with the same key, without processing the payment twice. This is essential for any API that will be called by automated systems, including AI agents.


9. Nail your response structure and be consistent across every endpoint

Nothing breaks a developer's flow like inconsistent response shapes. If /users returns data.users[] but /orders returns items[] directly, every endpoint requires re-reading the docs.

Adopt a consistent response envelope:

// Success response
{
  "data": { ... },          // The actual payload — always under "data"
  "meta": {
    "requestId": "req_abc123",
    "timestamp": "2026-05-05T10:30:00Z"
  }
}

// Collection response
{
  "data": [ ... ],          // Array of resources
  "meta": {
    "total": 100,
    "page": 1,
    "limit": 20,
    "totalPages": 5
  },
  "links": {
    "self":  "/users?page=1",
    "next":  "/users?page=2",
    "last":  "/users?page=5"
  }
}

// Error response
{
  "error": {
    "code": "NOT_FOUND",
    "message": "User 123 does not exist.",
    "docs_url": "https://docs.yourapi.com/errors/not-found"
  },
  "meta": {
    "requestId": "req_abc123",
    "timestamp": "2026-05-05T10:30:00Z"
  }
}

Pick this structure and apply it everywhere. Deviations require developers to handle edge cases, which means bugs.

Field naming conventions — pick one and never deviate:

// camelCase (most common for JSON APIs)
{ "firstName": "Jane", "createdAt": "2026-05-05T10:30:00Z" }

// snake_case (common in Python ecosystems)
{ "first_name": "Jane", "created_at": "2026-05-05T10:30:00Z" }

Date formats — always ISO 8601, always UTC, always the full timestamp:

"createdAt": "2026-05-05T10:30:00Z""createdAt": "05/05/2026""createdAt": 1746439200               ❌ (Unix timestamp — ambiguous seconds vs ms)

10. Build an API that can evolve without breaking things

The best APIs are designed for change. You will need to add fields, deprecate behaviour, restructure resources, and introduce new authentication patterns — the question is whether you can do so without causing pain for every existing integration.

Additive changes are safe; subtractive changes are breaking:

  • Adding a new optional field to a response → safe
  • Removing a field from a response → breaking
  • Adding a new optional request parameter → safe
  • Making an existing optional parameter required → breaking
  • Adding a new endpoint → safe
  • Removing an endpoint → breaking

Design your responses to be forward-compatible. Instruct clients to ignore unknown fields — this is the Postel's Law ("be conservative in what you send, be liberal in what you accept") applied to API design. It lets you add new fields to responses without breaking existing clients.

Use feature flags for gradual rollouts. New API behaviour can be gated behind an opt-in header before being promoted to the default. This lets interested clients test new behaviour early while existing integrations are unaffected:

GET /v1/users/123
X-API-Feature: new-user-profile-schema

Communicate changes early and clearly. Add the Deprecation header to ageing endpoints. Publish deprecation timelines in your changelog. Email affected API key holders before you make breaking changes. The goal isn't to prevent change — it's to ensure that change doesn't catch anyone by surprise.


The API design checklist

Before you ship any new endpoint, run through this:

Question What it catches
Does the URL use a resource noun, not an action verb? Action-oriented URL antipattern
Is the HTTP method correct for the operation? Method misuse
Does every error response include a code, message, and requestId? Unhelpful errors
Is the response structure consistent with every other endpoint? Inconsistent envelopes
Is pagination implemented for any collection endpoint? Unbounded responses
Are rate limit headers included in every response? Clients can't handle limits gracefully
Is this endpoint documented in the OpenAPI spec before it ships? Undocumented API surface
Does any write operation support an idempotency key? Duplicate creation on retry
Are breaking changes handled through versioning, not in-place? Silent breakage for existing clients

Frequently asked questions

What is the difference between REST and GraphQL for API design? REST uses multiple endpoints mapped to resources — /users, /orders/123 — with standard HTTP methods. GraphQL uses a single endpoint where clients specify exactly what data they need in a query. Use REST when you want simplicity, caching, and convention. Use GraphQL when clients need custom data responses or when frontend flexibility is critical. For most backend APIs serving multiple clients, REST is the right default. GraphQL shines for bandwidth-limited mobile apps and when different clients need significantly different data shapes from the same resources.

Should I use URL versioning or header versioning? URL versioning (/v1/users) is more explicit, easier to document, and easier to test. Header versioning (date-based, like Stripe) provides more granular control and forces discipline around breaking changes. For most teams, URL versioning is the right starting point. Move to header-based versioning if you need the granularity Stripe's model provides and you're prepared to invest in the operational discipline it requires.

How do I handle breaking changes without a major version bump? Most changes that feel like they need a version bump can be made non-breaking with careful design: add new fields instead of replacing old ones, make new parameters optional rather than required, deprecate old behaviour with headers and timelines before removing it. Reserve major version bumps for fundamental structural changes that can't be made additive.

What should I include in my API error responses? Every error response should include a machine-readable error code (string, not number), a human-readable message in plain English, a unique request ID, field-level details for validation errors, and a link to documentation for that error type. Include the HTTP status code in the response body as well — it helps when error responses are logged or forwarded through systems that strip headers.

How do I design an API that AI agents can use effectively? Serve your OpenAPI spec at /openapi.json and a plain-text llms.txt file describing your API for LLM consumption. Write precise, unambiguous endpoint descriptions. Make error responses self-explanatory with clear remediation instructions. Implement idempotency keys for all write operations so agents can safely retry on failure. Ensure your authentication scheme is straightforward — complex OAuth flows are hard for agents to navigate autonomously.

What's the most common API design mistake? Inconsistency. Mixed naming conventions, different response structures across endpoints, some endpoints using correct status codes and others returning 200 for errors, pagination working differently on different collection endpoints. Inconsistency forces developers to re-read documentation for every new endpoint instead of applying patterns they already learned. Pick conventions, document them, lint for them, and enforce them in code review.


Iria Fredrick Victor

Iria Fredrick Victor

Iria Fredrick Victor(aka Fredsazy) is a software developer, DevOps engineer, and entrepreneur. He writes about technology and business—drawing from his experience building systems, managing infrastructure, and shipping products. His work is guided by one question: "What actually works?" Instead of recycling news, Fredsazy tests tools, analyzes research, runs experiments, and shares the results—including the failures. His readers get actionable frameworks backed by real engineering experience, not theory.

Share this article:

Related posts

More from Software

View all →