Skip to main content

Connecting to gRPC APIs

Most custom connectors call HTTP REST APIs, but you can also build connectors for gRPC APIs. This guide shows how to load a gRPC service definition in a custom connector, package it with your build, and call a remote procedure from an action.

The complete example is available in the examples repo. It calls the SayHello method of Postman's public gRPC echo service.

Install the gRPC packages​

Start with a new custom connector project, then install the gRPC client library and the .proto file loader:

Install gRPC dependencies
npm install @grpc/grpc-js @grpc/proto-loader

Add the service definition​

gRPC APIs describe their methods and messages in .proto files. Get the .proto files for the API you're connecting to and place them in src/protoFiles/:

src/protoFiles/helloworld.proto
syntax = "proto3";

service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

// The request message containing the user's name.
message HelloRequest {
string greeting = 1;
}

// The response message containing the greetings
message HelloResponse {
string reply = 1;
}

Package .proto files with your connector​

Your connector loads .proto files from disk when it runs, so they must be in the published bundle alongside your compiled code. Add a copy entry to tsdown.config.mts that copies src/protoFiles/ into dist/protoFiles/:

tsdown.config.mts
import { defineConfig } from "tsdown";

export default defineConfig({
entry: { index: "src/index.ts" },
format: "cjs",
outDir: "dist",
platform: "node",
clean: true,
dts: false,
deps: {
alwaysBundle: () => true,
onlyBundle: false,
},
outExtensions: () => ({ js: ".js" }),
outputOptions: { exports: "named" },
loader: {
".md": "text",
},
copy: [
{ from: "assets/*", to: "dist" },
{ from: "src/protoFiles/*", to: "dist/protoFiles" },
],
});

Without this step, the connector builds successfully but fails at runtime when it can't find the .proto file. After you run npm run build, confirm that dist/protoFiles/ contains your .proto files before you publish.

Create a gRPC client​

Load the service definition and create a client for the service. Resolve the .proto path relative to __dirname so that it points to dist/protoFiles/ in the built connector.

gRPC sends credentials as request metadata rather than HTTP headers. This example builds a Basic authentication value from the connection's username and password and adds it to a Metadata object that each call sends:

src/client.ts
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
import type { Connection } from "@prismatic-io/spectral";
import path from "path";

export const createClient = (connection: Connection, service: string) => {
const PROTO_PATH = path.resolve(__dirname, "protoFiles/helloworld.proto");

const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});

const helloProto = grpc.loadPackageDefinition(packageDefinition) as any;

const client = new helloProto[service](
"grpc.postman-echo.com",
grpc.credentials.createInsecure(),
);

const auth =
"Basic " +
Buffer.from(
`${connection.fields.username}:${connection.fields.password}`,
).toString("base64");

const requestMetadata = new grpc.Metadata();
requestMetadata.set("Authentication", auth);

return { client, requestMetadata };
};

Adjust these details for your API:

  • Server address: Replace grpc.postman-echo.com with your API's host and port. To let each customer connect to their own server, add a host input to your connection and read it from connection.fields.
  • Channel credentials: The example uses grpc.credentials.createInsecure(), which sends traffic unencrypted. Use grpc.credentials.createSsl() for servers that accept TLS connections.
  • Metadata: Use the metadata key and value format your API expects, such as authorization with a bearer token.

Call a method from an action​

Methods on a @grpc/grpc-js client accept a callback. Wrap the call in a Promise so your action can await the response, and close the client when you're done:

src/actions.ts
import { action, input } from "@prismatic-io/spectral";
import { createClient } from "./client";

export const sayHello = action({
display: {
label: "Say Hello",
description: "Say hello to the person of the given name",
},
inputs: {
connection: input({
label: "Connection",
type: "connection",
required: true,
}),
name: input({
label: "Name",
type: "string",
comments: "Your name",
required: true,
}),
},
perform: async (context, { connection, name }) => {
const { client, requestMetadata } = createClient(
connection,
"HelloService",
);
const response = await new Promise((resolve, reject) => {
client.sayHello(
{ greeting: name },
requestMetadata,
(err: any, response: any) => {
if (err) {
reject(err);
}
resolve(response);
},
);
});
client.close();
return { data: response };
},
});

Because the example loads the .proto file dynamically, the client has no generated TypeScript types. If you want type checking for requests and responses, generate types from your .proto files with the proto-loader-gen-types command that @grpc/proto-loader provides.

Test the action​

The example includes a unit test that invokes Say Hello against the echo service:

src/index.test.ts
import { createConnection, invoke } from "@prismatic-io/spectral/dist/testing";
import { sayHello } from "./actions";
import { myConnection } from "./connections";

describe("test my action", () => {
test("verify the return value of my action", async () => {
const { result } = await invoke(sayHello, {
connection: createConnection(myConnection, {
username: "myuser",
password: "mypass",
}),
name: "John Doe",
});
expect(result.data).toEqual({ reply: "hello John Doe" });
});
});

Run it with npm run test. The test makes a network call to grpc.postman-echo.com, so it requires internet access.

Unit tests run against your TypeScript source, so they don't confirm that .proto files are copied into dist/. Check the build output, or run the action in a test integration after you publish.