When combining both protocols pays off instead of a full migration
The debate between GraphQL and REST is often framed as an either-or choice, but in practice the two protocols do not have to be mutually exclusive. REST remains the pragmatic choice for simple CRUD resources and webhooks, while GraphQL earns its place on complex, aggregating read operations in the frontend. This article shows what a hybrid architecture with shared backend logic looks like in practice, what it costs the team in tooling and coordination, and when the added complexity simply is not worth it.
Table of Contents
- 1. Why the question rarely comes down to either-or
- 2. A typical split: REST for CRUD, GraphQL for aggregating read operations
- 3. Shared backend logic behind both interfaces
- 4. Team and tooling costs of a two-protocol strategy
- 5. Caching behavior: HTTP caching versus query-specific caching
- 6. Comparing versioning and schema evolution
- 7. Security considerations: rate limiting and query complexity
- 8. When the added complexity is not worth it
- 9. A decision guide for your own architecture
- 10. Summary
- 11. FAQ
1. Why the question rarely comes down to either-or
In many teams the debate between GraphQL and REST gets framed as a binary decision, where one protocol is expected to fully replace the other. In practice this either-or framing rarely matches the actual requirements, because the two protocols are simply well suited to different access patterns. A team that adopts GraphQL purely to retire an existing REST backend often ends up carrying problems that REST never had into a new, more complex layer.
It usually makes more sense to use GraphQL where its strengths genuinely apply and to leave REST in place where it already works well. This hybrid mindset does require a deliberate architectural decision and a bit more coordination within the team, but it avoids the cost of a full migration whose benefit rarely justifies the effort in most projects. The rest of this article walks through what such a split looks like in practice and where its limits are.
2. A typical split: REST for CRUD, GraphQL for aggregating read operations
A proven split assigns simple CRUD resources to REST: creating, reading, updating, and deleting individual, clearly bounded entities such as orders, customers, or products. For these operations REST offers an immediate advantage through its established HTTP methods, status codes, and easy testability with curl or Postman, without needing an additional query schema. Webhooks, outbound notifications to external systems triggered by events, also fit REST structurally better, since at their core they are simple, individual POST requests with a fixed payload shape.
GraphQL earns its keep on complex, aggregating read operations in the frontend, where a single view needs data pulled from several deeply nested resources. A product detail page that has to show pricing, stock levels, reviews, and related products at once would need several separate REST requests, or a purpose-built endpoint, while GraphQL lets exactly this combination be expressed in a single query. For frontend teams that frequently build new views with shifting data combinations, that noticeably reduces the back-and-forth with the backend team.
3. Shared backend logic behind both interfaces
To keep introducing GraphQL alongside REST from creating duplicated business logic, the actual domain logic should live in a shared service layer that both REST controllers and GraphQL resolvers call into. In a Symfony application that means a use case such as fetching an order is implemented as its own service, independent of both the HTTP layer and the GraphQL schema.
This pattern keeps validation rules, authorization checks, and calculation logic from being implemented twice and drifting apart over time. The REST controller and the GraphQL resolver become thin adapter layers that simply translate between their respective protocol format and the shared domain layer, which also makes testing considerably easier, since the actual logic can be tested independently of the transport protocol.
<?php
declare(strict_types=1);
namespace App\Order\Application;
final class GetOrderService
{
public function __construct(
private readonly OrderRepositoryInterface $orderRepository,
) {
}
/**
* Loads an order by its ID.
* Used by both the REST controller and the GraphQL resolver.
*/
public function execute(string $orderId): Order
{
$order = $this->orderRepository->findById($orderId);
if ($order === null) {
throw new OrderNotFoundException($orderId);
}
return $order;
}
}
// REST controller (thin adapter)
final class OrderController
{
public function __construct(private readonly GetOrderService $getOrderService)
{
}
public function show(string $orderId): JsonResponse
{
$order = $this->getOrderService->execute($orderId);
return new JsonResponse(OrderNormalizer::normalize($order));
}
}
// GraphQL resolver (thin adapter, same service layer)
final class OrderResolver
{
public function __construct(private readonly GetOrderService $getOrderService)
{
}
public function resolveOrder(string $orderId): Order
{
return $this->getOrderService->execute($orderId);
}
}
4. Team and tooling costs of a two-protocol strategy
The price of a two-protocol strategy rarely shows up immediately, it accumulates over time in daily operations. Two interfaces mean two sets of documentation that must stay in sync, two different testing approaches (contract tests for REST, schema validation and query tests for GraphQL), and two monitoring strategies, since GraphQL error rates and latency can no longer be measured cleanly per endpoint but have to be tracked per field or resolver instead.
The team itself also needs to be comfortable with both mindsets: REST resource modeling follows different principles than GraphQL schema design with types, interfaces, and resolvers, and not every developer brings equal fluency in both. Smaller teams often underestimate how much extra coordination is needed to decide which new requirement should be implemented through which protocol, at least until that decision has settled into an actual team convention.
5. Caching behavior: HTTP caching versus query-specific caching
REST benefits directly from HTTP caching mechanisms such as ETag, Last-Modified, and Cache-Control headers, which browsers, CDNs, and reverse proxies like Varnish understand without any extra configuration. A GET request against a product resource can be cached with comparatively little effort, because the URL itself uniquely identifies which resource is meant.
GraphQL makes this pattern structurally harder, because virtually all requests hit the same endpoint via POST and the actual content of the request sits in the request body, which HTTP caches do not inspect by default. Caching has to be rebuilt at the application level instead, using something like persisted queries or a GraphQL-specific cache layer such as Apollo Server's cache control directives, which requires additional infrastructure and additional know-how on the team.
6. Comparing versioning and schema evolution
REST APIs are traditionally versioned through URL prefixes or headers, for example /v1/orders and /v2/orders, whenever the resource shape changes in an incompatible way. This versioning is explicit and visible, but it requires old versions to be maintained in parallel as long as consumers still depend on them.
GraphQL deliberately takes a different approach: instead of explicit versions, the schema evolves gradually, new fields get added, outdated fields get marked with an @deprecated notice rather than being removed right away. This works well as long as breaking changes are genuinely avoided, but it demands discipline in schema maintenance, because deprecated fields tend to stick around in practice longer than originally planned.
7. Security considerations: rate limiting and query complexity
With REST, rate limiting can be configured relatively simply per endpoint and HTTP method, since every resource and every operation has its own clearly identifiable URL. An API gateway can therefore control granularly how often a client is allowed to call a given operation.
GraphQL needs a different safeguard here, because a single request can, through deep nesting, become arbitrarily expensive for the backend even though it is technically only one request. Query complexity analysis is the common answer: every incoming query is assigned a cost score before execution, and a maximum nesting depth is enforced in the schema or in middleware, rejecting overly expensive queries up front.
8. When the added complexity is not worth it
Not every project benefits from a hybrid strategy. With a manageable domain made up of a handful of clearly bounded resources and a single frontend team consuming the API, the extra overhead of a second interface usually does not pay off. The additional operational, testing, and documentation burden of GraphQL is worth it mainly when multiple frontend teams with different, frequently changing data needs are actually working against the same API.
Small teams without dedicated GraphQL schema design experience should also question the introduction critically, since a poorly designed GraphQL schema with N+1 problems and no query complexity control can cause more trouble than it solves. In such cases a well modeled REST API with purpose-built aggregation endpoints tailored to frontend needs is often the more pragmatic, lower-maintenance solution.
9. A decision guide for your own architecture
The table below summarizes the key decision criteria to help weigh, for a concrete project, whether and to what extent a hybrid strategy combining REST and GraphQL is worth the added complexity.
| Criterion | REST | GraphQL | Recommendation |
|---|---|---|---|
| Resource caching | Simple via HTTP headers | Needs additional infrastructure | REST for heavily cached read resources |
| Aggregating read queries | Multiple requests needed | One query for nested data | GraphQL for complex frontend views |
| Webhooks and events | Native pattern | Uncommon, no standard | REST for outbound notifications |
| Protection against expensive queries | Per-endpoint rate limit | Needs query complexity analysis | Plan for extra effort with GraphQL |
| Team ramp-up | Widely known | Requires schema-first thinking | Only introduce GraphQL when genuinely needed |
Mironsoft
OpenAPI design, Symfony APIs, and API security
APIs that external teams can integrate without back-and-forth questions?
We review existing REST APIs for inconsistent error formats, missing OpenAPI documentation, and security gaps, then build an API that is clearly documented, versioned, and hardened against abuse.
API Review
Checking the OpenAPI spec, error formats, and status codes for consistency.
Symfony Implementation
Using DTOs, Serializer, and Validator for clean, type-safe request/response models.
Security Audit
Hardening rate limiting, auth schemes, and input validation against real attack surfaces.
10. Summary
GraphQL and REST Hybrid: The Essentials at a Glance
Core idea
GraphQL and REST are not mutually exclusive, they cover different access patterns and can share the same backend logic underneath.
Typical split
REST for CRUD resources and webhooks, GraphQL for complex, aggregating read operations in the frontend.
Biggest cost factor
Duplicated documentation, duplicated testing, and a team that has to be fluent in both mindsets at once.
When it is not worth it
For small domains with a single frontend team, a well modeled REST API is usually the simpler choice.