We’ve built on top of systems that were not designed with GraphQL in mind. The payments service runs on gRPC. Internal tools speak REST. A few teams are already writing GraphQL, but most are not. Meanwhile, product expects everything to be available behind a single GraphQL endpoint, and we’re the ones trying to make that possible.
Rewriting services to expose them through GraphQL isn’t a responsible use of engineering time. These systems are stable and already serving production traffic.
Rebuilding them means duplicating logic, managing multiple representations of the same domain, and introducing failure paths that didn’t exist before.
We could try proxy layers instead, but they tend to accumulate state, embed translation logic that’s hard to test, and eventually drift from the source system.
Grafbase Extensions offer a way to connect existing REST and gRPC services to a GraphQL federated graph without layering on custom gateways or writing glue code.
This guide will walk you through how to expose gRPC methods as GraphQL fields using .proto
definitions, map them into a subgraph, and query them from the gateway as if they were native to the graph. Before going into the technical details, let’s first understand how REST, gRPC and GraphQL can be integrated in a single ecosystem.
We rarely work in stacks built around a single protocol. REST is still everywhere, especially in mobile clients, partner APIs, and internal administrative tools. gRPC tends to handle service-to-service communication, especially in domains like payments, orders, or inventory, where performance and structure are critical.
GraphQL, for most teams, enters later, often through a few greenfield services or a frontend-led initiative. You’re not alone if you find yourself maintaining all three at once. This mix is common, and for good reason:
- REST offers a simple, well-supported interface for external consumers and UI teams.
- gRPC excels at structured, high-performance communication across internal services.
- GraphQL helps unify client access and reduce the overhead of tightly coupled endpoints.
The protocols exist for different reasons, but the moment you try to offer a unified API surface (especially through federation), you need a way to make them work together without forcing upstream changes.
Once you decide to bring REST or gRPC services into a GraphQL gateway, the gaps between these protocols become more visible. Each one makes different assumptions about schemas, serialization, and control flow. GraphQL expects a strongly typed schema from the start, but REST often relies on conventions or implicit contracts that were never meant to be queried declaratively.
You might start by trying to map endpoints to fields manually, but you’ll quickly run into the following issues:
-
Schema translation: Neither REST nor gRPC exposes a GraphQL-style schema by default. You need to define types, restructure payloads, and rename fields to match GraphQL expectations, which often means duplicating logic that already exists elsewhere.
-
Custom resolvers and orchestration: Connecting multiple endpoints or RPCs often requires building a resolver layer that joins responses, handles errors, and keeps things aligned across services.
-
Data mapping across protocols: REST uses JSON, gRPC uses Protobuf, and GraphQL expects a typed object graph. Moving between them means writing transformation logic that handles edge cases, nulls, and deeply nested structures, none of which are easy to maintain.
You can build your gateway, write this plumbing by hand, and maintain it over time. Alternatively, you can look for tools that treat these protocols as part of the same architectural story, thereby removing the need for hand-coded stitching.
When working with services that already expose gRPC endpoints, you don’t want to rewrite or wrap them to get them into your GraphQL gateway. Grafbase’s gRPC extension gives you a way to expose those existing methods as GraphQL fields by building on top of your .proto
files.
You don’t need to write custom resolvers or build a manual translation layer. Instead, you define your service and message types in Protobuf, and Grafbase uses that to generate a GraphQL subgraph that mirrors your gRPC API. This approach gives you schema alignment, field definitions, and type safety from day one.
Here’s what the extension supports out of the box:
- Start from
**.proto**
files: Use your existingProtobuf
definitions without needing to change upstream service logic. - Generate a GraphQL subgraph automatically: With protoc-gen-grafbase-subgraph, you can compile your
.proto
files into a GraphQL schema that plugs directly into your federated graph. - Support for streaming: Server-side streaming maps to GraphQL subscriptions. Client streaming is supported as well.
- No manual mapping required: Grafbase handles the protobuf-to-GraphQL schema translation, so you don’t need to write type mappings or boilerplate resolvers.
Next, we’ll walk through exactly how to do that.
To show how the gRPC extension works in practice, we’ll walk through a minimal setup: defining a gRPC service in Protobuf, running it locally, and exposing its methods in a federated GraphQL graph using Grafbase.
This example uses Python for the gRPC server and client, but the Grafbase integration works the same regardless of the language used in your upstream services.
Before you begin, make sure you have the following installed:
- Python: This guide will use Python to create a gRPC server. Install Python by following these steps.
- Grafbase CLI: This guide uses Grafbase CLI to run Grafbase commands locally. Install Grafbase CLI by following the instructions here.
- Protocol buffers: Here’s a guide to installing protoc (the protobuf compiler).
- Protoc-gen-grafbase-subgraph : Install the
protoc-gen-grafbase-subgraph
by following the instructions in the GitHub release.
In your main project folder, create a file called requirements.txt
and add the following contents:
grpcio
grpcio-tools
protobuf
Run the command to install the packages you need for the gRPC server on your local terminal:
pip install -r requirements.txt
The command above installs the latest version of grpcio on your local machine.
Create a Proto file
A .proto file is written in Protobuf's format, and it contains all of the information about the structure of the data and the available services. Create a file in the root directory of your folder called orders.proto
. In that file, add the following content:
syntax = "proto3";
package orders;
// Top-level service
service OrdersPricingService {
// Returns the full order (line items, quantities, etc.)
rpc GetOrder (GetOrderRequest) returns (GetOrderResponse);
// Returns the total price for a given order
rpc GetPrice (GetPriceRequest) returns (GetPriceResponse);
}
// --- Messages ---
message GetOrderRequest {
string order_id = 1;
}
message LineItem {
string sku = 1;
uint32 qty = 2;
double price = 3;
}
message GetOrderResponse {
string order_id = 1;
repeated LineItem items = 2;
}
message GetPriceRequest {
string order_id = 1;
}
message GetPriceResponse {
string order_id = 1;
double total_price = 2;
}
Now that you have your .proto
file, the next step is to compile it to generate the Python stubs. You can do this by running the following command in your terminal:
python3 -m grpc_tools.protoc \
--proto_path=. \
--python_out=. \
--grpc_python_out=. \
orders.proto
Executing this code produces two new files in your local directory named orders_pb2.py
and orders_pb2_grpc.py
. These files are autogenerated and they allow you create your servers and clients. At this point, your folder structure should look like this:
├── orders.proto
├── orders_pb2.py
├── orders_pb2_grpc.py
└── requirements.txt
Create a server
Create a file in your root directory called server.py
and add the following content:
import grpc
from concurrent import futures
import orders_pb2
import orders_pb2_grpc
# --- Sample in-memory data ---------------------------------------------------
_SAMPLE_ORDERS = {
"A100": [
{"sku": "SKU-1", "qty": 2, "price": 19.99},
{"sku": "SKU-2", "qty": 1, "price": 5.49},
],
"B200": [
{"sku": "SKU-3", "qty": 5, "price": 2.25},
],
}
# --- Service implementation --------------------------------------------------
class OrdersPricingService(orders_pb2_grpc.OrdersPricingServiceServicer):
def GetOrder(self, request, context):
items = _SAMPLE_ORDERS.get(request.order_id, [])
return orders_pb2.GetOrderResponse(
order_id = request.order_id,
items= [
orders_pb2.LineItem(sku=i["sku"], qty=i["qty"], price=i["price"])
for i in items
],
)
def GetPrice(self, request, context):
items = _SAMPLE_ORDERS.get(request.order_id, [])
total = sum(i["qty"] * i["price"] for i in items)
return orders_pb2.GetPriceResponse(order_id=request.order_id, total_price=total)
def serve(host: str = "0.0.0.0", port: int = 50051) -> None:
server = grpc.server(futures.ThreadPoolExecutor(max_workers=4))
orders_pb2_grpc.add_OrdersPricingServiceServicer_to_server(OrdersPricingService(), server)
server.add_insecure_port(f"{host}:{port}")
server.start()
print(f"gRPC OrdersPricingService running on {host}:{port}")
server.wait_for_termination()
if __name__ == "__main__":
serve()
This file sets up and runs a gRPC server that provides two main services related to orders:
**GetOrder**
– Fetches order details like SKUs, quantities, and prices.**GetPrice**
– Calculates and returns the total price of an order.
Similarly, create a gRPC client that calls this gRPC server.
Create a client
Create a file named client.py
and add the following content:
import grpc
import orders_pb2
import orders_pb2_grpc
def main():
# Connect to the gRPC server
channel = grpc.insecure_channel("localhost:50051")
stub = orders_pb2_grpc.OrdersPricingServiceStub(channel)
# Test GetOrder
order_id = "A100"
order_response = stub.GetOrder(orders_pb2.GetOrderRequest(order_id=order_id))
print(f"Order ID: {order_response.order_id}")
for item in order_response.items:
print(f" - {item.sku}: {item.qty} × ${item.price}")
# Test GetPrice
price_response = stub.GetPrice(orders_pb2.GetPriceRequest(order_id=order_id))
print(f"\nTotal Price for {price_response.order_id}: ${price_response.total_price:.2f}")
if __name__ == "__main__":
main()
This client connects to your gRPC server and calls remote procedures defined in your .proto
file:
- GetOrder: Retrieves order details (SKUs, quantity, prices).
- GetPrice: Calculates and fetches the total cost of an order.
Test the implementation
Open two terminal windows or tabs:
Terminal 1: Start the server:
python server.py
This will start the gRPC server and make it listen for requests on localhost:50051
.
Terminal 2: Run the client:
python client.py
This sends test requests to the server and prints the order details and total price to your console.
If you run the code, you should see an output similar to the image below.
Now that your gRPC server has been created, let’s integrate this service into a federated graph, using Grafbase.
Step 1: Generate a subgraph
The first thing you do is to generate a subgraph schema for your gRPC service. This can be done by running the command on your terminal:
protoc --proto_path=. --grafbase_out=. --plugin=protoc-gen-grafbase=<path-to-your-protoc-gen-grafbase-subgraph-binary> orders.proto
Replace <path-to-your-protoc-gen-grafbase-subgraph-binary>
with the actual path to your installed Grafbase subgraph generator. When you run the code, it creates a file called schema.graphql
in your root directory.
The generated schema.graphql
has the following content:
extend schema
@link(url: "https://grafbase.com/extensions/grpc/0.1.0", import: ["@protoServices", "@protoEnums", "@protoMessages", "@grpcMethod"])
@protoServices(
definitions: [
{
name: "orders.OrdersPricingService"
methods: [
{
name: "GetOrder"
inputType: ".orders.GetOrderRequest"
outputType: ".orders.GetOrderResponse"
}
{
name: "GetPrice"
inputType: ".orders.GetPriceRequest"
outputType: ".orders.GetPriceResponse"
}
]
}
]
)
@protoMessages(
definitions: [
{
name: ".orders.GetOrderRequest"
fields: [
{
name: "order_id"
number: 1
repeated: false
type: "string"
}
]
}
{
name: ".orders.LineItem"
fields: [
{
name: "sku"
number: 1
repeated: false
type: "string"
}
{
name: "qty"
number: 2
repeated: false
type: "uint32"
}
{
name: "price"
number: 3
repeated: false
type: "double"
}
]
}
{
name: ".orders.GetOrderResponse"
fields: [
{
name: "order_id"
number: 1
repeated: false
type: "string"
}
{
name: "items"
number: 2
repeated: true
type: ".orders.LineItem"
}
]
}
{
name: ".orders.GetPriceRequest"
fields: [
{
name: "order_id"
number: 1
repeated: false
type: "string"
}
]
}
{
name: ".orders.GetPriceResponse"
fields: [
{
name: "order_id"
number: 1
repeated: false
type: "string"
}
{
name: "total_price"
number: 2
repeated: false
type: "double"
}
]
}
]
)
type Mutation {
"""
Returns the full order (line items, quantities, etc.)
"""
orders_OrdersPricingService_GetOrder(input: orders_GetOrderRequestInput): orders_GetOrderResponse @grpcMethod(service: "orders.OrdersPricingService", method: "GetOrder")
"""
Returns the total price for a given order
"""
orders_OrdersPricingService_GetPrice(input: orders_GetPriceRequestInput): orders_GetPriceResponse @grpcMethod(service: "orders.OrdersPricingService", method: "GetPrice")
}
"64 bit signed integer" scalar I64
"64 bit unsigned integer" scalar U64
input orders_GetOrderRequestInput {
order_id: String
}
input orders_GetPriceRequestInput {
order_id: String
}
type orders_LineItem {
sku: String
qty: Int
price: Float
}
type orders_GetOrderResponse {
order_id: String
items: [orders_LineItem!]
}
type orders_GetPriceResponse {
order_id: String
total_price: Float
}
Here, all gRPC methods are mapped to Mutation
fields, except server streaming methods, which are mapped to Subscription
.
Step 2: Create your configuration file
Create a new file called grafbase.toml
and add the following content:
[extensions.grpc]
version = "0.1"
[[extensions.grpc.config.services]]
name = "orders.OrdersPricingService"
address = "http://localhost:50051"
[subgraphs.routeguide]
schema_path = "./schema.graphql"
Step 3: Start the Grafbase server
Start the gRPC server by running the following command in your terminal:
python server.py
When your gRPC server is up and running, run the following command to start the Grafbase server.
grafbase dev
This will open a dashboard at localhost:5000
. From this dashboard, you can perform mutations and queries. To create a mutation, enter the following command in the Mutation panel of your Grafbase dashboard:
mutation Orders_OrdersPricingService_GetPrice {
orders_OrdersPricingService_GetPrice(input: {order_id: "A100"}) {
order_id
total_price
}
}
The result of this query is shown in the image below:
You can see a similar result to what your gRPC client produced earlier. Similarly, you can run another mutation query to get the orders.
mutation Orders_OrdersPricingService_GetOrder {
orders_OrdersPricingService_GetOrder(input: {order_id: "A100"}) {
items {
price
qty
sku
}
}
}
This produces an output also displayed in the image below:
The steps above demonstrate how easily you can integrate your gRPC services with GraphQL in Grafbase in just three straightforward steps. But why does this matter?
When you connect REST and gRPC services to your federated GraphQL graph using Grafbase, you build an interface that reflects your system's work. You keep the existing service boundaries, but give consumers a single, coherent way to query across them.
Here’s what this enables in practice:
- No need to rewrite existing services
You can integrate APIs written in gRPC or REST without refactoring them or replicating business logic. Services continue to do what they were built for, while Grafbase handles the translation at the edge of the graph.
- Composable internal APIs
Each service remains independent, but its data can now be composed through GraphQL. This reduces the need for point-to-point connections and simplifies how features are assembled across domains.
- Cross-subgraph querying
Clients can follow relationships between services, such as orders and pricing, or users and subscriptions, without needing to know where that data lives or how it’s served internally.
- A consistent querying interface for frontend and internal tools
Whether someone is building a customer dashboard or an admin interface, they work with the same schema and query language. They don’t need to learn how to interact with individual services or understand which ones use gRPC behind the scenes.
- Fine-grained visibility and control
You can expose internal services selectively using @inaccessible
, so internal consumers can query them as part of the graph while keeping them hidden from public APIs or partner clients.
This adds up to a federated graph that mirrors your architecture without flattening it. You preserve the autonomy of your services while improving how teams interact with them.
You’ve seen how to expose gRPC services as GraphQL subgraphs using Grafbase. Starting from your existing .proto files
, you generated a schema, configured your gateway, and made those methods available through a federated graph, without rewriting services or building translation layers by hand.
This approach allows you to integrate what you already have, rather than forcing every team to adopt GraphQL all at once. REST, gRPC, and GraphQL services can all coexist behind a single schema, giving frontend and internal consumers a consistent way to query data.
Federation doesn’t have to start with an entire rewrite. With Grafbase, you can bring existing systems into the graph one at a time, using declarative tooling that reflects how your infrastructure works.
Start integrating your existing services with Grafbase today and experience federation beyond GraphQL.