REST API Design: Principles & Best Practices

intermediate 15 min read Updated 2026-02-11

After this topic, you will be able to:

  • Explain REST’s six architectural constraints and their implications for API design
  • Evaluate API designs against the Richardson Maturity Model
  • Assess when REST’s constraints (statelessness, uniform interface) align with or conflict with system requirements

TL;DR

REST is an architectural style for building APIs that treats everything as a resource with a unique identifier, manipulated through standard HTTP methods. It emphasizes statelessness, cacheability, and a uniform interface, making it the dominant pattern for public web APIs. REST’s constraints enable scalability and simplicity but can feel verbose compared to RPC or GraphQL for complex operations.

Cheat Sheet:

  • Six Constraints: Client-server, stateless, cacheable, layered system, uniform interface, code-on-demand (optional)
  • Resource-Oriented: Every entity is a resource with a URI (e.g., /users/123)
  • Standard Methods: GET (read), POST (create), PUT/PATCH (update), DELETE (remove)
  • Maturity Levels: Level 0 (POX), Level 1 (Resources), Level 2 (HTTP Verbs), Level 3 (HATEOAS)
  • Best For: Public APIs, CRUD operations, systems needing horizontal scaling

Background

REST was introduced by Roy Fielding in his 2000 PhD dissertation as a way to describe the architectural principles that made the World Wide Web successful. Fielding, one of the principal authors of the HTTP specification, observed that the web’s scalability and resilience came from specific design choices: stateless interactions, resource-based addressing, and standard methods for manipulation. He formalized these observations into REST, not as a protocol but as an architectural style.

Before REST became mainstream, APIs were dominated by SOAP (Simple Object Access Protocol) and XML-RPC, which treated remote calls like local function invocations. These approaches required complex tooling, tight coupling between client and server, and often struggled with caching and scalability. REST offered a simpler alternative: model your domain as resources, expose them through URIs, and manipulate them using standard HTTP methods. This alignment with HTTP’s existing semantics meant REST APIs could leverage web infrastructure—proxies, CDNs, browsers—without modification.

The term “RESTful API” became ubiquitous in the 2010s as companies like Twitter, Stripe, and GitHub adopted REST for their public APIs. REST’s simplicity and HTTP alignment made it the default choice for web services, though true adherence to all REST constraints (especially HATEOAS) remains rare in practice. Today, REST faces competition from GraphQL and gRPC, but it remains the foundation for most public-facing APIs due to its simplicity, tooling maturity, and universal HTTP support.

Architecture

REST architecture is built around six core constraints that define how clients and servers interact. These constraints aren’t implementation details—they’re design principles that shape the entire system.

Client-Server Separation: The client handles user interface concerns while the server manages data storage and business logic. This separation allows each to evolve independently. A mobile app, web frontend, and third-party integration can all consume the same REST API without the server knowing or caring about their implementation details.

Statelessness: Each request from client to server must contain all information needed to understand and process it. The server stores no client context between requests. If you need to fetch a user’s orders, you send the user ID with every request—the server doesn’t remember who you are from the last call. This constraint is what enables horizontal scaling: any server instance can handle any request without needing to coordinate session state.

Cacheability: Responses must explicitly mark themselves as cacheable or non-cacheable. When a response is cacheable, clients can reuse it for subsequent identical requests, reducing load and latency. HTTP’s built-in caching headers (Cache-Control, ETag, Last-Modified) make this transparent. A GET request for /products/123 might be cached for 5 minutes, eliminating server hits for repeated views.

Layered System: The client cannot tell whether it’s connected directly to the end server or to an intermediary. This allows you to insert load balancers, caching proxies, API gateways, and CDNs without changing client code. Stripe’s API, for example, sits behind Cloudflare’s CDN—clients don’t know or care.

Uniform Interface: This is REST’s defining constraint and has four sub-constraints. First, resources are identified by URIs (/users/123, not getUser(123)). Second, resources are manipulated through representations—you send JSON or XML describing the desired state, not method calls. Third, messages are self-descriptive through HTTP headers and status codes. Fourth, HATEOAS (Hypermedia as the Engine of Application State) means responses include links to related resources, allowing clients to discover actions dynamically.

Code-on-Demand (Optional): Servers can extend client functionality by transferring executable code (like JavaScript). This is the only optional constraint and rarely used in modern REST APIs.

These constraints work together to create APIs that are scalable, evolvable, and cacheable. The trade-off is verbosity and sometimes awkward mappings for operations that don’t fit the resource model.

REST Architectural Constraints

graph TB
    subgraph REST Architecture
        CS["Client-Server<br/><i>Separation of Concerns</i>"]
        SL["Stateless<br/><i>No Session Context</i>"]
        CA["Cacheable<br/><i>Explicit Cache Control</i>"]
        LS["Layered System<br/><i>Transparent Intermediaries</i>"]
        UI["Uniform Interface<br/><i>Standard Methods & URIs</i>"]
        COD["Code-on-Demand<br/><i>Optional</i>"]
    end
    
    CS --> |"Enables"| IND["Independent Evolution"]
    SL --> |"Enables"| SCALE["Horizontal Scaling"]
    CA --> |"Reduces"| LOAD["Server Load & Latency"]
    LS --> |"Allows"| PROXY["CDN/Load Balancers"]
    UI --> |"Provides"| INTER["Interoperability"]
    
    subgraph Uniform Interface Details
        R["Resource Identification<br/><i>URIs</i>"]
        M["Resource Manipulation<br/><i>Representations</i>"]
        SD["Self-Descriptive Messages<br/><i>Headers & Status Codes</i>"]
        H["HATEOAS<br/><i>Hypermedia Links</i>"]
    end
    
    UI -.-> R
    UI -.-> M
    UI -.-> SD
    UI -.-> H

The six REST constraints work together to enable scalability and simplicity. Statelessness and cacheability are the most impactful for performance, while the uniform interface (especially HATEOAS) is the most complex to implement correctly.

E-commerce REST API Design with Complex Operations

graph TB
    subgraph Core Resources
        Products["/products<br/>/products/{id}"]
        Users["/users<br/>/users/{id}"]
        Orders["/orders<br/>/orders/{id}"]
        Cart["/users/{id}/cart"]
    end
    
    subgraph Nested Resources
        UserOrders["/users/{id}/orders<br/><i>User's order history</i>"]
        OrderItems["/orders/{id}/items<br/><i>Items in an order</i>"]
        ProductReviews["/products/{id}/reviews<br/><i>Product reviews</i>"]
    end
    
    subgraph Non-CRUD Operations
        Checkout["POST /orders/{id}/checkout<br/><i>Transition order to payment</i>"]
        ApplyDiscount["POST /orders/{id}/discounts<br/>Body: {code: 'SAVE20'}<br/><i>Apply discount code</i>"]
        CancelOrder["DELETE /orders/{id}<br/><i>Cancel order (idempotent)</i>"]
        SearchProducts["GET /products?q=laptop&category=electronics<br/><i>Search with filters</i>"]
    end
    
    subgraph Versioning Strategy
        V1["/v1/products<br/><i>Legacy version</i>"]
        V2["/v2/products<br/><i>Current version</i>"]
        Header["Accept: application/vnd.api.v2+json<br/><i>Alternative: Header versioning</i>"]
    end
    
    subgraph Pagination & Filtering
        Page["GET /products?page=2&limit=20<br/>Response: {items: [...], next: '/products?page=3'}"]
        Filter["GET /orders?status=shipped&from=2024-01-01"]
    end
    
    Users --> UserOrders
    Orders --> OrderItems
    Products --> ProductReviews
    
    Orders -."State transition".-> Checkout
    Orders -."Resource creation".-> ApplyDiscount
    Orders -."Idempotent delete".-> CancelOrder
    Products -."Query operation".-> SearchProducts
    
    Products --> V1
    Products --> V2
    V2 -.-> Header
    
    Products --> Page
    Orders --> Filter

**

Richardson Maturity Model

Leonard Richardson’s REST Maturity Model provides a practical framework for evaluating how “RESTful” an API actually is. Most APIs claiming to be REST fall somewhere between Level 1 and Level 2—true Level 3 REST is rare.

Level 0: The Swamp of POX (Plain Old XML): Everything goes through a single URI endpoint, typically using POST for all operations. The HTTP method and URI provide no semantic information—you have to inspect the request body to understand intent. Example: A SOAP API with a single endpoint /api that accepts XML payloads describing operations. This isn’t REST at all; it’s just HTTP as a transport tunnel. Early web services often operated at this level.

Level 1: Resources: The API introduces multiple URI endpoints, each representing a distinct resource. Instead of /api, you have /users, /orders, /products. However, all operations still use POST, and the request body describes the action. Example: POST /users with a body like {"action": "get", "id": 123}. This is better than Level 0 because resources are identified, but HTTP methods aren’t used semantically. Many internal APIs stop here because it’s simple to implement.

Level 2: HTTP Verbs: The API uses HTTP methods according to their defined semantics. GET for retrieval (idempotent, safe), POST for creation, PUT/PATCH for updates, DELETE for removal. Status codes convey meaning: 200 for success, 201 for created, 404 for not found, 409 for conflicts. Example: GET /users/123 retrieves a user, DELETE /users/123 removes them, PUT /users/123 updates them. This is where most modern “REST” APIs live. Twitter’s API, Stripe’s API, and GitHub’s API are all Level 2. They’re resource-oriented, use HTTP verbs correctly, and leverage status codes, but they don’t implement HATEOAS.

Level 3: Hypermedia Controls (HATEOAS): Responses include links to related resources and available actions, allowing clients to navigate the API dynamically without hardcoding URIs. Example: A GET request to /orders/456 returns not just order data but also links like {"self": "/orders/456", "customer": "/users/789", "cancel": "/orders/456/cancel"}. The client discovers that cancellation is available by following the cancel link. This is true REST as Fielding envisioned it, but it’s complex to implement and rare in practice. PayPal’s HATEOAS API and some banking APIs reach Level 3, but most companies find the complexity outweighs the benefits for public APIs.

Most interviews expect you to understand Level 2 thoroughly and be aware of HATEOAS conceptually. Claiming your API is “RESTful” when it’s Level 1 is a red flag.

Richardson REST Maturity Model

graph TB
    L0["Level 0: Swamp of POX<br/><i>Single endpoint, POST everything</i>"]
    L1["Level 1: Resources<br/><i>Multiple URIs, still POST-heavy</i>"]
    L2["Level 2: HTTP Verbs<br/><i>Proper method semantics</i>"]
    L3["Level 3: HATEOAS<br/><i>Hypermedia controls</i>"]
    
    L0 --> |"Add resource URIs"| L1
    L1 --> |"Use HTTP methods correctly"| L2
    L2 --> |"Add hypermedia links"| L3
    
    subgraph Level 0 Example
        L0Ex["POST /api<br/>Body: {action: 'getUser', id: 123}"]
    end
    
    subgraph Level 1 Example
        L1Ex["POST /users<br/>Body: {action: 'get', id: 123}"]
    end
    
    subgraph Level 2 Example
        L2Ex1["GET /users/123"]
        L2Ex2["PUT /users/123"]
        L2Ex3["DELETE /users/123"]
        L2Ex4["Status: 200, 404, 409"]
    end
    
    subgraph Level 3 Example
        L3Ex["GET /orders/456<br/>Response includes:<br/>{self: '/orders/456',<br/>customer: '/users/789',<br/>cancel: '/orders/456/cancel'}"]
    end
    
    L0 -.-> L0Ex
    L1 -.-> L1Ex
    L2 -.-> L2Ex1
    L2 -.-> L2Ex2
    L2 -.-> L2Ex3
    L2 -.-> L2Ex4
    L3 -.-> L3Ex
    
    Industry["Most APIs: Level 2<br/><i>Stripe, Twitter, GitHub</i>"]
    Rare["Rare: Level 3<br/><i>PayPal HATEOAS API</i>"]
    
    L2 --> Industry
    L3 --> Rare

Most production REST APIs operate at Level 2, using resources and HTTP verbs correctly but skipping HATEOAS. Level 3 adds significant complexity with minimal practical benefit for most use cases, which is why it remains rare despite being ‘true REST’.

Internals

REST doesn’t prescribe specific data structures or algorithms—it’s an architectural style, not an implementation. However, certain patterns emerge in how RESTful systems are built.

Resource Modeling: The core internal decision is how to model your domain as resources. A resource is any concept that can be named and addressed: users, orders, products, sessions. Resources form hierarchies: /users/123/orders/456 represents order 456 belonging to user 123. Singleton resources don’t need IDs: /users/123/profile is the profile for user 123. The key is identifying stable, meaningful nouns—verbs become HTTP methods, not URI paths. Slack’s API models channels, messages, and users as resources; Stripe models customers, charges, and subscriptions.

Representation Negotiation: Clients and servers negotiate the format of resource representations using HTTP’s Accept and Content-Type headers. A client might request Accept: application/json and receive JSON, or Accept: application/xml for XML. The server’s internal representation (database rows, objects) is decoupled from the wire format. This allows you to add new formats (like Protocol Buffers) without changing the API structure.

Idempotency and Safety: REST leverages HTTP method semantics. GET and HEAD are safe (no side effects) and idempotent (multiple identical requests have the same effect as one). PUT and DELETE are idempotent but not safe. POST is neither safe nor idempotent. Implementing idempotency for PUT/DELETE often requires idempotency keys or version checks. Stripe’s API uses Idempotency-Key headers for POST requests to prevent duplicate charges if a request is retried.

Caching Implementation: RESTful systems use HTTP caching headers to control behavior. Cache-Control: max-age=300 tells clients and intermediaries to cache the response for 5 minutes. ETag headers provide version identifiers—clients send If-None-Match on subsequent requests, and the server returns 304 Not Modified if the resource hasn’t changed, saving bandwidth. GitHub’s API heavily uses ETags for rate limit efficiency.

Hypermedia (HATEOAS): Implementing HATEOAS requires embedding links in responses. Formats like HAL (Hypertext Application Language) or JSON:API standardize this. A HAL response looks like: {"_links": {"self": {"href": "/orders/456"}, "customer": {"href": "/users/789"}}, "status": "shipped"}. The server dynamically includes links based on state—a shipped order might not have a “cancel” link. This requires server-side logic to compute available transitions, adding complexity.

Versioning: REST APIs evolve over time. Common strategies include URI versioning (/v1/users), header versioning (Accept: application/vnd.myapi.v2+json), or content negotiation. Stripe uses URI versioning but also supports date-based versioning in headers, allowing clients to opt into specific API behaviors.

RESTful Resource Hierarchy and Caching Flow

graph LR
    Client["Client<br/><i>Web/Mobile App</i>"]
    CDN["CDN/Cache<br/><i>CloudFront</i>"]
    LB["Load Balancer<br/><i>ALB</i>"]
    API1["API Server 1"]
    API2["API Server 2"]
    Cache[("Redis Cache")]
    DB[("PostgreSQL")]
    
    Client --"1. GET /users/123/orders/456<br/>If-None-Match: etag123"--> CDN
    CDN --"Cache MISS"--> LB
    CDN --"2. 304 Not Modified<br/>(Cache HIT)"--> Client
    
    LB --"Route to any server<br/>(stateless)"--> API1
    LB --"Route to any server<br/>(stateless)"--> API2
    
    API1 --"3. Check cache<br/>Key: order:456"--> Cache
    Cache --"4. Cache HIT<br/>Return data"--> API1
    Cache --"Cache MISS"--> API1
    
    API1 --"5. Query if cache miss<br/>SELECT * FROM orders WHERE id=456"--> DB
    DB --"6. Return order data"--> API1
    
    API1 --"7. 200 OK + ETag<br/>Cache-Control: max-age=300"--> LB
    LB --> CDN
    CDN --"8. Cache response<br/>for 5 minutes"--> Client
    
    subgraph Resource Hierarchy
        R1["/users/123"]
        R2["/users/123/orders"]
        R3["/users/123/orders/456"]
        R4["/users/123/profile"]
        R1 --> R2
        R2 --> R3
        R1 --> R4
    end

REST’s statelessness allows any server to handle any request, simplifying load balancing. Multi-layer caching (CDN, application cache) dramatically reduces database load. ETags enable efficient cache validation, returning 304 Not Modified when resources haven’t changed.

Performance Characteristics

REST’s performance profile is shaped by its constraints, particularly statelessness and cacheability. These characteristics make REST excellent for read-heavy workloads but sometimes inefficient for complex operations.

Latency: A single REST request typically completes in 50-200ms for simple operations (GET a resource by ID) when the server is nearby. This includes network round-trip, server processing, and serialization. For cached responses, latency drops to 5-20ms if served from a CDN or local cache. However, REST’s resource-oriented nature can lead to the “N+1 problem”: fetching a list of 10 orders might require 1 request for the list plus 10 more to get customer details for each order, multiplying latency. GraphQL was created partly to solve this problem.

Throughput: RESTful servers can handle thousands to tens of thousands of requests per second per instance, depending on operation complexity. Stripe’s API serves millions of requests per day across thousands of instances. Statelessness enables horizontal scaling—add more servers to increase throughput linearly. Caching dramatically improves effective throughput: if 80% of GET requests hit a CDN cache, your origin servers handle only 20% of the load. GitHub’s API caches aggressively, with cache hit rates above 90% for public repository data.

Scalability: REST’s statelessness is its superpower for scaling. Because each request is self-contained, you can route requests to any server instance without session affinity. This makes REST ideal for global, distributed systems. Airbnb’s API runs across multiple AWS regions, with requests routed to the nearest region. Load balancers don’t need sticky sessions, simplifying infrastructure. The trade-off is that clients must send more data per request (authentication tokens, context) since the server remembers nothing.

Bandwidth Efficiency: REST can be bandwidth-inefficient compared to binary protocols like gRPC. JSON payloads are verbose, and over-fetching is common—a client requesting /users/123 might receive 50 fields when it only needs 3. This is why mobile apps sometimes struggle with REST APIs on slow networks. Compression (gzip, Brotli) helps but doesn’t eliminate the problem. Spotify’s mobile app uses REST for most operations but switches to custom binary protocols for audio streaming.

Caching Impact: Proper caching can reduce server load by 70-90% for read-heavy APIs. Twitter’s public API caches tweet data aggressively, with cache TTLs ranging from seconds (trending topics) to hours (user profiles). The challenge is cache invalidation—when data changes, you must invalidate or update cached copies. REST’s uniform interface makes this manageable: a PUT to /users/123 can trigger cache invalidation for that resource across all CDN nodes.

Trade-offs

REST’s constraints create clear trade-offs that make it excellent for some use cases and awkward for others.

Strengths: REST excels at CRUD operations on well-defined resources. If your domain maps naturally to nouns (users, products, orders), REST feels intuitive. Its alignment with HTTP means you get caching, authentication, and tooling for free—every programming language has HTTP libraries, and tools like Postman and curl work out of the box. REST’s statelessness enables massive horizontal scaling, making it ideal for public APIs serving millions of users. The uniform interface means clients can interact with any RESTful API using the same patterns, reducing learning curves. Stripe’s API is beloved because it’s predictable: if you know REST, you know Stripe.

Weaknesses: REST struggles with operations that don’t map to resources. How do you model “send a password reset email” or “calculate shipping cost”? You end up with awkward endpoints like POST /password-resets or POST /shipping-calculations, which feel like RPC calls disguised as REST. REST’s chattiness is another issue: fetching related data requires multiple round-trips. A mobile app displaying a user’s feed might need 20+ requests to assemble the view, killing performance on slow networks. This is why Facebook created GraphQL. REST also lacks a standard for real-time updates—you’re stuck with polling or bolting on WebSockets, which breaks the REST model. Finally, true HATEOAS is so complex that almost nobody implements it, meaning clients still hardcode URIs, losing one of REST’s theoretical benefits.

Versioning Pain: Evolving REST APIs is tricky. Adding fields is safe, but removing them or changing semantics breaks clients. URI versioning (/v2/users) is common but means maintaining multiple codebases. Header-based versioning is cleaner but harder for clients to discover. Stripe maintains backward compatibility by versioning API behavior by date, allowing clients to opt into changes gradually, but this requires significant engineering effort.

Over-fetching and Under-fetching: REST returns fixed resource representations. If a client needs only a user’s email but GET /users/123 returns 50 fields, you’ve over-fetched. If the client needs the user plus their orders, you’ve under-fetched and need a second request. GraphQL solves this by letting clients specify exactly what they need, but at the cost of complexity.

REST vs GraphQL vs gRPC Trade-offs

graph TB
    subgraph REST Strengths
        RS1["Simple & Familiar<br/><i>HTTP alignment</i>"]
        RS2["Excellent Caching<br/><i>HTTP headers</i>"]
        RS3["Horizontal Scaling<br/><i>Stateless</i>"]
        RS4["Universal Tooling<br/><i>curl, Postman</i>"]
    end
    
    subgraph REST Weaknesses
        RW1["Over/Under Fetching<br/><i>Fixed responses</i>"]
        RW2["Multiple Round-trips<br/><i>N+1 problem</i>"]
        RW3["Awkward Operations<br/><i>Non-CRUD actions</i>"]
        RW4["No Real-time<br/><i>Polling required</i>"]
    end
    
    subgraph GraphQL Strengths
        GS1["Flexible Queries<br/><i>Client specifies fields</i>"]
        GS2["Single Request<br/><i>Fetch related data</i>"]
        GS3["Strong Typing<br/><i>Schema validation</i>"]
    end
    
    subgraph GraphQL Weaknesses
        GW1["Complex Caching<br/><i>Query-dependent</i>"]
        GW2["Server Complexity<br/><i>Resolver logic</i>"]
        GW3["Performance Risk<br/><i>Unbounded queries</i>"]
    end
    
    subgraph gRPC Strengths
        GRPCS1["Low Latency<br/><i>Binary protocol</i>"]
        GRPCS2["Streaming<br/><i>Bidirectional</i>"]
        GRPCS3["Code Generation<br/><i>Type safety</i>"]
    end
    
    subgraph gRPC Weaknesses
        GRPCW1["Browser Support<br/><i>Requires proxy</i>"]
        GRPCW2["Not Human-readable<br/><i>Binary format</i>"]
        GRPCW3["Limited Caching<br/><i>No HTTP semantics</i>"]
    end
    
    UseCase1["Public API<br/>CRUD operations<br/>→ REST"]
    UseCase2["Mobile App<br/>Complex views<br/>→ GraphQL"]
    UseCase3["Microservices<br/>Internal comms<br/>→ gRPC"]
    
    RS1 & RS2 & RS3 --> UseCase1
    GS1 & GS2 --> UseCase2
    GRPCS1 & GRPCS2 --> UseCase3

REST excels for public APIs with simple CRUD operations and heavy caching needs. GraphQL solves REST’s over-fetching and N+1 problems but adds server complexity. gRPC offers the best performance for internal service communication but lacks browser support and HTTP caching benefits.

When to Use (and When Not To)

Choose REST when you need a simple, scalable API for resource-oriented operations, especially if the API will be public-facing or consumed by diverse clients.

Ideal Use Cases: REST is perfect for CRUD-heavy systems where operations map cleanly to resources. E-commerce platforms (products, orders, customers), content management systems (articles, authors, categories), and SaaS applications (accounts, projects, tasks) all fit naturally. If your API will be consumed by third-party developers, REST’s familiarity and tooling support make it the safe choice. Stripe, Twilio, and Slack all use REST for their public APIs because developers already know the patterns. REST is also excellent when caching is critical—if 80% of your traffic is reads, REST’s HTTP caching can dramatically reduce server load. GitHub’s API serves millions of requests per day with relatively modest infrastructure thanks to aggressive caching.

When to Consider Alternatives: If your API involves complex queries, aggregations, or operations that don’t map to resources, consider GraphQL. Facebook, Shopify, and GitHub (alongside their REST API) offer GraphQL for clients needing flexible data fetching. If you need low-latency, high-throughput communication between backend services, gRPC is better—Google, Netflix, and Uber use gRPC for internal microservice communication. For real-time updates (chat, live dashboards), WebSockets or Server-Sent Events are more appropriate than REST. If your operations are naturally procedural (“run this job,” “execute this workflow”), RPC-style APIs might be simpler than forcing them into REST’s resource model.

Hybrid Approaches: Many companies use REST for public APIs and other protocols internally. Uber’s mobile app talks to a REST API gateway, which translates requests to gRPC calls to backend services. Airbnb uses REST for most operations but GraphQL for complex, data-heavy views. The key is matching the protocol to the use case, not forcing everything into one paradigm.

Real-World Examples

company: Stripe context: Payment processing API serving millions of businesses implementation: Stripe’s API is a textbook example of Level 2 REST. Resources like customers, charges, subscriptions, and refunds are modeled with clear hierarchies (/customers/cus_123/subscriptions/sub_456). They use HTTP methods semantically: POST to create, GET to retrieve, DELETE to cancel. Status codes are precise: 402 for payment failures, 429 for rate limits. Stripe includes extensive metadata in responses and supports idempotency keys to prevent duplicate charges. Their API versioning is date-based—clients specify a version like 2023-10-16 in headers, allowing Stripe to evolve the API while maintaining backward compatibility. Stripe doesn’t implement HATEOAS, but their API is so well-documented and consistent that developers rarely struggle. interesting_detail: Stripe’s API uses expandable resources to reduce round-trips. Instead of fetching /charges/ch_123 and then /customers/cus_456 separately, you can request /charges/ch_123?expand[]=customer to get both in one response. This is a pragmatic solution to REST’s chattiness without abandoning the resource model.

company: Twitter (X) context: Social media API serving tweets, users, and timelines implementation: Twitter’s REST API (v2) models tweets, users, lists, and spaces as resources. Endpoints like /tweets/:id, /users/:id/tweets, and /users/:id/followers follow REST conventions. They use GET for reads, POST for creates (tweets, likes), and DELETE for removals. Twitter’s API is heavily cached—public tweet data is served from CDNs with short TTLs (seconds to minutes) to balance freshness and load. Rate limiting is strict: 300 requests per 15 minutes for user timelines, enforced via 429 status codes and X-Rate-Limit-* headers. Twitter doesn’t implement HATEOAS but provides pagination links in response headers (Link: <url>; rel="next"). interesting_detail: Twitter’s API evolved from v1 (not truly RESTful, more RPC-like) to v1.1 (better REST adherence) to v2 (modern REST with better resource modeling). This evolution reflects the industry’s growing understanding of REST principles. V2 introduced field selection (?tweet.fields=created_at,author_id) to reduce over-fetching, borrowing ideas from GraphQL without abandoning REST.

company: Spotify context: Music streaming API for playlists, tracks, and user libraries implementation: Spotify’s Web API is RESTful with resources for tracks, albums, artists, playlists, and users. Endpoints like /tracks/:id, /playlists/:id/tracks, and /me/player (current playback state) follow REST patterns. They use OAuth 2.0 for authentication, with scopes controlling access to user data. Spotify’s API supports pagination via limit and offset parameters, returning next and previous URLs in responses. They use HTTP status codes correctly: 204 for successful DELETE operations, 404 for missing resources, 429 for rate limits. Spotify’s API is read-heavy, so they cache aggressively—public track metadata is cached for hours. interesting_detail: Spotify’s API includes a /me/player resource representing the user’s current playback state. This is a singleton resource (no ID needed) and supports both GET (retrieve state) and PUT (control playback). This shows how REST can model stateful resources even though the protocol itself is stateless—the state lives on the server, not in the session.


Interview Essentials

Mid-Level

Explain the six REST constraints and why statelessness matters for scalability. Be ready to discuss how statelessness enables horizontal scaling and simplifies load balancing.

Describe the Richardson Maturity Model. Know the difference between Level 2 (HTTP verbs) and Level 3 (HATEOAS), and why most APIs stop at Level 2.

Design a RESTful API for a simple domain (e.g., blog with posts and comments). Show proper resource modeling, URI design, and HTTP method usage. Avoid verbs in URIs—use nouns for resources.

Explain how caching works in REST. Discuss Cache-Control, ETag, and when responses should be cacheable vs. non-cacheable.

Discuss idempotency: which HTTP methods are idempotent and why it matters. Be ready to explain how to implement idempotent POST requests (e.g., using idempotency keys).

Senior

Compare REST with GraphQL and gRPC. When would you choose each? Discuss trade-offs around flexibility, performance, and complexity. Be specific: REST for public APIs, gRPC for internal services, GraphQL for complex client queries.

Design a RESTful API for a complex domain (e.g., e-commerce with products, orders, payments, inventory). Handle nested resources, filtering, pagination, and versioning. Discuss how to model operations that don’t fit the resource model (e.g., “apply discount code”).

Explain how to version a REST API without breaking existing clients. Compare URI versioning, header versioning, and content negotiation. Discuss Stripe’s date-based versioning as a case study.

Discuss REST’s limitations for real-time updates and mobile apps. How would you address the N+1 query problem and over-fetching? Consider hybrid approaches (REST + WebSockets, REST + GraphQL).

Explain HATEOAS and why it’s rarely implemented. What are the benefits and costs? Discuss how clients would discover actions dynamically and the complexity of maintaining hypermedia links.

Staff+

Design an API strategy for a company with multiple client types (web, mobile, third-party). Should you offer REST, GraphQL, and gRPC, or standardize on one? Discuss API gateway patterns, versioning across protocols, and backward compatibility.

Discuss how to evolve a REST API at scale (millions of users, thousands of clients). Cover versioning, deprecation policies, monitoring, and migration strategies. How do you sunset old versions without breaking clients?

Explain how REST’s constraints align with or conflict with microservices architecture. Discuss service-to-service communication: should internal APIs be RESTful, or is gRPC better? Consider latency, coupling, and operational complexity.

Design a global, multi-region REST API with low latency and high availability. Discuss CDN caching, regional routing, data consistency, and cache invalidation across regions. How do you handle writes in a distributed system?

Critique a real-world API (e.g., Stripe, Twitter, GitHub). What REST principles does it follow or violate? How would you improve it? Discuss trade-offs between REST purity and pragmatic design.

Common Interview Questions

What is REST, and how does it differ from SOAP? (Focus on architectural style vs. protocol, simplicity, and HTTP alignment.)

Explain the difference between PUT and PATCH. (PUT replaces the entire resource; PATCH applies partial updates. Discuss idempotency.)

How do you handle errors in a RESTful API? (Use HTTP status codes: 400 for client errors, 500 for server errors. Include error details in the response body.)

What is HATEOAS, and why is it important? (Hypermedia as the Engine of Application State: responses include links to related resources. Important for discoverability but rarely implemented.)

How do you design a RESTful API for a many-to-many relationship? (Example: users and groups. Use junction resources: /users/:id/groups and /groups/:id/users.)

Red Flags to Avoid

Confusing REST with any HTTP API. Using POST for everything or ignoring HTTP status codes shows a lack of understanding.

Claiming an API is RESTful when it’s Level 0 or 1 on the Richardson Maturity Model. Know the difference.

Putting verbs in URIs (e.g., /getUser, /createOrder). This is RPC, not REST. Resources should be nouns.

Not understanding idempotency or why it matters for reliability. PUT and DELETE must be idempotent.

Ignoring caching entirely. REST’s cacheability is a core constraint—if you’re not leveraging it, you’re missing a key benefit.

Overcomplicating with HATEOAS when it’s not needed. Know when REST purity conflicts with pragmatism.


Key Takeaways

REST is an architectural style, not a protocol. It’s defined by six constraints: client-server, stateless, cacheable, layered system, uniform interface, and code-on-demand (optional). These constraints enable scalability and simplicity but require discipline to implement correctly.

Most “RESTful” APIs are Level 2 on the Richardson Maturity Model: they use resources, HTTP verbs, and status codes correctly but don’t implement HATEOAS. True Level 3 REST is rare because hypermedia adds complexity without clear benefits for most use cases.

REST excels at CRUD operations on well-defined resources, especially for public APIs. Its alignment with HTTP provides caching, tooling, and familiarity. However, REST struggles with complex queries, real-time updates, and operations that don’t map to resources—consider GraphQL, gRPC, or WebSockets for those scenarios.

Statelessness is REST’s superpower for scaling. Each request is self-contained, enabling horizontal scaling without session affinity. The trade-off is that clients must send more context per request, and the server can’t optimize based on session history.

In practice, REST is about trade-offs. Pure REST (with HATEOAS) is often impractical; pragmatic REST (Level 2) is the industry standard. Know when to follow REST principles strictly and when to bend them for simplicity or performance.