REST API Design: A Complete Guide for 2026
A backend team ships a new resource endpoint. It uses POST for a lookup, wraps every error in a 200 response with an "ok": false field, and versions the API by adding a v2 suffix wherever it felt right that sprint. None of it is illegal; HTTP doesn't stop you. But six months later, a partner integration breaks because its client retried a non-idempotent call, and the on-call engineer spends an hour working out why a "successful" response actually meant failure.
That gap between what HTTP allows and what makes an API predictable is where REST API design really happens. REST gives you six architectural constraints, and HTTP gives you a set of methods and status codes. What you do within those limits decides whether the next engineer who touches your API, human or automated, can guess how it behaves without reading the source code.
This guide covers the principles, the practical conventions, and the trade-offs, with enough detail to apply them to your next API and enough context to recognize when REST isn't the right tool at all.
What Is REST API Design?
REST API design is the practice of applying REST's architectural constraints (resource-oriented URIs, standard HTTP methods, and stateless requests) to a web API's endpoints, so that its behavior stays consistent and predictable for every client. Representational State Transfer (REST) is itself an architectural style, first described in Roy Fielding's 2000 doctoral dissertation.
That distinction matters more than it seems. REST doesn't specify a wire format, a URL scheme, or a status code for "email not found." It specifies a set of constraints, and REST API design is the discipline of applying them consistently. A REST API for a payments platform and one for a healthcare records system will have very different resource models, but both should behave the same way when a client sends the same GET twice in a row, or when a request is malformed.
The 6 Guiding Principles of REST Architecture
Fielding didn't pick REST's six constraints in isolation. He derived REST by starting from a client-server style and adding constraints one at a time, weighing what each one gained and what it cost. That matters for API designers, because it means REST is a specific combination of constraints, not a menu you choose from item by item.
An API that skips the uniform interface, or statelessness, isn't "REST with one feature missing." It's a pragmatic HTTP API that borrows some REST ideas, which is a perfectly legitimate design choice as long as it isn't described as fully RESTful. Of the six constraints, only code on demand is genuinely optional within the style. The other five are what make an architecture REST rather than something close to it.
1. Uniform Interface
The uniform interface is what separates REST from a generic RPC-over-HTTP API. It means every resource is identified and manipulated in the same way, whatever it represents:
- a consistent way to identify resources (URIs);
- a consistent way to manipulate them through representations (usually JSON);
- self-descriptive messages, where a response tells you enough about itself to be processed, through headers such as
Content-Type; - and, in Fielding's original formulation, hypermedia links that tell the client what it can do next.
Most production REST APIs, including those from the major cloud providers, implement the first three and skip the fourth. Fully hypermedia-driven APIs (HATEOAS) exist, but they add development overhead that most teams decide isn't worth it for internal or partner-facing APIs. The practical takeaway: aim for consistency in how resources are named and manipulated across your entire API, even if you leave out hypermedia links.
2. Statelessness
Each request must carry everything the server needs to understand and process it: authentication credentials, the resource identifier, and any context specific to that call. What statelessness rules out is server-side session state, meaning the server holding on to a client's context between requests. It doesn't rule out state in general. Resource data still lives in a database, and responses can still be cached; that's the state of the resource and the cache, not of the client.
If your API requires a client to call /session/start before anything else, and the server keeps that session in memory, you've broken statelessness. You've also made horizontal scaling harder, because the load balancer now has to send follow-up requests back to the same server instance.
The fix is straightforward: put the session context in the request itself. A bearer token or signed JWT is one common way to do that, but it's an implementation choice, not something REST requires. A client certificate or a signed request header carries the same information just as well, as long as any server instance can validate it independently.
3. Client-Server Separation
The client and server can evolve independently as long as the interface between them stays stable. A mobile app team and a backend team should be able to ship on different schedules without coordinating every release, because the contract, the API itself, is the only thing that has to stay compatible.
That's why a documented, versioned API surface matters more than internal code quality. The backend can be rewritten from Java to Go without the mobile team noticing, provided the URIs, methods, and response shapes don't change underneath them.
4. Cacheability
Every response should state explicitly whether it can be cached and, if so, for how long. HTTP provides the tools: the Cache-Control, ETag, and Last-Modified headers, defined in RFC 9111 (HTTP Caching). A GET /products/482 response that won't change within a given time window should say so, so that clients, CDNs, and reverse proxies can skip the round trip.
Not every response is safe to cache in the same way. A response is only eligible for shared caching, the kind a CDN or gateway can reuse across different users, if it doesn't contain personalized or sensitive data. A response that varies by authenticated caller needs Cache-Control: private or no-store instead. And if the representation changes based on a request header, such as Accept-Language or an authorization scope, the Vary header needs to name that header, so the cache stores each variant separately instead of serving one user's response to another.
Cacheability is the constraint most REST APIs underuse, and under-caching is the safer of the two possible mistakes. Adding a Cache-Control header to a genuinely public, non-personalized read endpoint that rarely changes costs almost nothing, and the reduction in latency and load is immediate. Getting it wrong on a private endpoint costs you a data exposure, not just a missed performance gain.
5. Layered System
A client shouldn't be able to tell, and shouldn't need to care, whether it's talking directly to the origin server or through a chain of intermediaries such as a load balancer, an API gateway, or a caching proxy. The layered system constraint lets those intermediaries sit between client and server without changing the interface either side relies on.
This is the constraint that makes API gateways possible at all. A gateway can enforce rate limits, terminate TLS, or transform a request, and as long as it preserves the uniform interface, neither the client nor the origin service needs to know it's there.
6. Code on Demand (Optional)
This is the only optional constraint. A server can extend a client's functionality by sending it executable code, most often JavaScript, for the client to run. A web page loading client-side validation logic from the server is a simple example. Most REST APIs, especially machine-to-machine ones, skip this constraint entirely, and they're still fully valid REST without it.
Designing Resource URIs and Naming Conventions
A URI names a resource, not an action. That single rule settles most of the naming arguments teams have.
- Use nouns, not verbs. The HTTP method already expresses the action, so
/getUsersrepeats information the method carries and reads like an RPC call bolted onto HTTP. - Use plural nouns for collections.
/ordersreturns a collection, and/orders/482returns one member of it. Mixing singular and plural across an API forces every consumer to memorize the exceptions. - Nest resources to express relationships, but stop at two levels.
/customers/482/ordersis clear: the orders belonging to customer 482./customers/482/orders/9931/line-items/4/discountsis technically valid and practically unreadable. Beyond two levels, promote the deeply nested resource to its own top-level endpoint with a filter, such as/line-items/4/discountsor/discounts?line_item_id=4. - Use lowercase, hyphen-separated paths.
/shipping-addressesis easier to scan, and less error-prone across case-sensitive systems, than/shippingAddressesor/shipping_addresses.
| Good | Bad | Why it matters |
|---|---|---|
GET /orders | GET /getAllOrders | The method already says "get"; the URI should name the resource, not repeat the verb. |
GET /orders/482 | GET /order/482 | Mixing singular and plural for the same resource forces consumers to guess. |
POST /customers/482/orders | POST /createOrderForCustomer?id=482 | Nesting expresses the relationship directly instead of hiding it in a query string. |
GET /orders?status=shipped | GET /orders/shipped | Filtering belongs in the query string; the path should stay stable whatever the filter. |
GET /shipping-addresses | GET /shippingAddresses | Lowercase, hyphenated paths avoid case-sensitivity bugs across clients and proxies. |
Choosing HTTP Methods and Status Codes
REST API design uses HTTP's methods rather than inventing new verbs, and each method has defined semantics for safety and idempotency, which RFC 9110 spells out precisely. Safe methods have no side effects the client should expect the server to perform. Idempotent methods can be retried any number of times with the same net effect as calling them once.
| Method | CRUD mapping | Idempotent? | Safe? |
|---|---|---|---|
GET | Read | Yes | Yes |
POST | Create | No | No |
PUT | Full update / replace | Yes | No |
PATCH | Partial update | Not guaranteed | No |
DELETE | Delete | Yes | No |
PUT and DELETE are idempotent because RFC 9110 defines them that way: sending the same DELETE /orders/482 request five times leaves the system in the same state as sending it once. POST is neither safe nor idempotent by default, which is exactly why retrying a POST after a timeout is risky. Without extra protection, a client that resends a payment POST after a dropped connection can create the charge twice.
PATCH sits in a grey area. RFC 9110 defines the general framework of safe and idempotent semantics, but PATCH itself is defined separately, in RFC 5789, which says plainly that PATCH is neither safe nor idempotent by default. That's because a patch body can describe a relative change, like incrementing a counter, where applying it twice gives a different result from applying it once. If your PATCH payloads only ever set absolute values, you can design them to be idempotent in practice, but that's a decision you make, not a guarantee the method gives you.
Status codes should match these semantics as precisely as the methods do. A few conventions are worth being deliberate about:
200 OKfor a successfulGETorPUTwith a response body, and201 Createdfor a successfulPOSTthat creates a resource, with aLocationheader pointing to it.204 No Contentfor a successfulDELETE, or an update with nothing meaningful to return.400 Bad Requestfor malformed input the server can't parse or validate, as distinct from422 Unprocessable Contentfor input that parses correctly but fails business validation.401 Unauthorizedwhen authentication is missing or invalid, and403 Forbiddenwhen the server refuses the request, typically because the caller lacks permission.409 Conflictfor state conflicts, such as trying to create a resource that already exists under a unique constraint.
Returning 200 OK with an error message in the body, instead of a real 4xx or 5xx status, is one of the most common REST design mistakes. It breaks every generic HTTP client, monitoring tool, and cache that relies on the status code to know what happened, and it forces every consumer to write custom logic just to detect a failure your infrastructure was already built to report for free.
Authentication and Security Basics
Authentication answers "who is calling?" Authorization answers "what are they allowed to do?" Because of REST's statelessness constraint, both have to travel with every request, since the server keeps nothing in memory between calls.
Four approaches cover most REST APIs:
- API keys are the simplest: a static token issued to each client and sent as a header. They're easy to implement but harder to rotate safely, and they carry no information about scope beyond whatever the server maps the key to.
- OAuth 2.0 / OAuth 2.1 delegates authorization through short-lived access tokens, usually obtained through a client credentials or authorization code flow. It's the standard for APIs that need scoped, revocable access rather than a single all-or-nothing key.
- JWTs are commonly used as the token format inside an OAuth flow. The token itself carries claims (who it's for, what scope it has, when it expires) that a server can validate without a database round trip, at the cost of being harder to revoke before expiry than a server-side session.
- mTLS authenticates at the transport layer using client certificates. It's common in service-to-service and regulated environments, where the calling system's identity matters more than a human user's.
Whichever mechanism you choose, plan for credential rotation from day one. A key or certificate that can't be rotated without downtime tends to stay in place long after it should have been retired.
Beyond choosing a mechanism, treat the OWASP API Security Top 10 as a checklist rather than background reading. Its top two risks are distinct failures with distinct fixes.
Broken Object Level Authorization (BOLA), ranked first, happens after authentication succeeds. The API correctly confirms who is calling but never checks whether that caller is allowed to touch the specific object ID in the request. A valid token proves identity; it doesn't prove the token holder owns order 482. Every endpoint that accepts a resource ID needs its own explicit authorization check against the calling identity, in addition to, not instead of, verifying the token.
Broken Authentication, ranked second, is a failure to establish or protect identity in the first place: weak or unsigned tokens, no rate limiting on login or password-reset endpoints, or credential stuffing that succeeds because there's no lockout.
Versioning Your API
APIs change. Fields get renamed, response shapes evolve, and endpoints are deprecated. Versioning exists so those changes don't break every client the moment you deploy.
This is a different problem from semantic versioning of a software package, where MAJOR.MINOR.PATCH numbers communicate compatibility at build time. A live HTTP endpoint has no build step, so most APIs settle on a single version marker, using one of the approaches below, instead of a full SemVer scheme.
| Approach | Example | Trade-off |
|---|---|---|
| URI versioning | /v1/orders, /v2/orders | The most visible and cacheable option, but the version is baked into every link and bookmark. |
| Header versioning | Api-Version: 2 | Keeps URIs stable across versions, but clients can easily forget to set it, and it's harder to test by pasting a URL into a browser. |
| Query parameter versioning | /orders?version=2 | Simple to add, but easy to leave out by accident, and less conventional than the other approaches. |
| Media type (content negotiation) versioning | Accept: application/vnd.company.v2+json | Uses content negotiation instead of a new resource identifier, so each resource keeps one canonical URI, but it's the least common approach in practice and the hardest for clients to learn. |
URI versioning wins on convention alone: it's what most public APIs use, so it's what most client developers expect. The trade-off is that /v1 and /v2 are technically different resources, which sits a little awkwardly with REST's uniform interface constraint. Header and media-type versioning avoid that tension, but ask more of every client integration.
Whichever approach you choose, the harder problem isn't the mechanism but the policy: how long you support the previous version, how you communicate deprecation, and whether breaking changes need a new major version or can ship as additive fields on the existing one.
Real APIs answer that policy question in different ways. GitHub versions its REST API by release date and asks clients to send the version in an X-GitHub-Api-Version header, falling back to an older version when the header is missing. Stripe does something similar with a Stripe-Version header, and lets an account roll back for 72 hours after an upgrade.
Two HTTP headers exist for exactly this kind of signal: Deprecation, which marks a version as no longer preferred, and Sunset, which announces the date it stops responding altogether. Using both, rather than relying on a changelog entry alone, lets a client's own tooling detect an approaching breaking change automatically.
Pagination, Filtering, and Partial Responses
A GET /orders endpoint that returns every order in the system works fine in a demo and falls over the first time a real customer has 200,000 orders. Pagination, filtering, and partial responses exist to keep both the response size and the query cost bounded.
Offset-based pagination (?offset=100&limit=25) is the simplest to implement and the easiest for clients to understand, but it degrades on large, frequently changing datasets. If a row is inserted while a client is paging through results, the offsets shift, and the client can see duplicates or skip rows entirely.
Cursor-based pagination (?after=eyJpZCI6NDgyfQ) uses an opaque token pointing to the last record seen instead of a numeric position. That avoids the shifting-offset problem and scales better on large datasets, at the cost of losing the ability to jump straight to "page 7."
Keyset pagination is a variant of cursor pagination that uses an actual indexed column, such as a timestamp or ID, as the cursor value instead of an opaque token. It trades a little implementation simplicity for a query the database can serve efficiently with a simple index range scan.
Filtering and partial responses (?fields=id,status,total) reduce payload size for clients that only need some of a resource's fields. That matters more for mobile clients and high-throughput integrations than for internal dashboards.
Error Handling and Idempotency
A status code on its own rarely tells a client, or the engineer debugging it, enough to fix the problem. RFC 9457 standardizes a JSON error body for exactly this gap, with a small set of fields:
type: a URI identifying the error category.title: a short, stable summary.status: the HTTP status code, repeated in the body.detail: specifics about this occurrence.instance: a reference to the specific request.
It also leaves room for custom extension fields. RFC 9457 formalizes what most mature APIs were already doing informally, and it obsoletes the older RFC 7807, which defined the same shape.
{
"type": "https://example.com/problems/insufficient-inventory",
"title": "Insufficient inventory",
"status": 409,
"detail": "Only 3 units of SKU-4821 are available, but 5 were requested.",
"instance": "/orders/9931"
}
A consistent error shape across every endpoint lets a client write one error-handling path instead of one per endpoint. The alternative, where one team's endpoints return {"error": "..."} and another's return {"message": "...", "code": "..."}, pushes that inconsistency onto every consumer.
Idempotency matters most on POST endpoints that create something with real-world consequences: a charge, an order, a shipment. Because POST isn't idempotent by default, a client retry after a network timeout can create a duplicate. The common fix is an Idempotency-Key header. The client generates a unique key for each logical operation, the server stores the result of the first request under that key, and any retry with the same key returns the original result instead of running again.
This isn't part of the HTTP or REST specifications. It's a convention that several major payment and API platforms arrived at independently, because RFC 9110 only guarantees retry safety for idempotent methods and advises clients not to retry non-idempotent requests automatically unless they know it's safe.
REST vs. GraphQL vs. gRPC: When to Use Alternatives
REST is the default for good reasons. It maps naturally onto HTTP, intermediaries can cache it without any extra client-side logic, and most engineers already know its conventions well enough to get up to speed on a new REST API in an afternoon. That default deserves respect, rather than being bypassed out of habit.
GraphQL fits when clients need to combine data from several resources in one round trip, especially bandwidth-constrained clients like mobile apps, and when different consumers of the same backend need meaningfully different shapes of the same data. The cost is that GraphQL's flexible queries make HTTP-level caching much harder, because a cache can no longer key on the URL alone. That trade-off is worth stating explicitly, since teams sometimes adopt GraphQL for its query flexibility and only discover the caching cost once traffic grows.
gRPC fits internal, service-to-service communication where you control both client and server, latency matters more than human readability, and you want strongly typed contracts enforced at compile time through Protocol Buffers. It's a poor fit for public-facing APIs, because it's harder to call from a browser and offers none of REST's discoverability through a URL a developer can paste into a browser tab.
The practical rule: default to REST for public and partner-facing APIs, where broad client compatibility and cacheability matter. Consider GraphQL when your clients have genuinely different data needs, and use gRPC inside your own service mesh, where you control both ends of every call.
Documenting and Governing Your APIs at Scale
Every principle in this guide, from consistent naming to correct method semantics and standardized errors, is easy to state and hard to keep true across fifty APIs built by ten teams over three years. Documentation drifts from the implementation. One team's PATCH is idempotent and another's isn't, with no note explaining why. A new engineer copies whichever pattern is closest to hand, not necessarily the right one.
OpenAPI (and AsyncAPI for event-driven and asynchronous interfaces) exists to make the contract the source of truth, rather than the code or a wiki page. Writing the specification first, then generating documentation, client SDKs, and mock servers from it, keeps the description of the API accurate to the spec.
It doesn't prove that the running implementation still matches the spec, though. A handler can drift from its own OpenAPI definition without anyone noticing until a client hits the mismatch. Closing that gap takes a separate step: validating requests and responses against the spec, and running the documented examples as part of CI, rather than generating the docs once and moving on.
Treating the spec as an artifact under version control, reviewed the same way code is, catches naming and status-code inconsistencies before they ship rather than after a partner integration fails. Contract testing in CI catches what review alone tends to miss, such as a field that quietly changed type or an example payload that no longer parses.
This is also where automated enforcement earns its place over relying on code review alone for the conventions themselves. WSO2 API Platform lets teams design APIs from an OpenAPI or AsyncAPI specification and apply governance policies automatically as part of that process. It checks naming conventions, versioning patterns, and required security schemes against Spectral rulesets whenever an API proxy is updated, and flags violations on a compliance dashboard instead of leaving drift to be discovered later in a retrospective.
The platform's control plane can also federate third-party gateways, such as Kong, AWS, and Azure, alongside WSO2's own, as described in WSO2's gateway federation documentation, so those conventions can extend to APIs already running on gateways an organization adopted separately. Federation covers discovery, cataloging, and governance compliance monitoring, plus publishing and deployment for supported gateways. It isn't a claim that every runtime behavior is identical across every third-party gateway, so teams should confirm which policies a given federated gateway enforces natively and which are enforced at the WSO2 control-plane layer. None of this replaces good API design judgment. It moves some of that judgment out of each reviewer's memory and into a checked, versioned ruleset.
Conclusion
A well-designed REST API is resource-oriented, stateless, and consistent. Those three properties are what let it scale as traffic grows, and be extended safely later without breaking the clients that already depend on it.
Before you ship your next endpoint, check it against a short list:
- Does the URI name a resource rather than an action?
- Does the HTTP method match its documented safety and idempotency guarantees?
- Does every error response use a real status code and a consistent error body?
- Is the versioning strategy documented, not just implied by the URL?
- Could a new engineer, or an AI agent calling the API programmatically, work out what an endpoint does from its method and path alone?
That last question is worth dwelling on. The same properties that make a REST API predictable to a human developer, namely consistent resource naming, clear semantics, and self-descriptive responses, also make it easier to expose safely to an AI agent later, whether through direct API calls or an agent-facing protocol layered on top. Designing for consistency now isn't only about the next engineer who reads your API. It also saves you from redesigning the whole surface the day an automated client needs to use it.
If you're standardizing REST conventions across more than a handful of APIs, relying on code review alone gets harder every time a new team or gateway joins the mix. WSO2 API Platform checks APIs automatically against governance rulesets like the ones in this guide, including APIs discovered on the third-party gateways your organization already runs, so the standard is applied consistently instead of depending on every reviewer remembering it.
Frequently Asked Questions
What is a RESTful API design?
A RESTful API design applies REST's architectural constraints (statelessness, a uniform interface, resource-oriented URIs, and standard HTTP methods) to a web API, so that its behavior is consistent and predictable across every endpoint rather than documented case by case.
What are the 5 basic principles of REST API?
The five required constraints are client-server separation, statelessness, cacheability, a uniform interface, and a layered system. A sixth, code on demand, is optional and rarely used in machine-to-machine APIs.
Is REST API still used?
Yes. REST is still the default architectural style for public and partner-facing web APIs, and it remains the primary interface for cloud providers and most API-first companies. Alternatives like GraphQL and gRPC have grown for specific use cases, flexible client-driven queries and internal service communication respectively, without displacing REST as the general-purpose default.