- API Design Principles at a Glance
- What Are API Design Principles?
- Why Poor API Design Gets More Expensive Over Time
- Contract Principles: What Consumers See
- Reliability Principles: How the API Behaves Under Real Traffic
- Security Principles: Protecting Your Most Exposed Surface
- Evolution Principles: Changing Your API Without Breaking Clients
- REST vs GraphQL vs gRPC vs Event-Driven APIs
- How to Design APIs for AI Agents and MCP
- How to Audit Your Existing API
- How EncodeDots Designs APIs That Scale
- Final Thoughts
- Frequently Asked Questions
Trace a recurring integration failure far enough back, and you usually reach an API decision made under deadline pressure: a field named two ways, a list endpoint with no pagination, a version number nobody added. Each one looked harmless when it shipped.
The pattern shows up in almost every production API review. The API serves its first client well. Then a mobile app, a partner, and an internal service all depend on it, and every schema change turns into a meeting with three teams.
This guide covers 15 API design principles that prevent this, for CTOs, tech leads, and engineering managers building or fixing an API layer.
API Design Principles at a Glance
| Principle | What it prevents |
| One naming convention, enforced in CI | Client-side mapping bugs |
| Resource-oriented URLs | Uncacheable RPC-style endpoints |
| Correct HTTP methods | Unsafe retries and side effects |
| Standard status codes | Broken monitoring and retries |
| Predictable data formats | Per-endpoint parsing code |
| A standard error format | Trial-and-error debugging |
| Stateless requests | Sticky sessions and scaling limits |
| Idempotency and concurrency control | Duplicate payments, lost updates |
| Pagination on every list | Timeouts as data grows |
| Caching and async jobs | Slow endpoints blocking clients |
| Per-endpoint observability | Incidents customers find first |
| Object-level authorization | Data leaks (BOLA) |
| Input validation and rate limits | Abuse and overload |
| Versioning and backward compatibility | Breaking every client at once |
| Contract-first design | Docs that drift from reality |
What Are API Design Principles?
API design principles are the rules that shape an API’s contract: naming, URL structure, status codes, errors, security, pagination, and versioning. They determine whether an API stays predictable and safe to change as clients, traffic, and teams grow.
They build on Roy Fielding’s six REST constraints, listed in the FAQ, but cover a lot he never addressed, such as error formats, rate limits, and deprecation.
Why Poor API Design Gets More Expensive Over Time
Design problems rarely cause one big outage. They add small costs that compound. Take response envelopes: if /orders wraps results in data and /customers wraps them in results, every client team maintains endpoint-specific parsing code. Multiply that across 40 endpoints and three client apps, and a simple schema change needs coordinated releases.
A slow query can be fixed behind the API without anyone noticing. A contract problem can’t, because the fix changes what clients already depend on.
Contract Principles: What Consumers See
1. Use One Naming Convention and Enforce It in CI
user_id in one response, userId in another and UserID in a third means every client writes mapping code. Choose snake_case or camelCase (camelCase suits JavaScript clients; snake_case is common in Python and Ruby), use plural nouns for collections (/orders), and keep verbs out of paths.
A Spectral ruleset in CI can enforce this by failing any pull request whose OpenAPI changes break the rules.
2. Model URLs Around Resources, Not Actions
URLs name things, and HTTP methods act on them, so /getUserOrders?id=1024 becomes GET /users/1024/orders.
A familiar failure: a team starts with /getCustomers because it reads well internally. Six months later there are /createCustomer, /updateCustomer, and /deleteCustomer; partners depend on all of them, and fixing the convention needs a new version.
Keep nesting to two levels, and expose /items?order_id=2 when line items need querying on their own. Actions that don’t fit CRUD can become resources, such as POST /orders/{id}/cancellation.
3. Use HTTP Methods for What They Mean
Caches, proxies, and SDK retry logic all make assumptions based on the method.
| Method | Use it to | Idempotent per RFC 9110 |
| GET | Read a resource | Yes |
| POST | Create a resource or trigger an action | No |
| PUT | Replace a resource entirely | Yes |
| PATCH | Update part of a resource | Not required |
| DELETE | Remove a resource | Yes |
That column is what the standard expects, and your handler still has to honour it. A DELETE that sends an email on every call isn’t safe to retry.
Two mistakes are common: GET requests that change state, which crawlers and prefetchers will trigger, and POST for every read, which gives up HTTP caching. POST /orders/search is fine as a documented exception when filters outgrow a query string.
4. Return Standard Status Codes, Never 200 for an Error
If failures return 200 OK with “success”: false, dashboards show 100% success during an outage and retry logic never runs. Use the codes as RFC 9110 defines them: 201 created, 204 no content, 400 malformed request, 422 invalid data, 401 unauthenticated, 403 forbidden, 409 conflict, 429 rate limited, and 5xx for server-side failures.
5. Keep Response Shapes and Data Formats Predictable
Define one response envelope as a shared OpenAPI component and reuse it on every endpoint, so SDKs can share parsing code.
{
“data”: {
“id”: “ord_9F2X”,
“total”: { “amount”: 12999, “currency”: “USD” },
“created_at”: “2026-09-14T10:15:00Z”
},
“meta”: { “request_id”: “req_8a1c” }
} Use ISO 8601 UTC timestamps, integer minor units plus a currency code for money (floats cause rounding errors), and string IDs so the ID scheme can change later.
6. Make Errors Actionable with Problem Details
Rather than designing a custom error format, adopt Problem Details (RFC 9457), which replaced RFC 7807. Each error gets a type, title, status, and a human-readable detail:
{
“type”: “https://api.example.com/errors/invalid-field”,
“title”: “Invalid field”,
“status”: 422,
“detail”: “email is not a valid address.”,
“request_id”: “req_8a1c”
} Log the request_id server-side too, so support can find a failing request by ID instead of asking partners for screenshots.
Reliability Principles: How the API Behaves Under Real Traffic
7. Keep Every Request Stateless
Each request should carry everything the server needs. Session state in one instance’s memory forces sticky sessions, which break during rolling deploys. The bug stays hidden on a single dev instance and appears behind a load balancer.
Keep any sessions in a shared store such as Redis. JWTs avoid the lookup, but signed tokens are hard to revoke, so keep access tokens short-lived and pair them with refresh tokens.
8. Make Retries and Concurrent Writes Safe
A mobile client times out on POST /payments and retries. If the first request went through, the customer pays twice.
An idempotency key handles this. The client sends a unique Idempotency-Key header per operation, and the server returns the stored result for any repeat. Stripe popularised the pattern, and the IETF is standardising it. Keys need an expiry window to limit storage; Stripe keeps them for 24 hours.
Concurrent edits need optimistic concurrency: return an ETag, require If-Match on updates, and respond 412 Precondition Failed if the record changed, so one admin’s save can’t silently overwrite another’s.
9. Paginate Every List Endpoint
An endpoint that returns everything works with 500 test rows and times out with five million, and adding pagination later changes the contract. Choose the method by how the list is used:
- Offset pagination (?page=40&limit=50) suits small, stable lists and admin screens where users jump to a specific page. It slows down on deep pages and can skip or repeat records when data changes between requests.
- Cursor pagination suits feeds, logs, orders, and anything large or fast-changing. It stays fast at any depth, but users can’t jump straight to page 40.
GET /orders?status=shipped&sort=-created_at&limit=50&cursor=eyJpZCI6ODgyMX0
Filters and sorting belong in query parameters.
10. Build In Caching, Lean Payloads and Async Jobs
Slow APIs push consumers into aggressive caching and retry loops, each a new source of bugs.
- Caching: Cache-Control and ETag headers let unchanged data return 304 Not Modified without a body. Skip caching for data that must always be current, such as account balances.
- Lean payloads: compression plus field selection (?fields=id,status) helps most on mobile networks.
- Async jobs: operations slower than a second or two can return 202 Accepted with a job URL, or notify by webhook, signed with HMAC, retried with backoff and carrying an event ID for deduplication.
11. Observe Every Endpoint, Not Just the Server
An uptime check won’t show that /checkout p99 latency rose from 300 ms to 4 seconds after Tuesday’s deploy. Track p50, p95, and p99 latency and error rates per endpoint, and alert on SLO breaches. OpenTelemetry traces follow a request across services, and the request_id from principle 6 links a complaint to its trace.
Security Principles: Protecting Your Most Exposed Surface
12. Authorize Every Object, Not Just Every Request
Broken Object Level Authorization (BOLA) is the first entry in the OWASP API Security Top 10 (2023). The API checks that the token is valid, then returns /invoices/5591 without checking whether that invoice belongs to the caller. Changing the number returns another customer’s data.
Put the ownership check in the data-access layer so new endpoints can’t skip it, and match authentication to the consumer:
| Mechanism | Best for |
| OAuth 2.0 with PKCE | Third-party and mobile user access |
| Short-lived JWTs | Your own clients and services |
| Scoped API keys | Server-to-server partner integrations |
| mTLS | Internal traffic in zero-trust networks |
13. Validate All Input and Rate-Limit Every Client
Validate every request against your schema at the edge, internal calls included. On writes, reject fields clients may not set, or a profile update carrying “role”: “admin” might succeed (mass assignment). On reads, clients should ignore unknown fields so you can add new ones safely.
Rate-limit per API key rather than globally, return 429 with a Retry-After header, and enforce it centrally in a gateway such as Kong or AWS API Gateway.
Evolution Principles: Changing Your API Without Breaking Clients
14. Version Deliberately and Stay Backward Compatible
Partner code, other teams’ services, and old mobile app versions can’t be updated on your schedule.
| Safe (additive) changes | Breaking changes |
| Adding an optional field | Removing or renaming a field |
| Adding a new endpoint | Changing a field’s type |
| Adding an optional parameter | Making an optional field required |
URI versioning (/v1/orders) is easy to route and debug; header versioning keeps URLs stable but is harder to test in a browser. When retiring a version, publish a timeline and send Deprecation (RFC 9745) and Sunset (RFC 8594) headers. Twelve months of overlap is a common baseline for public APIs, longer when mobile apps depend on it.
15. Make the Contract the Source of Truth
In API-first development, the OpenAPI 3.1 spec is reviewed with its consumers before implementation. A naming problem caught there costs an afternoon; caught after partners integrate, it costs a new version.
The spec then generates docs, mocks, and SDKs, and contract tests in CI fail the build when code and spec drift. AsyncAPI does the same for event-driven APIs. The trade-off is more design time before the first endpoint ships.
REST vs GraphQL vs gRPC vs Event-Driven APIs
| REST | GraphQL | gRPC | Event-driven | |
| Best for | Public and partner APIs | UIs built from nested data | Internal service calls | Work that needs no instant reply |
| Strength | HTTP caching, universal support | Clients fetch exactly what they need | Very low latency (Protobuf, HTTP/2) | Decoupled, high throughput |
| Less suited to | Screens needing many round trips | Simple CRUD, heavy HTTP caching | Browser-facing public APIs | Requests that need an answer now |
At EncodeDots we usually start public and partner APIs on REST, since every client and tool supports it, add GraphQL when frontends assemble screens from many resources, and use gRPC between internal services we control. Team skills and existing infrastructure can shift that choice. Platforms that mix styles stay coherent by sharing naming, error and auth conventions.
How to Design APIs for AI Agents and MCP
More API calls now come from AI agents picking tools at runtime, often via the Model Context Protocol (MCP), an open standard Anthropic introduced in November 2024. Agents read contracts literally and can’t ask a colleague what an endpoint means, so they need:
- Descriptions that explain intent (“Lists a customer’s orders, newest first”), not “Gets orders”
- Error details specific enough to self-correct
- Idempotency, because agents retry often
- Narrow token scopes, with human approval for destructive or financial actions
- Page limits and quotas, because agents can loop
Should You Add an MCP Server to Your Existing API?
Usually yes, but mapping every endpoint to a tool leaves the agent with dozens of low-level choices. A few task-level tools, such as “find overdue invoices for a customer,” built on a clean REST API work better. An inconsistent API confuses agents just as it confuses developers, so fix that first.
How to Audit Your Existing API
Pre-Production API Design Checklist
- Naming enforced by a CI linter
- Every list paginated, with a maximum page size
- Real status codes for every error
- One error format, with request IDs
- Per-object authorization checks
- Input validation on every endpoint
- Per-key rate limits
- Idempotency keys on critical POSTs
- A published versioning and deprecation policy
- Docs and contract tests generated from the spec
- Per-endpoint latency and error alerts
- Load tests at 5–10x current peak
Fix, Refactor or Rebuild?
Fix in place when the problems are gaps, such as missing pagination or rate limits, that can be added without breaking anything. Release a new version when naming or response shapes are inconsistent across many endpoints. Rebuild when the foundation itself is flawed, for example, an authorization model that can’t express per-object ownership. Even then, the strangler pattern lets you migrate endpoint by endpoint behind a gateway rather than in one cutover.
How EncodeDots Designs APIs That Scale
Our API engagements start by mapping every consumer, including partners and AI agents. The OpenAPI contract is reviewed with them before implementation, and security, caching, and load targets are set in the architecture. Docs, SDKs, contract tests, and OpenTelemetry dashboards ship with the API.
A common objection is that the API works, so a review can wait. Most of the issues above only surface with the next partner, mobile release, or traffic spike, when they cost more to fix.
Final Thoughts: Start with Five Checks
If you do one thing this week, check five areas: naming consistency, pagination limits, object-level authorization, error handling, and versioning. Each gap gets harder to close as more clients depend on the current behaviour. If you’d like help prioritising what you find, talk to an API expert at EncodeDots for a review, or explore our API development services.


