An API is a contract, and the cost of breaking it lands on everyone who built against you. Good API design in 2026 is less about picking a fashionable protocol and more about being predictable: consistent naming, stable errors, honest versioning, and pagination that does not fall apart at scale. This guide covers the conventions that hold up across REST, GraphQL, and gRPC, and the small decisions that quietly decide whether developers trust your service.
What changed in 2026
- REST kept winning by default. Despite years of GraphQL hype, most public APIs shipped in the last year are still JSON-over-HTTP REST, because the tooling, caching, and debugging story is unbeatable.
- OpenAPI is the lingua franca. OpenAPI 3.1 specs now drive client SDK generation, mock servers, and contract tests in most teams, so an undocumented API feels increasingly unusual.
- Idempotency keys went mainstream. Following the pattern popularized by Stripe, more write endpoints now accept an
Idempotency-Key header so retries do not double-charge or double-create.
- AI clients raised the bar for clarity. With LLM agents calling APIs directly, machine-readable errors and crisp descriptions matter more - a vague 400 is now a tool-use failure.
REST, GraphQL, or gRPC
There is no universal winner. Pick by client shape and latency needs.
| Style |
Best for |
Trade-off |
| REST + JSON |
Public APIs, CRUD, broad client reach |
Over- and under-fetching; many round trips |
| GraphQL |
Many clients with varied data needs (apps, dashboards) |
Caching, rate limiting, and auth are harder |
| gRPC |
Internal service-to-service, low latency, streaming |
Browser support needs a proxy; less human-debuggable |
For most teams, REST for the public edge and gRPC between internal services is a sane split. Reach for GraphQL when you genuinely have many consumers fetching different shapes of the same graph - not because it is new.
Conventions that keep clients happy
- Name resources as plural nouns.
GET /invoices, GET /invoices/{id} - not verbs in the path. Let HTTP methods carry the action.
- Use status codes honestly. 200 for success, 201 for creation, 400 for client error, 401 vs 403 distinctly, 404 for missing, 409 for conflict, 422 for validation, 429 for rate limits, 5xx only when the server is at fault.
- Version in the path.
/v1/invoices is blunt but obvious. Header-based versioning is cleaner in theory and a debugging headache in practice.
- Paginate with cursors. Return an opaque
next_cursor rather than ?page=42&offset=.... Offset pagination skips or repeats rows when data changes mid-scan and gets slow on large tables.
- Make writes idempotent. Accept an idempotency key on POST so a retried request is safe. Networks fail; clients retry; you should not double-charge.
- Return structured errors. A stable code, a human message, and optional field-level details:
{
"error": {
"code": "insufficient_funds",
"message": "The account balance is too low for this charge.",
"field": "amount"
}
}
The code is the contract; the message can change wording, the code must not.
Documenting and securing the contract
Write an OpenAPI spec and treat it as the source of truth - generate SDKs and run contract tests against it so the docs cannot drift from reality. For auth, prefer OAuth 2.1 or signed API keys over rolling your own; put rate limits behind 429 with a Retry-After header so clients can back off politely. If you expose an internal API to AI agents, lean on the same machine-readable conventions - it is the same discipline that makes a service easy to consume from a different backend framework.
What to skip
- Skip premature GraphQL. A single client and a dozen endpoints do not need a query language. The operational overhead outweighs the flexibility.
- Skip offset pagination on large tables. It silently corrupts results as rows are inserted or deleted during paging.
- Skip leaking internal errors. Never surface stack traces or SQL strings in responses; map them to a generic 500 with a correlation ID.
- Skip breaking changes without a version bump. Renaming a field or tightening validation is a breaking change even if it feels minor.
FAQ
Should I put the version in the URL or a header?
The URL. It is visible in logs, easy to test in a browser, and unambiguous for clients. Header versioning is cleaner conceptually but harder to debug and route.
REST or GraphQL for a new public API?
REST, unless you genuinely have many clients fetching different shapes of the same data. REST has better caching, simpler rate limiting, and a far larger pool of developers who already understand it.
How do I handle backward-incompatible changes?
Ship them under a new major version and keep the old one running with a deprecation window and clear sunset dates. Never silently change behavior on an existing version.
What status code for a validation error?
422 Unprocessable Entity for semantic validation failures, 400 Bad Request for malformed syntax. Be consistent so clients can branch on the code.
Where to go next
Compare backend frameworks for 2026, pick a startup database, and weigh microservices against a monolith.