Integrate gRPC services into your GraphQL API declaratively

Tom HouléTom Houlé
Integrate gRPC services into your GraphQL API declaratively

Enterprises adopting GraphQL Federation often face a key challenge: not all of their backend services expose GraphQL APIs. Many critical services still rely on REST or gRPC. That’s where Grafbase Extensions come in and today, we're releasing a powerful new piece of that solution: the gRPC extension.

With this extension, you can expose any gRPC + Protocol Buffers service method as a field in your federated GraphQL graph, without needing to rewrite or proxy those services as GraphQL first.

Let’s walk through a quick example and then explore how it works.

Make sure you have Docker, and the latest version of the Grafbase CLI, protoc-gen-grafbase-subgraph and protoc (optionally buf) installed. You will also need .proto definitions for your gRPC service, and of course the corresponding, running gRPC service. We'll use the service from the example project in this walkthrough.

We will build a small project following the example in the Grafbase repository.

First, generate the subgraph schema for your gRPC services:

protoc --grafbase-subgraph_out=. ./proto/route_guide.proto -I ./proto/

That produces a subgraph schema file called schema.graphql.

Now add the following to your grafbase.toml configuration file:

[extensions.grpc] version = "0.1" [[extensions.grpc.config.services]] name = "routeguide.RouteGuide" address = "http://localhost:10000" [subgraphs.routeguide] schema_path = "./schema.graphql"

Note: by the rules of composition, a federated graph needs at least one root Query field, so you will need to either add a second virtual subgraph in grafbase.toml, with a corresponding schema, or manually add a Query definition with a field in schema.graphql. This is because all gRPC methods are mapped to Mutation fields (except server streaming methods, they are on Subscription) as of version 0.1.2. Protobuf method option definitions are planned to support mapping methods to Query fields in code generation.

Assuming the gRPC service is running, we are ready to start the Grafbase dev server:

grafbase dev

The Explorer UI is accessible on http://localhost:5000. You can run queries against the Mutation fields mapped to your gRPC methods. For example:

mutation { routeguide_RouteGuide_GetFeature(input: { latitude: 409146138 longitude: -746188906 }) { location { latitude longitude } name } }

The most important directive defined by the gRPC extension is @grpcMethod. It allows you to expose a gRPC method on a given service as a field in your subgraph schema. It looks like this:

type Query { user(input: UserFetchInput): User @grpcMethod(service: "user.User", method: "FetchUser") }

The service and method arguments bind the field to a specific method.

Note that while the method is on Query in this example, it can be on any field. Method fields on Subscription let you take advantage of server-streaming methods.

While the @grpcMethod directive is the point of the extension, it is not enough for a fully functioning example.

First, the extension must know the URL of the gRPC service. This is done in gateway configuration. For example:

[[extensions.grpc.config.services]] name = "user.User" address = "{{ env.USER_SERVICE_URL }}"

Then, to convert the GraphQL input into Protocol Buffers, and the Protocol Buffer response into JSON, the extension must know the schema of the input and output messages of the method. This is achieved with the @protoServices, @protoEnums and @protoMessages on the schema definition. They look like this:

extend schema @link( url: "https://grafbase.com/extensions/grpc/0.1.2", import: ["@grpcMethod", "@protoMessages", "@protoServices"] ) @protoMessages(definitions: [ { name: "routeguide.Point" fields: [ { name: "latitude", type: "int32", number: 1 } { name: "longitude", type: "int32", number: 2 } ] }, { name: "routeguide.Feature" fields: [ { name: "name", type: "string", number: 1 } { name: "location", type: "routeguide.Point", number: 2 } ] } ]) @protoServices(definitions: [ { name: "routeguide.RouteGuide" methods: [ { name: "GetFeature", inputType: "routeguide.Point", outputType: "routeguide.Feature" } ] }} ])

Corresponding to the following protobuf SDL:

syntax = "proto3"; package routeguide; message Point { int32 latitude = 1; int32 longitude = 2; } message Feature { string name = 1; Point location = 2; } service RouteGuide { rpc GetFeature(Point) returns (Feature) {} }

As you can imagine, these definitions can get very large for services with many methods and messages. In order to make authoring them more convenient, we also released a protoc plugin to generate virtual subgraphs defining all the services, messages, enums and fields, with the corresponding GraphQL input and output types.

The protoc-gen-grafbase-subgraph Protocol Buffer compiler plugin can generate a virtual subgraph (a subgraph that does not correspond to a live GraphQL API) schema from your .proto files that contain service definitions. It integrates seamlessly with your protoc or buf code generation workflow. A sample buf.gen.yaml configuration can look like this:

version: v2 managed: enabled: true plugins: - local: protoc-gen-grafbase-subgraph out: . inputs: - directory: proto

Or with protoc:

protoc --grafbase-subgraph_out=. proto/routeguide.proto

You can download pre-compiled binaries from GitHub releases.

With GraphQL federation, these fields do not have to be public. They can be exposed in the federated graph for other subgraphs to use. For example, if you define the following virtual subgraph for a gRPC service:

type Query { restaurant_Menu_GetMenu(input: GetMenuInput): Menu @grpcMethod(service: "restaurant.Menu", method: "GetMenu") @inaccessible }

The @inaccessible directive ensures that the method is not directly exposed to clients. But another resolver, potentially in another subgraph, can access it:

type Query { getMenuWithCalories: MenuWithCalories @requires(fields: "restaurent_Menu_GetMenu(input: {}) { menuItem { name ingredients } }") }

The Grafbase Gateway will resolve the gRPC resolver with the gRPC extension, and the resolver for getMenuWithCalories gets the data it @required declaratively.

The gRPC extension provides support for different streaming patterns. gRPC offers both client streaming (where the client sends multiple messages) and server streaming (where the server responds with multiple messages). Bidirectional streaming combines both approaches.

Server streaming maps naturally to GraphQL subscriptions, allowing you to expose server-streaming gRPC methods as subscription fields in your federated graph. For example:

type Subscription { listenForUpdates(input: ListenInput): Update @grpcMethod(service: "notification.Service", method: "Listen") }

Client streaming presents a unique challenge since GraphQL doesn't have a native equivalent. In the current implementation, you can use client streaming methods, but with a single input message. For more complex client streaming scenarios, we're exploring options like mutation + subscription pairs with stream identifiers, or potentially leveraging multipart requests.

GraphQL federation does not have to mean that you are federating only GraphQL APIs. The federated graph is a GraphQL API, but with Grafbase extensions, you have the flexibility to federate any service or data source you want to take part in that federated API. The integration point is the GraphQL schema of these services. You can define what types from other subgraphs are provided, required or extended with federation directives, and create deeper integrations than just a juxtaposition of separate APIs. The magic of federation is subgraphs augmenting each other without having to communicate with each other directly.

With extensions, you have the flexibility to build your own or tweak existing extensions. The gRPC extension is, like all the extensions we have built, open source. This is a first version, there are many features and improvements we are thinking about, and any feedback is helpful, so please don't hesitate to reach out or join us on Discord.