Blog

By

API Design Principles in 2026: Clear Steps for Better Software APIs

September 7, 2026

Key takeaways:

  • An API's design is a contract you cannot quietly change later, so the expensive decisions are the ones made before any code exists.
  • Most of what people call API design principles reduces to four questions: what are the resources, what can you do to them, what happens when something fails, and how does a caller find out.
  • The standards already answer more than most teams realize. RFC 9110 defines which methods are idempotent, and RFC 9457 defines a standard error format almost nobody uses.
  • Version last, not first. Additive changes cost nothing; a /v2 costs you every consumer, forever.

A well-designed API is boring to use. You read three pages of documentation, guess what the next endpoint is called, and guess right. A badly designed one is memorable in the worst way: you learn that 200 OK sometimes means the request failed, that the "update" endpoint silently erases fields you did not send, and that page 500 of a collection takes eleven seconds to return.

The difference is rarely technical sophistication. It is whether someone made a small number of decisions deliberately, and wrote them down, before the first endpoint shipped.

This guide covers the principles of web API design that survive contact with real consumers: resource naming, HTTP semantics, error shape, idempotency, pagination, versioning, rate limiting, and documentation. Each section gives you the specific convention plus the trade-off you are accepting by choosing it. Where practitioners genuinely disagree, and on two or three points they do, this says so rather than pretending the question is settled.

What API Design Principles Actually Cover

API design is the decision layer above implementation. It sets what the resources are, what a caller can do to them, what comes back, and what happens when something goes wrong. The code underneath can be rewritten in a weekend. The design cannot, because other people's software now depends on it.

The scope is worth being precise about, because "API design" gets used for two different jobs. One is the developer experience question: is this pleasant and predictable to call? The other is the contract question: what have you promised, and what breaks if you change it? The second is the one that costs money.

The stakes rise sharply once the caller is somebody else's engineering team. An internal endpoint can be renamed on a Tuesday; a public one cannot, which is why the two-sided marketplace is the hardest version of this problem, with buyers and sellers integrating against the same contract from opposite directions.

Volume is part of why this matters more than it used to. Cloudflare's Application Security Report covers April 2023 through March 2024 and titles one of its sections "60% of dynamic (non cacheable) traffic is API-related," two percentage points up on the previous edition.

Their definition is specific and worth keeping attached to the number: any HTTP request whose response content type is XML or JSON. That covers Cloudflare's own network rather than the internet as a whole, which is a narrower claim than the round "APIs are most of the internet" figure that circulates without a source.

The same report, under a section headed "A quarter of APIs are shadow APIs," found organizations running a median of 33% more public-facing API endpoints than they knew about. Endpoints accumulate faster than anyone documents them, which is the practical case for deciding the conventions early.

Design the Contract Before the Code

Contract-first means writing the specification, reviewing it, and only then implementing against it. Zalando's engineering guidelines state the rule plainly: define the API using a standard specification language, and get peer review on that definition before implementation starts.

This inverts the common order, where an endpoint appears because a screen needed data, and the "API" is whatever twelve of those endpoints happen to look like once the feature is done. That path produces an interface shaped like your database, or worse, shaped like last quarter's UI.

The two orders produce the same endpoints and a very different interface.

Two API design orders compared: written specification first, versus endpoints accreted screen by screen

The practical version is short. Write the resource list. Write one example request and response per operation, including the failure cases. Show it to somebody who will have to call it and is not on your team. Then build.

Two things make this cheap enough to actually do. A specification format, almost always the OpenAPI Specification, gives you a machine-readable contract that generates documentation, client libraries and mock servers from one file. And a mock server built from that spec lets the consuming team start integrating while the real implementation is still empty, which surfaces naming problems while they are still free to fix.

When we build a product API at VAULT, this is the stage that changes the most. It is faster to argue about a resource name in a document than to deprecate it eighteen months later, a discipline we apply across custom software development work generally.

REST API Design Principles That Still Hold

Most web APIs are REST-shaped, and the RESTful API design principles worth keeping are the ones that make an interface guessable. Three conventions do the bulk of that work.

Name Resources, Not Actions

A URL identifies a thing. The HTTP method says what you are doing to it. Putting the verb in the path duplicates the method and immediately creates a second, competing vocabulary.

Bad:   GET  /api/create-products
Good:  POST /api/products

Use plural nouns for collections, and express ownership by nesting one level:

Bad:   GET /product-reviews
Good:  GET /products/{product_id}/reviews

Stop nesting at collection, item, collection. /customers/1/orders/99/items is defensible; two more levels past that produces URLs nobody can hold in their head, and it hard-codes a hierarchy you may want to change.

Use the HTTP Methods as Specified

The methods have defined meanings, and half of API confusion comes from ignoring them. Idempotency is the property that matters most: making the same call twice has the same effect on the server as making it once.

MethodWhat it meansIdempotent?Typical success codes
GETRead a resourceYes200, 404 if absent
POSTCreate, or a non-standard actionNo201 with a Location header, 200
PUTReplace the resource entirelyYes200, 201 on create, 204
PATCHUpdate part of the resourceNot guaranteed200, 204, 409 on conflict
DELETERemove the resourceYes204, 200, 202 if queued

RFC 9110, the current HTTP semantics standard, is explicit in section 9.2.2: PUT, DELETE and the safe methods are idempotent, and a proxy must not automatically retry anything else. That same section carries a caveat worth knowing, since it trips people up. Idempotency is a promise about the intended effect, not a guarantee that nothing else happens. Writing an audit-log row on every PUT is perfectly compliant.

One detail worth knowing because it explains a lot of inconsistency: PATCH is not in RFC 9110 at all. It arrived separately in RFC 5789 in 2010, which is why its semantics are vaguer than the others.

That also explains why two competing body formats exist for it. JSON Merge Patch is simpler but cannot distinguish "field omitted" from "set this to null." JSON Patch sends an explicit list of add, remove and replace operations, which is more precise and more verbose.

Counted out, the cost of treating PUT as a partial update is easy to see.

REST API design principles: a PUT carrying three of twelve fields erases the other nine

The failure this prevents is specific and common: a client sends a PUT with three of a resource's twelve fields, and the other nine are erased, because PUT means "this is now the complete state."

Return Status Codes That Tell the Truth

The status code is the first thing every client library, proxy and monitoring tool reads. When it lies, everything downstream breaks in ways that are hard to trace.

RFC 9110 draws the line clearly: 4xx means the client appears to have erred, 5xx means the server knows it erred or cannot perform the method. A validation failure is 400, not 500. A missing record is 404, not an empty 200. Rate limiting is 429, never 503.

Asked on Reddit for the worst APIs they had worked with, the most-upvoted answer in an r/ExperiencedDevs thread described one that returned 200 for everything with a body structure that changed depending on the error. It is anecdotal, but it topped a thread of hundreds of replies, which tells you how much time that pattern costs people.

API ARCHITECTURE

The contract is the expensive part

VAULT has been designing and integrating APIs on production systems since 2012, and the contract decisions come before any endpoint.

Contact Us

Make Errors Machine-Readable

Most guides say "use standard status codes" for errors and stop there. That leaves the actual body undefined, so every API invents its own shape and every client writes a bespoke parser.

There is a standard, and it is the single most underused thing in this whole area. RFC 9457 defines the application/problem+json media type with five named members: type, a URI identifying the class of problem; status, the HTTP code; title, a short summary that does not change per occurrence; detail, an explanation of this specific occurrence; and instance, a URI for this occurrence. You can add your own fields alongside them.

Here is the RFC's own example, which shows why the extension fields matter:

HTTP/1.1 403 Forbidden
Content-Type: application/problem+json

{
  "type": "https://api.example.com/probs/out-of-credit",
  "title": "You do not have enough credit.",
  "status": 403,
  "detail": "Your current balance is 30, but that costs 50.",
  "instance": "/account/12345/msgs/abc",
  "balance": 30,
  "accounts": ["/account/12345", "/account/67890"]
}

A client can branch on type without string-matching your prose, show detail to a human, and read balance programmatically. Nothing here is new or hard. It is simply a decision somebody has to make once, and adopting a published standard costs less than designing your own and documenting it.

Handle Repeat Requests Without Double Charging

Networks fail after the server has already done the work. The client sees a timeout, retries, and the customer is charged twice. Since POST is not idempotent, the protocol cannot solve this for you, so the convention is an idempotency key.

Stripe's idempotency documentation is the one worth copying, because it is documented in enough detail to reproduce. The client generates a unique key, Stripe recommends a V4 UUID, and sends it in an Idempotency-Key header, up to 255 characters. Stripe stores the status code and body of the first request under that key and returns the identical result for every retry.

Two calls arrive with the same key, and only one of them reaches the charge.

Idempotency key: two identical API calls resolve to one stored response and a single charge

The part people get wrong is what happens on failure. Stripe replays a stored 500 too, not just successes. That is deliberate: a retry must not accidentally "succeed" against a request that genuinely failed, because the client would then believe two different things about one operation. Keys are pruned after at least 24 hours, and reusing one after that starts a fresh request.

Apply it to every POST that has a side effect a customer can see. GET and DELETE do not need it, since they are already idempotent by definition.

Mobile clients make this urgent rather than theoretical. A phone moving between cell towers drops connections a desktop browser never would, so the same checkout endpoint sees far more retries from an app than from the web, one of several reasons the web and mobile builds diverge more than teams expect.

Paginate Before the Collection Grows

Every collection endpoint needs pagination from day one, because retrofitting it is a breaking change. The choice between the two approaches is a real trade-off, not a style preference.

ApproachRequestQuery behaviorWhere it breaks
Offset?offset=100000&limit=10Database counts through 100,000 rows before returning 10Deep pages get slow; rows shift if the underlying data changes mid-scroll
Cursor?cursor=32&limit=10Indexed lookup, WHERE id > 32 ORDER BY id LIMIT 10Cannot jump to an arbitrary page number

Offset is fine for a settings list of forty rows. It degrades badly on anything that grows, since the cost of a page rises with how deep it is. Cursor pagination returns page one and page ten thousand at the same speed, at the price of losing "jump to page 40," which most API consumers never needed.

Whichever you pick, cap the page size on the server. A client asking for limit=100000 should get 25 back, not a query that takes your database down. That cap belongs in the documentation, not just the code, so callers can plan around it.

Version Only When You Have To

Versioning is where the good API design principles genuinely disagree with each other, so treat anybody who tells you there is one right answer with suspicion.

The uncontroversial part first: additive changes do not need a version. Adding a field, adding an optional parameter, adding an endpoint. Clients that ignore the new field keep working. Reserve versioning for changes that break existing callers, and you will need it far less often than you expect.

When you do need it, four mechanisms are in real use.

MechanismHow it looksWhat you gainWhat it costs
URI pathGET /v2/customers/3Trivial to route, cache-friendly, obvious in logsThe same resource now has two URIs
Query stringGET /customers/3?version=2Same URI for the resourceEasy to omit by accident; clutters caching keys
Custom headerX-API-Version: 2URLs stay cleanInvisible in a browser, harder to debug, more server logic
Media typeAccept: application/vnd.contoso.v2+jsonMost correct by REST's own rulesCache fragmentation, and unfamiliar to most callers

That disagreement is live rather than theoretical. On an r/webdev thread asking why versions sit in the path, the top reply argued the practical case, that you can put a proxy on the domain and route /v2 to a different host than /v1.

A reply further down made the opposing case: a person is always a person, so /person should always be true, and a structural change belongs in the content type rather than the URL. Both are right about their own priority. Pick the one whose cost you would rather pay, then apply it everywhere.

Two response headers are worth adopting alongside whichever you choose, because they let the API announce its own retirement schedule instead of relying on an email nobody read: Deprecation flags an endpoint as deprecated, and Sunset gives the date it stops working.

The difference between the two kinds of change is who has to do the work.

API versioning: an additive change costs nobody, a breaking change bills every consumer

Retiring a version is a project in itself, not a flag flip, since every consumer has to move before you can delete anything. Budgeting for that work is part of treating post-launch software work as real engineering rather than cleanup.

SYSTEMS INTEGRATION

Before you ship a v2

VAULT will read the API you are about to publish and tell you which decisions you will not be able to reverse.

Book A Call

Rate Limiting and Authentication Conventions

Rate limiting is not only abuse protection. It is the thing that stops one enthusiastic integrator from taking down the service for everyone else, usually by accident rather than malice.

Return 429 Too Many Requests, never 503, and include a Retry-After header so a well-behaved client knows exactly how long to wait. Publish the current state on every response through the near-universal trio of X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, so callers can back off before they hit the wall rather than after.

Where you set the limit is partly an architecture question, since a burst that autoscales cheaply on one platform is an outage on another, and the serverless against Kubernetes decision changes what a spike actually costs you.

Four algorithms are in common use, and the choice affects how bursty traffic feels to a caller. Fixed window is simplest and allows a double-rate burst across a window boundary. Sliding window fixes that at higher bookkeeping cost. Token bucket permits short bursts while holding the average, which is why it is the usual default for public APIs. Leaky bucket smooths output to a constant rate.

On authentication, use something that already exists. OAuth 2.0 for user-delegated access, long-lived API keys for server-to-server, short-lived JWTs where you need stateless verification.

Engineer Sean Goedecke makes a fair argument in his API design writeup that many API consumers are not full-time engineers and will struggle through an OAuth handshake they did not need, so an API key is often the kinder default for a straightforward integration. What is never defensible is inventing your own scheme.

Document the API as You Design It

Documentation written after the fact describes what got built. Documentation written during design catches the problems while they are still cheap, because writing "returns the user, unless the account is suspended, in which case it returns a partial user" out loud makes the flaw obvious.

The OpenAPI specification you wrote in the contract stage is most of the work already. What it does not capture, and what every reader needs, is a worked example per endpoint including the error cases, a description of the auth flow, and the rate limits.

Good documentation also survives staff turnover, which is when undocumented conventions get expensive. Anyone who has handled a project changing hands knows the API nobody wrote down is the part that takes the longest to relearn.

One rule that pays for itself: keep the reference documentation public and ungated. In the same r/ExperiencedDevs thread on bad APIs, a highly-upvoted reply offered it as a reliable predictor, that if you cannot read the docs without registering, it is very likely going to be awful to work with. Whether or not the correlation holds, the perception is real among the people deciding whether to integrate with you.

When REST Is the Wrong Choice

REST is the right default for a public web API. It is not the right answer everywhere, and the honest version of API design principles and best practices includes knowing when to pick something else.

gRPC suits internal service-to-service calls where you control both ends and want the performance of HTTP/2 with binary Protocol Buffers. GraphQL suits clients that need to compose data from many resources in one round trip, which is exactly the mobile problem Meta built it for in 2012.

Each of the three answers a different question about who is calling you.

Choosing REST, gRPC or GraphQL by who calls the API rather than by fashion

The cost of GraphQL is real and less discussed. Caching gets harder once clients can craft arbitrary queries, the backend gets fiddlier, and it is genuinely harder for a non-specialist integrator to pick up than a list of URLs.

We hit that trade-off directly on the Leaf Trade wholesale marketplace, a two-sided platform running Django, PostgreSQL, Redis and a forked Saleor commerce framework behind a React frontend. GraphQL came out of the stack and the REST API stayed.

For a marketplace whose consumers are storefront clients and integrators working against known resources, the flexibility was not buying enough to justify the complexity it added. That is not an argument against GraphQL in general. It is an argument for picking the protocol against your actual consumers rather than the current fashion.

A Review Checklist for Your Next API

Run this against a specification before implementation starts, or against an existing API you have inherited. Each item corresponds to a failure that is expensive once callers depend on it.

  1. Every path names a resource, and no path contains a verb.
  2. Every method matches its RFC 9110 meaning, and PUT genuinely replaces rather than partially updates.
  3. No endpoint returns 200 for a failure, and no validation error returns 500.
  4. Errors use one documented body shape across every endpoint, ideally application/problem+json.
  5. Every side-effecting POST accepts an idempotency key.
  6. Every collection endpoint paginates and caps its page size server-side.
  7. Rate limits return 429 with Retry-After, and remaining quota is visible on every response.
  8. Breaking changes have a versioning mechanism, and additive changes do not use it.
  9. Documentation is generated from the specification and reachable without a login.

Work down it in order, since the early items are the ones that cannot be fixed later without breaking somebody. If you cannot answer an item, that is the design decision you have not made yet.

Getting the Decisions in the Right Order

The pattern running through the principles of web API design is that the cheap moment and the expensive moment sit far apart. Naming a resource costs nothing on a whiteboard and costs every integrator a migration once it ships. Choosing an error format takes an afternoon before launch and takes a coordinated release afterward.

So the sequence matters more than any individual convention. Decide the resources, the methods, the error shape and the pagination style before you write the endpoints. Adopt the published standards where they exist, since RFC 9110 and RFC 9457 have already resolved arguments you would otherwise have in a code review. Leave versioning until something genuinely breaks. An API built that way ends up boring, and boring is the whole goal.

FAQs

What are essential api design principles for a first public API?

Five, in order: name resources with nouns and let the HTTP method carry the verb, match each method to its defined semantics so PUT replaces and PATCH updates, return status codes that accurately reflect what happened, use one documented error format everywhere, and paginate every collection from the start. Those five prevent the failures that cannot be repaired without breaking your callers. Everything else can be added later without a migration.

How is REST different from the other options?

REST models your system as resources at URLs, manipulated with standard HTTP methods, which makes it guessable and cacheable using infrastructure that already exists. gRPC trades that legibility for binary efficiency over HTTP/2 and fits internal service-to-service traffic.

GraphQL lets the client specify exactly what it wants in one request, which suits data-hungry mobile clients but complicates caching. The main practical advantage of the REST API design principles is that any developer can explore your API with a browser and curl.

Should the version go in the URL or in a header?

Both are defensible and practitioners genuinely disagree. URL versioning is easier to route, cache and debug, and it is what most public APIs default to. Header or media-type versioning keeps one URI per resource, which is more correct by REST's own rules, at the cost of being invisible in a browser and harder to cache. Pick one, apply it consistently, and spend the saved effort on making breaking changes rare enough that it rarely matters.

What is the right way to return errors?

Use the correct HTTP status code, then a consistent JSON body. RFC 9457's application/problem+json gives you a ready-made structure with type, title, status, detail and instance, plus room for your own fields. The specific format matters less than using the same one on every endpoint, since a client that must write a different parser per endpoint will write one bad parser for all of them.

How do I stop retries from creating duplicate records?

Accept a client-generated idempotency key on every POST with a visible side effect, store the first response against that key, and return the stored response for any repeat. Stripe's implementation is the reference: a UUID in an Idempotency-Key header, results retained for at least 24 hours, and stored failures replayed as failures. Without it, a timeout on a network the client cannot see becomes a duplicate charge.

Talk Through Your API Design

If you are specifying an API that other teams or customers will build against, the decisions above are worth an hour with someone who has had to live with the consequences of getting them wrong. We have shipped production APIs for marketplaces, healthcare platforms and connected hardware, and we are happy to review a specification before it becomes a contract.

Contact us and we will take a look at what you have.

We're here to help
Do you have questions about our services or need help building a product?
Contact Us