Why this comparison still matters
REST and GraphQL are the two dominant API styles for web services, and the choice between them shapes your application for years. REST (Representational State Transfer) was defined in Roy Fielding's 2000 PhD dissertation and underpins nearly every public web API you have ever used. GraphQL was developed at Facebook in 2012 and released publicly in 2015 as a query language for APIs that solves problems REST leaves open. Neither is strictly better. They optimize for different constraints, and pretending otherwise produces bad architectures.
This article compares them on architecture, fetching efficiency, versioning, caching, learning curve, and tooling, with a clear recommendation at the end.
REST: the HTTP-native default
REST is an architectural style, not a protocol. It maps operations to HTTP verbs (GET, POST, PUT, PATCH, DELETE) against resource URLs (/users/42, /orders/1001/items). Each endpoint returns a fixed shape, and the client gets whatever the server decided to return. REST's genius is its alignment with HTTP itself — caching, content negotiation, status codes, conditional requests (ETag, If-Modified-Since), and idempotency all work without extra effort.
Caching is the headline win. A REST GET response carries Cache-Control and ETag headers that let browsers, CDNs, and proxies cache it transparently. Stripe, GitHub, and Twitter can serve millions of cached reads because HTTP caching is built into the protocol. Debugging REST is also trivial — curl, browser devtools, and any HTTP client work with no special tooling.
The main pain points are over-fetching and under-fetching. A mobile dashboard that needs a user's name and their last three orders might hit /users/42 (returning 40 fields you don't need) and then /users/42/orders (returning 50 orders when you need 3). Workarounds exist — compound documents (JSON:API), ?fields= sparse fieldsets, dedicated /dashboard aggregate endpoints — but they are all bolt-ons rather than first-class solutions.
Versioning is typically done via URL path (/v2/users) or a custom Accept header. Both create migration debt: clients on v1 keep working, but every breaking change means maintaining two code paths until everyone migrates. Stripe's API has been versioned with dates since 2014 and now supports thousands of versions simultaneously — a testament to how much effort versioning can absorb.
GraphQL: the client-shaped query
GraphQL flips the contract. The server publishes a schema describing every type and field it offers; the client sends a query asking for exactly the fields it wants, and the server returns precisely that shape. The dashboard scenario above becomes a single request: query { user(id: 42) { name orders(last: 3) { id total } } }.
That solves over- and under-fetching in one stroke. It also gives you a single endpoint (/graphql) and a self-describing schema that powers code generation, type-safe clients (Apollo, urql, Relay), and tooling like GraphiQL and Apollo Studio. Frontend teams love GraphQL because they can evolve the UI without negotiating new endpoints with the backend.
The trade-offs are real. Caching breaks: POST requests to /graphql are not cached by HTTP intermediaries, so you must implement a client-side normalized cache (Apollo Client, Relay) to avoid re-fetching shared data. The N+1 problem is endemic — a query that fetches a list of authors and their books triggers one database query for the authors plus one per author for their books, which is why DataLoader exists to batch and cache resolver-level fetches.
Authorization moves from the route layer into every resolver. Error handling is non-standard: GraphQL returns HTTP 200 with an errors array, which breaks many HTTP monitoring tools and load-balancer health checks. Query complexity analysis becomes mandatory to prevent abusive queries (a recursive query like { user { friends { friends { ... } } } } can blow up exponentially). File uploads require a separate multipart spec, and subscriptions over WebSocket need their own infrastructure.
The learning curve is steeper. Schemas, resolvers, mutations, subscriptions, fragments, directives, and the normalized cache are concepts your team must absorb. For a small API consumed by one client, that overhead is rarely worth it.
Beyond the basics: BFF, subscriptions, and operations
The Backend-for-Frontend (BFF) pattern has become the most common way to use GraphQL well. Instead of exposing a single GraphQL API to all clients, you deploy one GraphQL service per client type (web, iOS, Android) that aggregates downstream REST or gRPC services. Each BFF shapes data for its client without coupling the underlying services to a particular UI. This pattern, popularized at SoundCloud and Expedia, addresses GraphQL's operational complexity by scoping it to a thin orchestration layer rather than the entire backend.
GraphQL subscriptions extend the model to real-time data over WebSocket. A subscription query declares which events the client cares about; the server pushes updates when those events fire. The wire protocol is defined (graphql-ws, formerly graphql-transport-ws), but implementation requires WebSocket infrastructure, connection lifecycle management, and careful subscription cleanup. REST achieves the same effect with Server-Sent Events (simpler, unidirectional, auto-reconnecting) or raw WebSockets (bidirectional), both well-understood and operationally simpler than GraphQL subscriptions.
File uploads in GraphQL required a separate multipart specification (the graphql-multipart-request-spec by Jayden Seric) because the standard transport is JSON over HTTP POST. The result is a hybrid request: a multipart form with the GraphQL operations in one part and file blobs in others. REST handles file uploads as a single multipart POST — no special spec, no client-side encoding gymnastics.
The error model deserves attention. REST's HTTP status codes (200, 201, 400, 401, 403, 404, 422, 429, 500) are understood by every HTTP client, load balancer, and monitoring tool. GraphQL returns 200 with an errors array containing message, path, and extensions — which means your monitoring must parse GraphQL responses rather than rely on status codes. Some teams wrap GraphQL in a thin REST proxy just to recover status-code semantics for their dashboards.
Side-by-side comparison
When to choose which
Choose REST when you are exposing a public API consumed by many independent clients you don't control. Stripe, GitHub, and Twitter all use REST for their public APIs because REST's HTTP alignment makes their APIs cacheable, debuggable with curl, and approachable to anyone who knows HTTP. If you need webhooks, file uploads, server-sent events, or HTTP-level caching, REST handles these natively where GraphQL requires workarounds. REST is also the right choice for internal microservices where simplicity and operational maturity matter more than flexible queries.
Choose GraphQL when you have a single complex client (typically a large SPA or mobile app) hitting many related resources per screen, and the cost of over-fetching is measurable in bandwidth or latency. The New York Times, Shopify, and GitHub (in their v4 API) adopted GraphQL because the volume of data their clients needed was not served well by fixed-shape REST endpoints. GraphQL also shines when many different frontends need different slices of the same data — the schema lets each client ask for what it needs without backend changes.
Avoid GraphQL for simple CRUD APIs, internal services with one consumer, or cases where your team has no GraphQL expertise. The operational overhead — schema design, resolver performance, query complexity analysis, normalized cache tuning, persistence of the cache — is significant and ongoing. Avoid REST only when over-fetching is causing real bandwidth or latency pain at scale; otherwise REST is fine and far simpler.
A hybrid pattern is increasingly common: REST for external/public APIs, GraphQL for internal product UIs. That gives you cache-friendly external surfaces and flexible internal queries without forcing one model everywhere. GitHub does exactly this — REST v3 for the public API, GraphQL v4 for the same data with finer-grained access. If you go this route, invest in a GraphQL gateway (Apollo Router, or graphql-ruby with persisted queries for Ruby shops) that handles caching, rate limiting, and query complexity analysis in one well-instrumented place — so your backend services stay simple REST endpoints and the orchestration complexity lives in a single layer you can monitor and tune.
Conclusion
REST and GraphQL are not competitors in the abstract; they are tools optimized for different constraints. REST optimizes for HTTP alignment, cacheability, and simplicity. GraphQL optimizes for client flexibility and avoiding round-trips. The right choice depends on who consumes your API and how varied their data needs are. Default to REST for public APIs and simple services; reach for GraphQL when you have a sophisticated client that would otherwise require multiple REST calls per screen.