npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lambdalisue/connectrpc-grpcreflect

v0.5.0

Published

A connect-es module for gRPC reflection

Readme

@lambdalisue/connectrpc-grpcreflect

Unit Test

gRPC Server Reflection Protocol implementation for ConnectRPC in ECMAScript/TypeScript.

Provides both server and client implementations for dynamic service discovery and inspection.

Overview

This library enables:

  • Server: Expose service definitions at runtime for tools like grpcurl and grpc_cli
  • Client: Dynamically discover and inspect gRPC services without .proto files

Requirements

[!IMPORTANT] HTTP/2 Required: The gRPC Reflection protocol specification mandates bidirectional streaming for request consistency in reverse proxy environments. When using Connect protocol, this requires HTTP/2.

  • Use httpVersion: "2" with createConnectTransport
  • Or use createGrpcTransport which always uses HTTP/2
  • ConnectRPC: @connectrpc/connect v2.1+ and @connectrpc/connect-node v2.1+
  • Protobuf: @bufbuild/protobuf v2.7+

Installation

npm install @lambdalisue/connectrpc-grpcreflect

Server Usage

Register reflection services on your ConnectRPC server to enable dynamic service discovery.

Basic Server Setup

import { createConnectRouter } from "@connectrpc/connect";
import { registerServerReflectionFromFileDescriptorSet } from "@lambdalisue/connectrpc-grpcreflect/server";
import { fileDescriptorSet } from "./generated/descriptor_pb.js";

const router = createConnectRouter();
// ... register your services

// Add reflection support
registerServerReflectionFromFileDescriptorSet(router, fileDescriptorSet);

From Binary File

import { registerServerReflectionFromFile } from "@lambdalisue/connectrpc-grpcreflect/server";

// Load FileDescriptorSet from a binary file
registerServerReflectionFromFile(router, "./path/to/descriptor.binpb");

From Uint8Array

import { registerServerReflectionFromUint8Array } from "@lambdalisue/connectrpc-grpcreflect/server";

const binaryData = await fetch("/api/descriptor").then((res) =>
  res.arrayBuffer(),
);
registerServerReflectionFromUint8Array(router, new Uint8Array(binaryData));

Using with Tools

Once registered, tools like grpcurl can discover and interact with your services:

# List services
grpcurl -plaintext localhost:8080 list

# Describe a service
grpcurl -plaintext localhost:8080 describe mypackage.MyService

# Call a method
grpcurl -plaintext -d '{"name": "World"}' localhost:8080 mypackage.MyService/SayHello

Client Usage

Use the reflection client to dynamically discover services and their definitions.

[!IMPORTANT] The gRPC Reflection protocol is designed with bidirectional streaming to ensure request consistency in reverse proxy environments. When using Connect protocol, this requires HTTP/2. Use httpVersion: "2" in your transport configuration, or use createGrpcTransport which always uses HTTP/2.

Basic Client Setup

Option 1: Using gRPC Protocol

import { ServerReflectionClient } from "@lambdalisue/connectrpc-grpcreflect/client";
import { createGrpcTransport } from "@connectrpc/connect-node";

// gRPC transport always uses HTTP/2 (no httpVersion option needed)
const transport = createGrpcTransport({
  baseUrl: "https://api.example.com",
});

// Create client with await using for automatic disposal
{
  await using client = new ServerReflectionClient(transport);

  // List all services
  const services = await client.listServices();
  console.log("Available services:", services);

  // Get service details
  const serviceDesc = await client.getServiceDescriptor("mypackage.MyService");
  console.log(
    "Methods:",
    serviceDesc.methods.map((m) => m.name),
  );
} // client is automatically disposed here

Option 2: Using Connect Protocol

import { createConnectTransport } from "@connectrpc/connect-node";

// Connect protocol requires explicit HTTP/2 for bidirectional streaming
const transport = createConnectTransport({
  baseUrl: "https://api.example.com",
  httpVersion: "2", // Required for bidirectional streaming
});

Exploring Services

import { ServerReflectionClient } from "@lambdalisue/connectrpc-grpcreflect/client";

await using client = new ServerReflectionClient(transport);

// Get all services
const services = await client.listServices();

// Inspect each service
for (const serviceName of services) {
  const service = await client.getServiceDescriptor(serviceName);
  console.log(`\n${service.fullName}:`);

  for (const method of service.methods) {
    const type =
      method.clientStreaming && method.serverStreaming
        ? "bidi stream"
        : method.clientStreaming
          ? "client stream"
          : method.serverStreaming
            ? "server stream"
            : "unary";

    console.log(`  ${method.name} (${type})`);
    console.log(`    → ${method.inputType}`);
    console.log(`    ← ${method.outputType}`);
  }
}

Building FileRegistry

import { ServerReflectionClient } from "@lambdalisue/connectrpc-grpcreflect/client";

await using client = new ServerReflectionClient(transport);

// Build complete registry from all services
const registry = await client.buildFileRegistry();

// Use registry for dynamic message creation
const serviceDesc = registry.getService("mypackage.MyService");
const messageDesc = registry.getMessage("mypackage.MyRequest");

Calling Methods Dynamically

After building a FileRegistry from reflection, use DynamicDispatchClient or ProxyDispatchClient to invoke methods dynamically:

Using DynamicDispatchClient

DynamicDispatchClient allows method invocation by specifying service and method names as strings:

import {
  ServerReflectionClient,
  DynamicDispatchClient,
} from "@lambdalisue/connectrpc-grpcreflect/client";
import { createConnectTransport } from "@connectrpc/connect-node";

const transport = createConnectTransport({
  baseUrl: "https://api.example.com",
  httpVersion: "2",
});

// Step 1: Get schema via reflection
let registry;
{
  await using reflectionClient = new ServerReflectionClient(transport);
  registry = await reflectionClient.buildFileRegistry();
} // reflectionClient is automatically closed

// Step 2: Create dynamic dispatch client
const client = new DynamicDispatchClient(transport, registry);

// Call unary method (supports both PascalCase and lowerCamelCase method names)
const response = await client.call("mypackage.MyService", "Say", {
  sentence: "Hello, world!",
});

// Call server streaming method
for await (const msg of client.serverStream(
  "mypackage.MyService",
  "SayStream",
  { sentence: "Hello" },
)) {
  console.log(msg);
}

// Call client streaming method
async function* requests() {
  yield { sentence: "Hello" };
  yield { sentence: "World" };
}
const result = await client.clientStream(
  "mypackage.MyService",
  "SayClientStream",
  requests(),
);
console.log(result);

// Call bidirectional streaming method
for await (const msg of client.bidiStream(
  "mypackage.MyService",
  "SayBidi",
  requests(),
)) {
  console.log(msg);
}

Using ProxyDispatchClient

ProxyDispatchClient provides a more fluent API with method access via property names:

import {
  ServerReflectionClient,
  ProxyDispatchClient,
} from "@lambdalisue/connectrpc-grpcreflect/client";

// Build registry via reflection
let registry;
{
  await using reflectionClient = new ServerReflectionClient(transport);
  registry = await reflectionClient.buildFileRegistry();
}

// Create proxy-based client for a specific service
const echo = new ProxyDispatchClient(
  transport,
  registry,
  "mypackage.MyService",
);

// Call methods directly by name (lowerCamelCase)
const response = await echo.say({ sentence: "Hello" });

// Server streaming
for await (const msg of echo.sayServerStream({ sentence: "Hello" })) {
  console.log(msg);
}

// Client streaming
async function* requests() {
  yield { sentence: "Hello" };
  yield { sentence: "World" };
}
const result = await echo.sayClientStream(requests());
console.log(result);

// Bidirectional streaming
for await (const msg of echo.sayBidi(requests())) {
  console.log(msg);
}

Manual Method Invocation (Advanced)

For more control, you can manually build the registry and use createClient:

import { ServerReflectionClient } from "@lambdalisue/connectrpc-grpcreflect/client";
import { createClient } from "@connectrpc/connect";
import { create } from "@bufbuild/protobuf";
import { createConnectTransport } from "@connectrpc/connect-node";

const transport = createConnectTransport({
  baseUrl: "https://api.example.com",
  httpVersion: "2",
});

// Discover and build registry
let registry;
{
  await using reflectionClient = new ServerReflectionClient(transport);
  registry = await reflectionClient.buildFileRegistry();
}

// Get service descriptor
const serviceDesc = registry.getService("mypackage.MyService");

// Create dynamic client
const client = createClient(serviceDesc, transport);

// Get input message type
const inputType = registry.getMessage("mypackage.SayRequest");

// Create request dynamically
const request = create(inputType, {
  sentence: "Hello, world!",
});

// Call method
const response = await client.say(request);
console.log(response.sentence);

Using Cached Client

Reduce network calls by caching file and service descriptors. The cached client wraps any reflection client:

import {
  ServerReflectionClient,
  CachedServerReflectionClient,
} from "@lambdalisue/connectrpc-grpcreflect/client";

// Wrap the auto-detecting client with caching
const client = new CachedServerReflectionClient(
  new ServerReflectionClient(transport),
);

// First call - fetches from server
const service1 = await client.getServiceDescriptor("mypackage.MyService");

// Second call - returns from cache (no network call)
const service2 = await client.getServiceDescriptor("mypackage.MyService");

// Check cache statistics
const stats = client.getCacheStats();
console.log(`Hits: ${stats.hits}, Misses: ${stats.misses}`);

// Clear cache when needed
client.clearCache();

You can also wrap version-specific clients:

import {
  v1,
  CachedServerReflectionClient,
} from "@lambdalisue/connectrpc-grpcreflect/client";

// Wrap v1-only client with caching
const client = new CachedServerReflectionClient(
  new v1.ServerReflectionClient(transport),
);

Automatic Version Detection

The ServerReflectionClient (default export) automatically detects which reflection protocol version (v1 or v1alpha) your server supports. This ensures compatibility with both modern servers (v1) and legacy servers (v1alpha) without manual configuration.

How it works:

  1. On first use, the client tries v1 protocol first (recommended by gRPC spec)
  2. If the server returns UNIMPLEMENTED, it automatically falls back to v1alpha
  3. The detected version is cached for subsequent calls

Example:

import { ServerReflectionClient } from "@lambdalisue/connectrpc-grpcreflect/client";

const client = new ServerReflectionClient(transport);

// Automatically detects and uses the correct protocol version
const services = await client.listServices();

// Check which version was detected
console.log(`Using protocol: ${client.detectedVersion}`); // "v1" or "v1alpha"

With Caching:

import {
  ServerReflectionClient,
  CachedServerReflectionClient,
} from "@lambdalisue/connectrpc-grpcreflect/client";

// Combine auto-detection with caching
const client = new CachedServerReflectionClient(
  new ServerReflectionClient(transport),
);

const services = await client.listServices();

Manual Version Selection:

If you need to use a specific protocol version, you can bypass auto-detection:

import { v1, v1alpha } from "@lambdalisue/connectrpc-grpcreflect/client";

// Force v1 protocol
const v1Client = new v1.ServerReflectionClient(transport);

// Force v1alpha protocol
const v1alphaClient = new v1alpha.ServerReflectionClient(transport);

// With caching
import { CachedServerReflectionClient } from "@lambdalisue/connectrpc-grpcreflect/client";

const cachedV1Client = new CachedServerReflectionClient(
  new v1.ServerReflectionClient(transport),
);
const cachedV1alphaClient = new CachedServerReflectionClient(
  new v1alpha.ServerReflectionClient(transport),
);

Formatting Utilities

import {
  formatServiceList,
  formatServiceDescriptor,
  formatMethodDescriptor,
} from "@lambdalisue/connectrpc-grpcreflect/client";

const services = await client.listServices();
console.log(formatServiceList(services));

const service = await client.getServiceDescriptor("mypackage.MyService");
console.log(formatServiceDescriptor(service));

API Reference

Server API

Functions

  • registerServerReflectionFromFileDescriptorSet(router, fileDescriptorSet) - Register from FileDescriptorSet object
  • registerServerReflectionFromUint8Array(router, data) - Register from binary data
  • registerServerReflectionFromFile(router, path) - Register from file path

Client API

Classes

  • ServerReflectionClient - Default client with automatic v1/v1alpha version detection (implements AsyncDisposable)
  • CachedServerReflectionClient - Wraps any IServerReflectionClient to add caching support (implements AsyncDisposable)
  • DynamicDispatchClient - Dynamic method invocation by service/method name
  • ProxyDispatchClient - Proxy-based method invocation via property access
  • v1.ServerReflectionClient - v1-only reflection client (implements AsyncDisposable)
  • v1alpha.ServerReflectionClient - v1alpha-only reflection client (implements AsyncDisposable)

Interfaces

  • IServerReflectionClient - Interface for all reflection clients, used by CachedServerReflectionClient

ServerReflectionClient Methods

Reflection Methods:

  • listServices(options?) - List all available services
  • getFileByFilename(filename, options?) - Get file descriptor by filename
  • getFileContainingSymbol(symbol, options?) - Get file containing a symbol
  • getFileContainingExtension(type, number, options?) - Get file containing an extension
  • getAllExtensionNumbersOfType(type, options?) - Get all extension numbers
  • getServiceDescriptor(serviceName, options?) - Get service metadata
  • getMethodDescriptor(serviceName, methodName, options?) - Get method metadata
  • buildFileRegistry(options?) - Build complete FileRegistry

Lifecycle Methods:

  • close() - Close the client and cancel pending requests
  • [Symbol.asyncDispose]() - Dispose the client (for await using)
  • disposed - Returns whether the client has been disposed

DynamicDispatchClient Methods

  • call(service, method, request, options?) - Call a unary method
  • serverStream(service, method, request, options?) - Call a server streaming method
  • clientStream(service, method, requests, options?) - Call a client streaming method
  • bidiStream(service, method, requests, options?) - Call a bidirectional streaming method

ProxyDispatchClient

Access methods directly via property names (lowerCamelCase or PascalCase):

const echo = new ProxyDispatchClient(transport, registry, "mypackage.MyService");
await echo.say({ sentence: "Hello" }); // Unary
for await (const msg of echo.sayStream({ sentence: "Hello" })) { ... } // Streaming

Cache Methods (CachedServerReflectionClient)

  • clearCache() - Clear all caches
  • clearFileCache() - Clear file caches only
  • clearServiceCache() - Clear service caches only
  • getCacheStats() - Get cache statistics
  • resetCacheStats() - Reset statistics

ServerReflectionClient Properties

  • detectedVersion - Get detected protocol version ("v1" or "v1alpha", undefined before initialization)
  • disposed - Returns whether the client has been disposed

Utilities

  • formatServiceList(services) - Format service list
  • formatServiceDescriptor(service) - Format service descriptor
  • formatMethodDescriptor(method) - Format method descriptor

Protocol Versions

Both v1 and v1alpha protocols are supported. The ServerReflectionClient (default export) automatically detects and uses the appropriate version.

Automatic Detection (Recommended):

import { ServerReflectionClient } from "@lambdalisue/connectrpc-grpcreflect/client";

// Automatically detects v1 or v1alpha
const client = new ServerReflectionClient(transport);

// With caching
import { CachedServerReflectionClient } from "@lambdalisue/connectrpc-grpcreflect/client";

const cachedClient = new CachedServerReflectionClient(
  new ServerReflectionClient(transport),
);

Manual Version Selection:

// Force v1 protocol
import { v1 } from "@lambdalisue/connectrpc-grpcreflect/client";
const client = new v1.ServerReflectionClient(transport);

// Force v1alpha protocol
import { v1alpha } from "@lambdalisue/connectrpc-grpcreflect/client";
const client = new v1alpha.ServerReflectionClient(transport);

Resource Management

Client Disposal

ServerReflectionClient and CachedServerReflectionClient implement the AsyncDisposable interface for proper resource cleanup. This cancels any in-flight requests when the client is disposed.

// Using await using (recommended)
{
  await using client = new ServerReflectionClient(transport);
  const services = await client.listServices();
  // Client is automatically disposed when leaving this block
}

// Or manually close
const client = new ServerReflectionClient(transport);
try {
  const services = await client.listServices();
} finally {
  await client.close();
}

Request Cancellation

All client methods accept an optional CallOptions parameter for individual request cancellation:

const abortController = new AbortController();

// Cancel the request after 5 seconds
setTimeout(() => abortController.abort(), 5000);

try {
  const services = await client.listServices({
    signal: abortController.signal,
  });
} catch (error) {
  if (error.name === "AbortError") {
    console.log("Request was cancelled");
  }
}

Closing HTTP/2 Connections

The ServerReflectionClient receives a Transport instance, but the Transport interface in ConnectRPC does not expose a method to close the underlying HTTP/2 connection. To properly close HTTP/2 connections, you need to manage the session at the transport level using Http2SessionManager:

import { createGrpcTransport } from "@connectrpc/connect-node";
import { Http2SessionManager } from "@connectrpc/connect-node";

// Create a session manager for connection lifecycle control
const sessionManager = new Http2SessionManager("https://api.example.com");

const transport = createGrpcTransport({
  baseUrl: "https://api.example.com",
  sessionManager, // Pass the session manager
});

const client = new ServerReflectionClient(transport);

try {
  const services = await client.listServices();
  console.log(services);
} finally {
  // Close the client (cancels in-flight requests)
  await client.close();

  // Close the HTTP/2 connection
  sessionManager.abort();
}

[!NOTE] The Http2SessionManager.abort() method closes all HTTP/2 sessions managed by that instance. If you're sharing a transport across multiple clients, only call abort() when all clients are done.

Package Structure

@lambdalisue/connectrpc-grpcreflect
├── /server      - Server-side reflection implementation
├── /client      - Client-side reflection implementation
└── /common      - Shared utilities (FileRegistry helpers)

Import Paths

// Server API (backward compatible)
import { registerServerReflectionFromFile } from "@lambdalisue/connectrpc-grpcreflect";

// Explicit server import
import { registerServerReflectionFromFile } from "@lambdalisue/connectrpc-grpcreflect/server";

// Client API
import {
  ServerReflectionClient,
  DynamicDispatchClient,
  ProxyDispatchClient,
} from "@lambdalisue/connectrpc-grpcreflect/client";

// Common utilities
import { getFileByFilename } from "@lambdalisue/connectrpc-grpcreflect/common";

Migration Guide

Migrating from Older Versions

CachedServerReflectionClient Constructor Change

In newer versions, CachedServerReflectionClient no longer accepts a Transport directly. Instead, it wraps any IServerReflectionClient instance.

Before (Old API):

import { CachedServerReflectionClient } from "@lambdalisue/connectrpc-grpcreflect/client";

const client = new CachedServerReflectionClient(transport);

After (New API - Recommended):

import {
  ServerReflectionClient,
  CachedServerReflectionClient,
} from "@lambdalisue/connectrpc-grpcreflect/client";

// Option 1: With automatic v1/v1alpha detection (recommended)
const client = new CachedServerReflectionClient(
  new ServerReflectionClient(transport),
);

// Option 2: With specific version
import { v1 } from "@lambdalisue/connectrpc-grpcreflect/client";

const client = new CachedServerReflectionClient(
  new v1.ServerReflectionClient(transport),
);

Benefits of the new architecture:

  • Separation of Concerns: Caching and version detection are now separate responsibilities
  • Flexibility: You can wrap any reflection client (v1, v1alpha, or custom)
  • Composability: Easier to combine different client behaviors

No changes required for:

  • ServerReflectionClient from default export (now includes auto-fallback)
  • v1.ServerReflectionClient (v1-only)
  • v1alpha.ServerReflectionClient (v1alpha-only)

Development

Prerequisites

  • Node.js v18+
  • pnpm

Setup

# Clone the repository
git clone https://github.com/lambdalisue/ts-connectrpc-grpcreflect.git
cd ts-connectrpc-grpcreflect

# Install dependencies
pnpm install

# Generate protobuf code
pnpm run gen

# Build
pnpm run build

# Run tests
pnpm test

Scripts

  • pnpm run gen - Generate code from protobuf definitions
  • pnpm run build - Build the project
  • pnpm run dev - Build in watch mode
  • pnpm test - Run tests
  • pnpm run lint - Run linting
  • pnpm run typecheck - Run type checking
  • pnpm run fmt - Format code

License

MIT


Credits

This implementation is based on the gRPC Server Reflection Protocol.