gRPC example

Try it
Start your own copy of this example:
npm create @setu-ts my-app -- --example grpc
cd my-app && npm install
npm start
The commands further down run inside a clone of the repository.

This application co-hosts a normal HTTP health route with GrpcPlugin and a descriptor-backed Echo service on one application. src/modules/echo/echo.proto is the readable source of the committed descriptor set in echo.descriptor.ts; no code-generation step is required.

pnpm --dir apps/grpc run start

Its smoke check sends a real Connect JSON RPC through the plugin’s interception seam and verifies the decoded Echo response alongside the ordinary HTTP route.

Read the example deep dive for the concept and source walkthrough.

Read, in order

src/modules/echo/echo.client.ts

import type * as protobuf from '@bufbuild/protobuf';
import * as connect from '@connectrpc/connect';
import { createConnectTransport } from '@connectrpc/connect-web';
import { ECHO_SERVICE } from './echo.service.ts';

/** The client view of `example.EchoService` this example calls. */
export interface EchoClient {
  echo(request: { readonly message: string }): Promise<{ readonly response: string }>;
}

/** Creates a Connect client for `example.EchoService` at the given origin. */
export function createEchoClient(baseUrl: string): EchoClient {
  // The plugin's descriptor type is structural; Connect wants its own concrete one.
  const service = ECHO_SERVICE as unknown as protobuf.DescService;
  return connect.createClient(
    service,
    createConnectTransport({ baseUrl }),
  ) as unknown as EchoClient;
}

src/modules/echo/echo.descriptor.ts

/** The compiled FileDescriptorSet for `echo.proto`, so no code generation is needed. */
export const ECHO_DESCRIPTOR_BASE64 =
  'CuYICgplY2hvLnByb3RvEgdleGFtcGxlIicKC0VjaG9SZXF1ZXN0EhgKB21lc3NhZ2UYASABKAlSB21lc3NhZ2UiKgoMRWNob1Jlc3BvbnNlEhoKCHJlc3BvbnNlGAEgASgJUghyZXNwb25zZSINCgtQaW5nUmVxdWVzdCIiCgxQaW5nUmVzcG9uc2USEgoEcG9uZxgBIAEoCFIEcG9uZyInCg1TdHJlYW1SZXF1ZXN0EhYKBnByZWZpeBgBIAEoCVIGcHJlZml4IioKDlN0cmVhbVJlc3BvbnNlEhgKB21lc3NhZ2UYASABKAlSB21lc3NhZ2UydwoLRWNob1NlcnZpY2USMwoERWNobxIULmV4YW1wbGUuRWNob1JlcXVlc3QaFS5leGFtcGxlLkVjaG9SZXNwb25zZRIzCgRQaW5nEhQuZXhhbXBsZS5QaW5nUmVxdWVzdBoVLmV4YW1wbGUuUGluZ1Jlc3BvbnNlMlIKDVN0cmVhbVNlcnZpY2USQQoMU2VydmVyU3RyZWFtEhYuZXhhbXBsZS5TdHJlYW1SZXF1ZXN0GhcuZXhhbXBsZS5TdHJlYW1SZXNwb25zZTABSpwFCgYSBAAAES4KCAoBDBIDAAASCggKAQISAwEAEAoKCgIGABIEAwAGAQoKCgMGAAESAwMIEwoLCgQGAAIAEgMEAi8KDAoFBgACAAESAwQGCgoMCgUGAAIAAhIDBAsWCgwKBQYAAgADEgMEIS0KCwoEBgACARIDBQIvCgwKBQYAAgEBEgMFBgoKDAoFBgACAQISAwULFgoMCgUGAAIBAxIDBSEtCgoKAgYBEgQIAAoBCgoKAwYBARIDCAgVCgsKBAYBAgASAwkCQgoMCgUGAQIAARIDCQYSCgwKBQYBAgACEgMJEyAKDAoFBgECAAYSAwkrMQoMCgUGAQIAAxIDCTJACgkKAgQAEgMMACsKCgoDBAABEgMMCBMKCwoEBAACABIDDBYpCgwKBQQAAgAFEgMMFhwKDAoFBAACAAESAwwdJAoMCgUEAAIAAxIDDCcoCgkKAgQBEgMNAC0KCgoDBAEBEgMNCBQKCwoEBAECABIDDRcrCgwKBQQBAgAFEgMNFx0KDAoFBAECAAESAw0eJgoMCgUEAQIAAxIDDSkqCgkKAgQCEgMOABYKCgoDBAIBEgMOCBMKCQoCBAMSAw8AJwoKCgMEAwESAw8IFAoLCgQEAwIAEgMPFyUKDAoFBAMCAAUSAw8XGwoMCgUEAwIAARIDDxwgCgwKBQQDAgADEgMPIyQKCQoCBAQSAxAALAoKCgMEBAESAxAIFQoLCgQEBAIAEgMQGCoKDAoFBAQCAAUSAxAYHgoMCgUEBAIAARIDEB8lCgwKBQQEAgADEgMQKCkKCQoCBAUSAxEALgoKCgMEBQESAxEIFgoLCgQEBQIAEgMRGSwKDAoFBAUCAAUSAxEZHwoMCgUEBQIAARIDESAnCgwKBQQFAgADEgMRKitiBnByb3RvMw==';

src/modules/echo/echo.proto

// The source of echo.descriptor.ts. The app serves EchoService; StreamService is
// declared in the descriptor but not implemented.
syntax = "proto3";

package example;

service EchoService {
  rpc Echo(EchoRequest) returns (EchoResponse);
  rpc Ping(PingRequest) returns (PingResponse);
}

service StreamService {
  rpc ServerStream(StreamRequest) returns (stream StreamResponse);
}

message EchoRequest { string message = 1; }
message EchoResponse { string response = 1; }
message PingRequest {}
message PingResponse { bool pong = 1; }
message StreamRequest { string prefix = 1; }
message StreamResponse { string message = 1; }

src/modules/echo/echo.service.ts

import * as protobuf from '@bufbuild/protobuf';
import * as wkt from '@bufbuild/protobuf/wkt';
import * as connect from '@connectrpc/connect';
import * as protocol from '@connectrpc/connect/protocol';
import { adaptConnectModule } from '@setu-ts/grpc-plugin';
import type {
  ConnectModuleLike,
  GrpcPluginOptions,
  GrpcServiceDefinition,
} from '@setu-ts/grpc-plugin';
import { ECHO_DESCRIPTOR_BASE64 } from './echo.descriptor.ts';

/** The Connect runtime the plugin serves through, assembled from the installed packages. */
export const connectModule = adaptConnectModule({
  connect,
  // Connect's concrete handler type is narrower than the plugin's structural port,
  // but it implements the same runtime seam.
  protocol: protocol as unknown as ConnectModuleLike['protocol'],
  protobuf,
  wkt,
});

const descriptors = connectModule.reviveDescriptorSet(ECHO_DESCRIPTOR_BASE64);
const echoService = connectModule.getService(descriptors, 'example.EchoService');
if (echoService === undefined) {
  throw new Error('The embedded descriptor set declares no example.EchoService.');
}

/** The `example.EchoService` descriptor, shared by the server and the client. */
export const ECHO_SERVICE: GrpcServiceDefinition = echoService as GrpcServiceDefinition;

type GrpcServiceRegistration = NonNullable<GrpcPluginOptions['services']>[number];

/** The server-side implementation of `example.EchoService`. */
export const echoServiceRegistration: GrpcServiceRegistration = {
  definition: ECHO_SERVICE,
  implementation: {
    echo: (request: { message: string }) => ({ response: `echo: ${request.message}` }),
    ping: () => ({ pong: true }),
  },
};

smoke.ts

import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import type { IKernelApplication } from '@setu-ts/kernel';
import { freePort } from '../smoke-helpers.ts';
import { createEchoClient } from './src/modules/echo/echo.client.ts';
import { createApp } from './setu.config.ts';

describe('grpc example', () => {
  let app: IKernelApplication;
  let baseUrl: string;

  before(async () => {
    const port = await freePort();
    baseUrl = `http://127.0.0.1:${port}`;
    app = createApp();
    await app.start({ port });
  });

  after(async () => {
    await app.stop();
  });

  it('answers a Connect RPC at the root path', async () => {
    const response = await createEchoClient(baseUrl).echo({ message: 'from the example' });

    assert.equal(response.response, 'echo: from the example');
  });

  it('serves an ordinary HTTP route on the same port', async () => {
    const response = await fetch(`${baseUrl}/health`);

    assert.equal(response.status, 200);
    assert.deepEqual(await response.json(), { status: 'ok' });
  });
});