GraphQL has been production-viable for a few years now, and a new internal API project this year reopened a debate we hadn't fully settled internally: REST's simplicity versus GraphQL's flexible querying. Different engineers on the team had strong, somewhat calcified opinions from past projects, and rather than let the decision come down to whoever argued loudest in the kickoff meeting, we tried to work out an actual rule based on the shape of each project's consumers.
The case that tipped toward GraphQL
The API in question serves several frontends, a dashboard, a mobile app, and a partner integration, each needing different shapes of the same underlying data, which is exactly the scenario GraphQL is built for. The dashboard needs deeply nested data for a handful of detail views but shallow data everywhere else; the mobile app needs a much leaner payload across the board given bandwidth and battery constraints; the partner integration needs a stable, narrow slice of fields regardless of what either of the other two consumers are doing. Under REST, satisfying all three well would have meant either three separate sets of endpoints, or one generic endpoint that over-fetches for the mobile client and under-fetches for the dashboard, forcing extra round trips.
With GraphQL, each consumer writes exactly the query that matches its own needs against a single schema, which eliminated both the over-fetching and the proliferation of near-duplicate REST endpoints we'd otherwise have ended up maintaining. The schema also gave us a single, strongly typed contract that all three frontend teams could develop against independently once it was defined, using GraphQL's introspection to generate client-side types automatically rather than hand-maintaining TypeScript interfaces that could drift from the actual API shape over time.
- The API serves several frontends, a dashboard, a mobile app, and a partner integration, each needing different shapes of the same underlying data, which is exactly the scenario GraphQL is built for.
- REST's simplicity and better caching story still won out for a smaller, single-consumer service we shipped the same quarter, reinforcing that the answer genuinely depends on the number of consumers.
The case that tipped the other way
REST's simplicity and better caching story still won out for a smaller, single-consumer service we shipped the same quarter, reinforcing that the answer genuinely depends on the number of consumers rather than being a blanket preference either way. That service has exactly one consumer, an internal reporting tool, with a stable and well-understood set of data needs that doesn't vary by client. Standing up a GraphQL schema, resolvers, and the associated tooling for a single consumer with simple needs would have been meaningfully more infrastructure than the problem warranted, and REST's straightforward HTTP caching semantics, which GraphQL's single-endpoint, POST-based model makes considerably harder to leverage, mattered more here than flexible querying that nobody was actually going to use.
Costs on the GraphQL side that don't show up in the pitch
It's worth naming what GraphQL cost us that a REST equivalent wouldn't have. Query complexity and depth limiting needed explicit attention almost immediately, since an unrestricted GraphQL schema lets a client construct a deeply nested query that fans out into an expensive database access pattern without the API author having designed for that specific shape of request, a class of problem REST's fixed endpoint shapes mostly sidesteps by construction. We added query cost analysis and depth limits before the API went to partners specifically, since an external partner's client is the one consumer whose queries you can't fully trust to be well-behaved by default.
N+1 query problems also needed real attention on the resolver side; a naive resolver implementation for nested fields will happily issue one database query per item in a list rather than batching them, and this bit us during load testing before we introduced a batching layer using the dataloader pattern. None of this is a reason to avoid GraphQL, but it's real complexity that a team evaluating GraphQL purely on flexible-querying appeal should budget for rather than discovering under load in production.
Our rule going forward
Versioning looks different under each approach
Versioning strategy ended up being one of the more concrete differences in day-to-day maintenance between the two services. The REST service versions at the URL level, `/v2/reports`, when a breaking change is unavoidable, which is simple to reason about but means maintaining parallel endpoint implementations during a deprecation window. The GraphQL schema instead evolves field by field, marking an old field deprecated with a directive while introducing its replacement alongside it, and consumers migrate to the new field at their own pace before the deprecated one is eventually removed. Neither approach is strictly better, but the GraphQL model has meant noticeably fewer coordinated "everyone needs to move to v3 by this date" migrations across the three consuming teams, since each team migrates individual fields independently as their own schedule allows.
Authorization at the field level added real design work
Authorization also took more deliberate design in the GraphQL service than the REST equivalent required. A REST endpoint's authorization check happens once, at the route level, before any data is fetched, but a single GraphQL query might touch a dozen different fields across several types, some of which the requesting client should see and some of which it shouldn't, particularly given that the partner integration client needs a narrower view of the schema than our internal dashboard does. We implemented field-level authorization directly in the resolvers rather than relying solely on a single top-level check, which is more code to write and reason about than REST's simpler model, but is also the only way to safely expose one unified schema to consumers with genuinely different access levels rather than maintaining separate schemas per consumer.
Tooling and developer experience differences the pitch decks don't emphasize
Choosing GraphQL also meant investing in a different tooling ecosystem than the team's existing REST-focused habits assumed: a schema registry, code generation for client-side types from the schema, and a resolver-testing approach that doesn't map directly onto the request-and-response testing patterns the team already had muscle memory for with REST endpoints. None of this was prohibitively difficult, but it was real ramp-up time that a team choosing GraphQL purely for its query-flexibility pitch should plan for explicitly rather than assuming the switch is purely a schema-design decision with no accompanying tooling investment.
Documentation ended up easier on the GraphQL side, which surprised us
One genuine, somewhat unexpected win on the GraphQL side was documentation quality. GraphQL's introspection means the schema is always an accurate, queryable description of the API's actual current capabilities, and tools built on that introspection generate interactive documentation automatically that can't drift out of sync with the real API the way a hand-maintained REST API doc can. The REST service's documentation, by contrast, still depends on someone remembering to update a separate OpenAPI spec whenever an endpoint changes, and predictably, it has already drifted slightly out of sync with the actual implementation in at least one place we've caught so far.
Revisiting the rule a year later
A year after settling on this rule, it's held up well enough that we've stopped re-litigating it project by project, which was the actual goal from the start. Two additional internal services shipped since, one clearly matching the multiple-differently-shaped-consumers profile and built in GraphQL without much debate, one a single-consumer internal tool built in REST just as quickly. Having a concrete, previously-argued-through rule to point to has turned what used to be a multi-meeting architecture debate into a five-minute conversation, which by itself has probably saved more engineering time than either technology choice did on its own.
We also underestimated, going in, how much the schema itself would become a useful artifact for cross-team planning conversations that have nothing to do with implementation. Product and design have started referencing the GraphQL schema directly when scoping a new feature, since it's a precise, always-current description of what data already exists and in what shape, which has occasionally shortened a scoping conversation that would otherwise have required pulling in an engineer just to answer "do we already have this data available somewhere."
GraphQL when multiple, differently-shaped consumers exist; REST when there's one primary consumer and simplicity matters more than flexibility. It's a narrower rule than either camp's original pitch, but it's held up across the projects we've applied it to since, and it's ended the recurring architecture debate by giving the team a concrete question to ask, how many differently-shaped consumers does this API actually have, rather than a values-based argument about which technology is generally better.