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

protoc-gen-connect-sdk

v0.1.2

Published

Generates idiomatic namespaced SDKs over ConnectRPC clients

Readme

Connect SDK

Generates idiomatic, namespaced client SDKs from ConnectRPC schemas, for TypeScript and Rust.

A Connect schema names methods so they stand alone. CreateWorkspace says what it creates because nothing around it does. A generated client groups those methods by service, so the name repeats itself:

client.workspaceService.createWorkspace(request);

This generator groups them by resource and drops the repetition:

client.workspaces.create(request);

It does not reimplement Connect. protoc-gen-es and protoc-gen-connect-rust already generate working clients; this emits a facade over them that decides only what the methods are called and how they are grouped. Serialization, streaming, cancellation, and error mapping stay where they were.

The naming rule

For a service [Resource]Service, the namespace is the pluralized resource, and each method drops the resource its namespace already names:

| RPC | Reads as | | --- | --- | | WorkspaceService.ListWorkspaces | workspaces.list | | WorkspaceService.CreateWorkspace | workspaces.create | | WorkspaceService.GetWorkspaceOverview | workspaces.getOverview | | ExportService.ListExportRuns | exports.listRuns | | AccountService.GetAccountStatus | accounts.getStatus |

Two rules keep it from over-reaching.

A method naming no resource keeps every word, because there is nothing to remove: WorkspaceService.SetDraftMode stays workspaces.setDraftMode.

Only a leading occurrence is removed. ExportService.ListEntityExports lists the exports of an entity rather than a collection of entities, so it stays exports.listEntityExports. Removing the resource wherever it appeared would name it exports.listEntity, which reads as listing entities.

Matching is on whole words, not substrings, so AccountService.ListAccountingPeriods derives listAccountingPeriods rather than listingPeriods.

Pluralization comes from pluralize, so the irregular resources English actually has come out right on their own: PersonService gives people, ChildService gives children, and AnalyticsService stays analytics.

Overriding a derived name

The derivation follows a schema's naming, which is occasionally not the naming its callers want. Two annotations correct it:

import "connectsdk/v1/annotations.proto";

service WorkspaceService {
  // `projects`, because that is what the product calls them.
  option (connectsdk.v1.namespace) = "projects";

  rpc ListEntityExports(ListEntityExportsRequest) returns (ListEntityExportsResponse) {
    option (connectsdk.v1.method) = "listForEntity";
  }
}

Set one only where the derived name is wrong; a redundant override is a second place to change a name.

Two services may declare the same namespace, which is how a schema presents one client-facing surface that it implements as several services. Where two methods in a namespace derive the same name, generation fails and names the method to annotate rather than silently keeping one of them.

TypeScript

# buf.gen.yaml
version: v2
plugins:
  - local: protoc-gen-es
    out: src/gen
    opt: [target=ts, import_extension=js]
  - local: protoc-gen-connect-sdk
    out: src/gen
    opt: [target=ts, import_extension=js]

One client per proto package, beside the messages it is built from:

import { createSloperDesktopClient } from "./gen/sloper/desktop/v1/client.js";

const client = createSloperDesktopClient(transport);

const overview = await client.workspaces.getOverview({ workspaceId });
for await (const batch of client.events.subscribe({})) {
  // ...
}

Method signatures are read off the generated service descriptors rather than restated, so request and response types stay exactly what protoc-gen-es produced, and a schema change reaches callers without this generator knowing what changed.

Rust

version: v2
plugins:
  - local: protoc-gen-connect-sdk
    out: src/generated
    strategy: all
    opt:
      - language=rust
      - service_path=crate::generated::service
      - message_path=crate::generated::messages
      - extern_path=.google.protobuf=::buffa_types::google::protobuf

The shape follows async-openai: one client owning the transport, with borrowed group structs reached through accessors.

let client = SloperDesktopClient::new(transport, config);

let overview = client.workspaces().get_overview(request).await?;
let mut events = client.events().subscribe(request).await?;

Unary methods return the response message rather than the transport's UnaryResponse, since headers and trailers are a detail of the hop; a caller who wants them can reach the generated client directly. Streaming methods return the stream itself.

service_path and message_path name where the generated clients and messages live. extern_path follows the spelling the message and service generators already take, and is needed for the same reason: a well-known type such as google.protobuf.Empty lives in buffa_types rather than beside the package's own messages.

Bring your own types

The byot feature adds a *_byot twin of every method, generic over request and response, for sending a type this schema does not describe:

let response = client
    .workspaces()
    .create_byot::<_, MyResponseView>(my_request)
    .await?;

The wire call is identical; only the types at the edges change.

License

MIT or Apache-2.0, at your option.