REST API Design
REST Principles and Constraints
Read aloud is unavailable in this browser.
The Architectural Style That Powers the Web
In the year 2000, Roy Thomas Fielding published his doctoral dissertation, "Architectural Styles and the Design of Network-based Software Architectures." Chapter 5 of that dissertation introduced Representational State Transfer (REST)—an architectural style that would fundamentally reshape how the world builds distributed systems.
Today, REST has become so ubiquitous that many developers use the term without fully understanding its meaning. APIs are casually labeled "RESTful" when they merely use HTTP and JSON, missing the profound architectural principles that make REST powerful. This misunderstanding leads to APIs that are fragile, tightly coupled, and difficult to evolve.
To design APIs that stand the test of time—that scale, evolve gracefully, and delight developers who consume them—we must return to the source. We must understand REST not as a technology or a protocol, but as an architectural style defined by specific constraints that, when properly applied, produce systems with remarkable properties.
By the end of this page, you will understand REST as Roy Fielding envisioned it—not as a vague synonym for HTTP APIs, but as a rigorous architectural style. You'll learn the six constraints that define REST, why each constraint exists, and how they combine to produce systems that are scalable, evolvable, and resilient.
What REST Actually Is
Before diving into constraints, we must establish what REST fundamentally is—and what it is not.
REST is an architectural style, not a protocol or framework.
Roy Fielding derived REST by analyzing what made the early Web successful. He identified the architectural properties that enabled the Web to scale from a few academic servers to billions of connected devices. REST is a description of those properties, expressed as constraints on how components interact.
This distinction matters enormously. A protocol specifies exact message formats and behaviors. An architectural style provides guidelines for constructing systems with desired properties. REST doesn't mandate HTTP, JSON, or any specific technology. It describes a set of constraints that, when applied, produce systems with properties like:
- Performance — efficient network usage through caching and layered processing
- Scalability — stateless servers that can be replicated without coordination
- Simplicity — a uniform interface that reduces cognitive load
- Modifiability — loose coupling that allows components to evolve independently
- Visibility — intermediaries can observe and optimize interactions
- Portability — components can be moved across deployment environments
- Reliability — failures in one component don't cascade to others
Most "RESTful APIs" in the wild would be more accurately called "HTTP-based APIs" or "JSON over HTTP." They use HTTP verbs and status codes but ignore crucial REST constraints like hypermedia (HATEOAS). This isn't necessarily wrong—sometimes full REST is unnecessary—but developers should understand what they're trading away.
The Richardson Maturity Model
Leonard Richardson proposed a model for classifying APIs by their adherence to REST principles. This model helps us understand the spectrum from "not REST at all" to "fully RESTful":
| Level | Name | Description | Example |
|---|---|---|---|
| 0 | The Swamp of POX | Single URI, single HTTP method, tunneled all operations | SOAP/XML-RPC over POST |
| 1 | Resources | Multiple URIs representing different resources | /users, /orders separate endpoints |
| 2 | HTTP Verbs | Proper use of HTTP methods (GET, POST, PUT, DELETE) | GET /users vs POST /users |
| 3 | Hypermedia Controls | Responses include links to related actions/resources | HATEOAS fully implemented |
Level 3 is "true" REST as Fielding envisioned it. Most production APIs today operate at Level 2, which provides significant benefits but misses REST's most powerful feature: hypermedia-driven discovery.
The Six REST Constraints
Fielding defined REST through six architectural constraints. Each constraint is deliberately chosen to induce specific architectural properties. Understanding the why behind each constraint is essential for making intelligent tradeoffs when designing APIs.
Let's examine each constraint in depth:
- •1. Client-Server — Separation of concerns between user interface and data storage
- •2. Stateless — Each request contains all information needed to process it
- •3. Cacheable — Responses explicitly indicate their cacheability
- •4. Uniform Interface — A standardized interaction model between components
- •5. Layered System — Components only see the immediate layer they interact with
- •6. Code-On-Demand (Optional) — Servers can extend client functionality with executable code
Let's examine each constraint in detail, understanding both the principle and its practical implications.
Constraint 1: Client-Server Separation
The client-server constraint establishes the foundational separation of concerns in REST architectures:
The Principle: Clients are concerned with user interface and user state. Servers are concerned with data storage, domain logic, and business rules. These concerns are separated by a uniform interface, allowing each to evolve independently.
Why This Matters:
This separation seems obvious today, but it wasn't always the norm. Early distributed systems often blurred these responsibilities. The client-server constraint enables:
- Independent Evolution — Mobile apps, web apps, and CLI tools can all consume the same API
- Portability — User interfaces can be deployed across platforms without server changes
- Scalability — Servers can be replicated and load-balanced transparently
- Organizational Scaling — Different teams can own clients and servers with clear contracts
- •User interface rendering
- •User input handling
- •Local state management
- •Request construction
- •Response interpretation
- •Error presentation
- •Data persistence
- •Business logic execution
- •Authorization enforcement
- •Resource state management
- •Response generation
- •Integration coordination
The client-server constraint implies that your API should expose resources and operations, not UI-specific data structures. If your API returns pre-formatted HTML snippets or screen-specific response shapes, you've violated this constraint. The server shouldn't know whether the client is a mobile app, a web page, or a command-line tool.
Constraint 2: Statelessness
The stateless constraint is perhaps the most impactful—and most frequently violated—of all REST constraints.
The Principle: Each request from client to server must contain all the information necessary to understand and process that request. The server cannot rely on any stored context from previous requests. Session state, if needed, must be kept entirely on the client.
Why This Matters:
Statelessness induces several critical architectural properties:
| Property | How Statelessness Enables It |
|---|---|
| Scalability | Any server can handle any request—no session affinity required |
| Reliability | Server failures don't cause session loss; clients can retry elsewhere |
| Visibility | Intermediaries can understand requests without session context |
| Simplicity | Servers need less memory; no session synchronization complexity |
12345678910111213141516171819202122232425262728293031
// ❌ STATEFUL ANTI-PATTERN: Server remembers client context// Session stored on serverapp.post('/cart/add', (req, res) => { const session = sessions[req.sessionId]; // Server-stored state session.cart.push(req.body.item); // Modifies session on server res.json({ success: true });}); // This creates problems:// - Can't load-balance without sticky sessions// - Server memory grows with active users// - Server restart loses all sessions // ✅ STATELESS PATTERN: All context in requestapp.post('/cart/add', authenticateJWT, (req, res) => { // User identity comes from JWT token (in request) const userId = req.user.id; // Cart state is persisted in database, not server memory const cart = await cartRepository.findByUser(userId); cart.addItem(req.body.item); await cartRepository.save(cart); res.json({ cart: cart.toJSON() });}); // Benefits:// - Any server can handle the request// - Horizontal scaling is trivial// - No session synchronization requiredStatelessness isn't free. Every request must carry authentication tokens, potentially increasing bandwidth. Clients must manage state they would otherwise offload to servers. This is a deliberate tradeoff: REST accepts increased per-request overhead to gain scalability and reliability. For most web-scale systems, this tradeoff is overwhelmingly worthwhile.
Common Statelessness Violations:
- Server-side sessions — Storing cart contents, wizard progress, or view preferences on the server
- Implicit pagination cursors — When
GET /items?page=2means different things based on prior/items?page=1calls - Multi-step transactions — "Add to cart, then checkout" where checkout assumes server remembers cart
- Sticky load balancing — Required only because servers hold client state
Acceptable State:
Statelessness refers to application state, not resource state. The database obviously holds state. The constraint is that servers shouldn't hold client session state between requests. Each request must be self-sufficient.
Constraint 3: Cacheability
The cache constraint transforms REST from merely functional to performant at global scale.
The Principle: Responses must explicitly or implicitly define themselves as cacheable or non-cacheable. When a response is cacheable, clients and intermediaries may reuse that response for equivalent future requests.
Why This Matters:
Caching is the single most effective technique for improving distributed system performance. The cache constraint:
- Reduces Latency — Cached responses arrive in milliseconds rather than round-trips
- Decreases Server Load — Identical requests don't hit the origin
- Improves Reliability — Stale cache can serve during origin outages
- Saves Bandwidth — No data transfer for cached responses
12345678910111213141516171819202122232425262728293031323334
# CACHEABLE RESPONSE - Product catalog (changes rarely)HTTP/1.1 200 OKContent-Type: application/jsonCache-Control: public, max-age=3600, stale-while-revalidate=86400ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"Last-Modified: Tue, 07 Jan 2025 12:00:00 GMTVary: Accept-Encoding # Cache-Control directives explained:# - public: CDNs and proxies can cache this# - max-age=3600: Fresh for 1 hour# - stale-while-revalidate=86400: Can serve stale for 1 day while revalidating # NON-CACHEABLE RESPONSE - User's bank balanceHTTP/1.1 200 OKContent-Type: application/jsonCache-Control: private, no-store, no-cache, must-revalidatePragma: no-cache # - private: Only client can cache (not CDNs)# - no-store: Don't persist to disk# - no-cache: Always revalidate before use# - must-revalidate: Never serve stale # CONDITIONAL REQUEST - Bandwidth-efficient revalidationGET /api/products/12345 HTTP/1.1If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"If-Modified-Since: Tue, 07 Jan 2025 12:00:00 GMT # Server response if unchanged (saves bandwidth):HTTP/1.1 304 Not ModifiedETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"| Directive | Meaning | Use Case |
|---|---|---|
| public | Any cache may store | Static assets, public content |
| private | Only client may cache | User-specific data |
| max-age=N | Fresh for N seconds | Content with known freshness |
| s-maxage=N | CDN-specific max-age | Different CDN vs browser timing |
| no-cache | Must revalidate before use | Data that changes frequently |
| no-store | Never persist to disk | Sensitive/financial data |
| must-revalidate | Never serve stale | Critical data accuracy |
| stale-while-revalidate=N | Serve stale while refreshing | Perceived performance |
| immutable | Never revalidate | Versioned static assets |
ETags are content-based (hash of response body), while Last-Modified is time-based. ETags are more precise—they detect changes even if the modification time is wrong. Use both when possible; HTTP allows clients to send both If-None-Match and If-Modified-Since, with ETag taking precedence.
Constraint 4: Uniform Interface
The uniform interface is REST's central distinguishing feature—the characteristic that most differentiates it from other distributed architecture styles.
The Principle: All components in a REST architecture interact through the same standardized interface, regardless of what resources they access or what operations they perform. This uniformity simplifies the architecture and improves visibility.
Fielding defined four sub-constraints that together constitute the uniform interface:
4.1 Identification of Resources
Resources are named by URIs. A resource is any concept that can be named—a document, an image, a collection, a computed result, a business entity. The URI identifies the resource, not the representation.
/users/12345 — A specific user
/users — Collection of users
/users/12345/orders — Orders belonging to a user
/search?q=widgets — A computed search result
4.2 Manipulation Through Representations
Clients interact with resources through representations (JSON, XML, HTML, etc.). When a client wants to modify a resource, it sends a representation of the desired state. The server then validates and applies that change.
4.3 Self-Descriptive Messages
Each message contains enough information to describe how to process it. Media types, HTTP methods, and headers all contribute to self-description. An intermediary should be able to understand what a request does without application-specific knowledge.
4.4 Hypermedia as the Engine of Application State (HATEOAS)
Clients discover available actions through hypermedia links in responses. Instead of hardcoding API URLs, clients follow links to transition between application states. This is REST's most under-implemented—and most powerful—constraint.
1234567891011121314151617181920212223242526272829303132
{ "orderId": "ord-123", "status": "pending_payment", "total": { "amount": 99.99, "currency": "USD" }, "items": [ {"product": "Widget", "quantity": 2, "price": 49.995} ], "_links": { "self": { "href": "/orders/ord-123" }, "pay": { "href": "/orders/ord-123/payments", "method": "POST", "title": "Submit payment for this order" }, "cancel": { "href": "/orders/ord-123", "method": "DELETE", "title": "Cancel this order" }, "customer": { "href": "/customers/cust-456" }, "items": { "href": "/orders/ord-123/items" } }}HATEOAS decouples clients from server URL structures. If the server changes /orders/{id}/payment to /payments/orders/{id}, HATEOAS clients continue working—they follow links rather than constructing URLs. This enables servers to evolve independently of clients, which is crucial for long-lived public APIs.
Constraint 5: Layered System
The layered system constraint organizes the architecture hierarchically, where each layer only knows about its immediate neighbors.
The Principle: A client cannot tell whether it's connected directly to the origin server or to an intermediary. Each component only sees the layer it interacts with directly. Intermediaries can be added transparently for load balancing, caching, security, and other cross-cutting concerns.
Why This Matters:
Layering enables architectural evolution without client changes:
┌─────────────────────────────────────────────────────────────────┐
│ Client │
│ (Only knows it talks to "the API") │
└─────────────────── │ ────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ CDN Edge (Cloudflare/Fastly) │
│ - Static asset caching │
│ - DDoS protection │
│ - Geographic latency reduction │
└─────────────────── │ ────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ API Gateway (Kong/Ambassador) │
│ - Rate limiting │
│ - Authentication │
│ - Request routing │
│ - Protocol translation │
└─────────────────── │ ────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Load Balancer │
│ - Health checking │
│ - Traffic distribution │
│ - Failover │
└─────────────────── │ ────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Application Servers (Origin) │
│ - Business logic │
│ - Data access │
└─────────────────────────────────────────────────────────────────┘
- •Security in Depth — Multiple layers can enforce independent security checks
- •Encapsulation — Legacy systems can be wrapped without exposing their complexity
- •Scalability — Each layer can scale independently based on its specific demands
- •Flexibility — Intermediaries can be added/removed without client changes
- •Simplified Components — Each layer handles a focused set of responsibilities
Layering adds latency—each hop takes time. This is typically offset by caching, but deep layer stacks can introduce measurable overhead. Modern architectures often collapse layers for performance-critical paths while maintaining the layered model for non-critical traffic.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.