Hybrid Approach — REST and GraphQL Together
In this tutorial, you'll learn about the Hybrid Approach. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
A hybrid approach runs both REST and GraphQL APIs, routing each request to the most appropriate style. REST handles simple CRUD and public endpoints, GraphQL powers complex internal UIs.
What You'll Learn
By the end of this lesson, you will design a hybrid API architecture, use GraphQL to wrap REST endpoints, and implement use-case-based routing.
Why It Matters
Choosing between REST and GraphQL is not binary. Many successful companies run both. A hybrid approach lets you use each tool where it excels.
Real-World Use
Shopify runs GraphQL for admin and REST for storefront. Netflix uses GraphQL for client apps and REST for service-to-service communication.
Hybrid Architecture
flowchart TD
Client[Client Apps] --> Router[API Router]
Router -->|Simple CRUD| REST[REST Endpoints]
Router -->|Complex Queries| GQL[GraphQL Endpoint]
REST --> Services[Backend Services]
GQL --> Services
GQL --> RESTWrapper[GraphQL wraps REST]
GraphQL Wrapping REST
# graphql_wraps_rest.py
from typing import Any, Dict
class RESTClient:
def get_user(self, user_id: int) -> Dict:
return {"id": user_id, "name": "Alice", "email": "a@x.com"}
def get_orders(self, user_id: int) -> Dict:
return {"orders": [{"id": 101, "total": 50}]}
class GraphQLWrapper:
def __init__(self):
self.rest = RESTClient()
def resolve_user(self, user_id: int, fields: list) -> Dict:
user = self.rest.get_user(user_id)
return {f: user[f] for f in fields if f in user}
def resolve_user_with_orders(self, user_id: int, user_fields: list,
order_fields: list) -> Dict:
user = self.resolve_user(user_id, user_fields)
orders = self.rest.get_orders(user_id)
user["orders"] = [
{f: o[f] for f in order_fields if f in o}
for o in orders["orders"]
]
return user
def query(self, gql_query: str) -> Dict:
# Simplified query parser
if "orders" in gql_query:
return {"data": self.resolve_user_with_orders(1, ["name", "email"], ["id", "total"])}
return {"data": self.resolve_user(1, ["name", "email"])}
wrapper = GraphQLWrapper()
rest_client = RESTClient()
print(f"REST: {rest_client.get_user(1)}")
print(f"REST: {rest_client.get_orders(1)}")
gql = wrapper.query("{ user(id: 1) { name email orders { id total } } }")
print(f"GraphQL (wrapping REST): {gql['data']}")
Expected output:
REST: {'id': 1, 'name': 'Alice', 'email': 'a@x.com'}
REST: {'orders': [{'id': 101, 'total': 50}]}
GraphQL (wrapping REST): {'name': 'Alice', 'email': 'a@x.com', 'orders': [{'id': 101, 'total': 50}]}
Use Case Router
# use_case_router.py
from typing import Any, Callable, Dict
class UseCaseRouter:
def __init__(self):
self.routes: Dict[str, callable] = {}
self.gql_routes: Dict[str, callable] = {}
def rest(self, pattern: str):
def wrapper(fn):
self.routes[pattern] = fn
return fn
return wrapper
def graphql(self, query_type: str):
def wrapper(fn):
self.gql_routes[query_type] = fn
return fn
return wrapper
def handle(self, method: str, path: str, body: Dict = None) -> Dict:
if path.startswith("/graphql") and body and "query" in body:
query = body["query"]
for qtype, handler in self.gql_routes.items():
if qtype in query:
return {"style": "graphql", "data": handler(body.get("variables", {}))}
return {"error": "Unknown query"}
handler = self.routes.get(path)
if handler:
return {"style": "rest", "data": handler()}
return {"error": "Not found"}
router = UseCaseRouter()
@router.rest("/users")
def list_users():
return [{"id": 1, "name": "Alice"}]
@router.rest("/users/1")
def get_user():
return {"id": 1, "name": "Alice"}
@router.graphql("users")
def gql_users(variables):
return [{"id": 1, "name": "Alice"}]
@router.graphql("user")
def gql_user(variables):
return {"id": 1, "name": "Alice"}
print(router.handle("GET", "/users"))
print(router.handle("POST", "/graphql", {"query": "{ users { id name } }"}))
print(router.handle("GET", "/unknown"))
Expected output:
{'style': 'rest', 'data': [{'id': 1, 'name': 'Alice'}]}
{'style': 'graphql', 'data': [{'id': 1, 'name': 'Alice'}]}
{'error': 'Not found'}
Data Source Abstraction
# data_source.py
from typing import Any, Dict, List
class UserRepository:
def __init__(self):
self.users = {
1: {"id": 1, "name": "Alice", "email": "a@x.com", "role": "admin"},
2: {"id": 2, "name": "Bob", "email": "b@x.com", "role": "user"},
}
def get_by_id(self, user_id: int) -> Dict:
return self.users.get(user_id, {})
def list_all(self) -> List[Dict]:
return list(self.users.values())
class RESTAdapter:
def __init__(self, repo: UserRepository):
self.repo = repo
def handle(self, path: str) -> Dict:
if path == "/users":
return {"users": self.repo.list_all()}
return {"user": self.repo.get_by_id(1)}
class GraphQLAdapter:
def __init__(self, repo: UserRepository):
self.repo = repo
def resolve(self, fields: List[str], user_id: int = None) -> Dict:
if user_id:
user = self.repo.get_by_id(user_id)
return {f: user.get(f) for f in fields}
users = self.repo.list_all()
return [{f: u.get(f) for f in fields} for u in users]
repo = UserRepository()
rest_adapter = RESTAdapter(repo)
gql_adapter = GraphQLAdapter(repo)
# Same data, different access patterns
print(f"REST: {rest_adapter.handle('/users')}")
print(f"GraphQL: {gql_adapter.resolve(['id', 'name'], 1)}")
Expected output:
REST: {'users': [{'id': 1, 'name': 'Alice', 'email': 'a@x.com', 'role': 'admin'}, {'id': 2, 'name': 'Bob', 'email': 'b@x.com', 'role': 'user'}]}
GraphQL: {'id': 1, 'name': 'Alice'}
Common Mistakes
1. Duplicating Business Logic
Maintaining separate REST and GraphQL handlers for the same logic doubles bugs. Share a service layer.
2. Inconsistent Security
REST and GraphQL must apply the same authentication and authorization. Different implementations can create security gaps.
3. Mixed Caching Strategies
If REST caches responses but GraphQL bypasses cache, users see inconsistent data. Coordinate caching at the data source level.
4. No Unified Documentation
Documenting REST separately from GraphQL confuses developers. Create a unified API portal.
5. Premature Hybrid Architecture
Starting with both is unnecessary complexity. Begin with REST or GraphQL, add the second when the need is proven.
Practice Questions
1. When should you use a hybrid REST/GraphQL approach?
When you have different client types with different needs, or when migrating from REST to GraphQL gradually.
2. How does GraphQL wrap REST endpoints?
GraphQL resolvers call REST endpoints internally, transforming the REST response into the requested GraphQL fields.
3. What is the main risk of a hybrid approach?
Inconsistency between REST and GraphQL for the same data. Both must use the same business logic.
4. How do you share logic between REST and GraphQL?
Extract business logic into a shared service layer that both REST handlers and GraphQL resolvers call.
Challenge
Design a hybrid API for an e-commerce platform: REST for product listing and checkout, GraphQL for the admin dashboard. Share a service layer between both.
FAQ
Mini Project: Hybrid API Gateway
# hybrid_gateway.py
from typing import Any, Callable, Dict
class HybridGateway:
def __init__(self):
self.rest_handlers: Dict[str, Callable] = {}
self.gql_resolvers: Dict[str, Callable] = {}
def rest(self, path: str, handler: Callable):
self.rest_handlers[path] = handler
def graphql(self, field: str, resolver: Callable):
self.gql_resolvers[field] = resolver
def process(self, request: Dict) -> Dict:
if request.get("type") == "rest":
handler = self.rest_handlers.get(request["path"])
return {"from": "rest", "data": handler() if handler else "not found"}
elif request.get("type") == "graphql":
resolver = self.gql_resolvers.get(request.get("field"))
return {"from": "graphql", "data": resolver() if resolver else "not found"}
return {"error": "unknown"}
gw = HybridGateway()
gw.rest("/users", lambda: [{"id": 1}])
gw.graphql("users", lambda: [{"id": 1, "name": "Alice"}])
print(gw.process({"type": "rest", "path": "/users"}))
print(gw.process({"type": "graphql", "field": "users"}))
Expected output:
{'from': 'rest', 'data': [{'id': 1}]}
{'from': 'graphql', 'data': [{'id': 1, 'name': 'Alice'}]}
What's Next
You understand the hybrid approach. Finally, learn how to choose the right API style for your project.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro