A good API gets out of the developer’s way. They read the docs once, write some code, and it works. A bad API makes them read the docs three times, post on Stack Overflow, and eventually write a wrapper just to hide your design decisions from their own codebase. Most bad APIs are bad in the same specific ways.

Inconsistent Naming Kills Predictability

Pick a convention and apply it everywhere. If you use camelCase in one endpoint and snake_case in another, every developer who touches your API has to check every field name instead of predicting it.

Bad:

{
  "userId": 123,
  "user_name": "chirag",
  "UserEmail": "[email protected]"
}

Good:

{
  "user_id": 123,
  "user_name": "chirag",
  "user_email": "[email protected]"
}

The same applies to endpoint paths. If you have /getUsers, /create-post, and /delete_comment, you have three conventions in one API. That is three places where a developer will guess wrong.

HTTP Status Codes Are Not Optional

Using 200 for everything and putting the real status in the response body is not creative. It is wrong. It breaks every HTTP client, proxy, cache, and monitoring tool that exists.

Mistake Correct
200 with {"error": "not found"} 404
200 with {"error": "unauthorized"} 401
200 with {"success": false} 4xx or 5xx as appropriate
500 for validation errors 422

The HTTP status code is the primary signal. Use it. Then add detail in the body.

Error Messages Need to Be Actionable

This is an actual error response from a production API I had to integrate with:

{
  "error": "Invalid request",
  "code": 1042
}

What is invalid? Which field? What is the valid format? What does code 1042 mean? The developer now opens a support ticket, which costs the API team time and the developer time.

This is what an error response should look like:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address",
        "received": "chirag@"
      }
    ],
    "request_id": "req_7f3a9b2c"
  }
}

The request_id is especially important. It lets developers provide a reference when asking for help, and lets you trace the request in your logs.

Pagination That Actually Works

Offset-based pagination breaks at scale and causes missed or duplicate records when data changes between pages.

Bad:

GET /posts?page=3&per_page=20

When a new post is inserted at page 1 while someone is paginating, every subsequent page shifts. Items get skipped.

Better: Cursor-based pagination.

GET /posts?limit=20&cursor=eyJpZCI6MTIzfQ==

The cursor encodes the position in the dataset, usually the last ID or timestamp. New insertions do not affect traversal. The tradeoff is you cannot jump to page 47 directly - which most users never need to do.

Versioning From Day One

Breaking changes happen. The question is whether you manage them gracefully or break your users. Options:

  • URL versioning: /v1/posts, /v2/posts - clear and cacheable
  • Header versioning: API-Version: 2026-04-01 - cleaner URLs, harder to see
  • Query param: /posts?version=2 - messy

URL versioning is the most common and most tooling-friendly. Use it from the first release. Retrofitting versioning after you have users is painful.

The Response Envelope Problem

Some APIs wrap every response in an envelope:

{
  "success": true,
  "data": {
    "user": { ... }
  },
  "meta": { }
}

Others return the resource directly:

{
  "id": 1,
  "name": "Chirag"
}

Pick one pattern and never mix them. Mixing means every consumer has to inspect the response shape before accessing data.

Idempotency Keys for Mutations

Network requests fail. Clients retry. Without idempotency keys, a payment gets charged twice. With them, the second request returns the result of the first.

POST /payments
Idempotency-Key: client-generated-uuid-here

The server stores the key with the result for a window (24-48 hours). If the same key arrives again, return the stored result without executing the operation again. Stripe and Braintree both implement this. It is not hard to build and it eliminates an entire class of serious bugs.

Documentation That Includes Errors

Docs that only show the happy path are incomplete. Every endpoint should document the error responses it can return. Developers need to handle 400, 401, 403, 404, 422, and 429 differently. If your docs do not tell them when each can occur, they find out at 2am in production.

Bottom Line

Good API design is mostly about respecting the developer’s time. Whether you are building a REST API or considering gRPC, use HTTP status codes correctly, make error messages actionable, paginate with cursors, version from day one, and stay consistent on naming. None of this is hard. It is just discipline applied early - which is much cheaper than fixing a bad API after you have users depending on it.