Connect-Web: TypeScript library for calling RPC servers from web browsers
buf.build
buf.build
Yes, the guarantee of no breaking changes in an API is wonderful. Everything else sucks.
Not being able to use JSON to test your api? sucks.
Having to convert the changed .proto files to a new library for your language (gem for ruby in my case) and then have your system use that instead of the latest release (because you haven't tested it yet) sucks.
A terrible ruby library written by people who were obviously not ruby devs (it makes FooServiceService classes for example)? sucks.
hoop-jumping to force a breaking change in something you thought was good, but isn't and hasn't been released yet? Sucks
horribly degraded iteration loops on new functionality? sucks
not being able to host it anywhere that doesn't support HTTP2 (Heroku)? sucks
Unless you're working on something at google scale and the performance difference and bandwidth savings are actually noticeable RUN FOR THE HILLS. Protobuffers will bring you nothing but pain.
Eventually, we'd like you to have `connect-rails` (or maybe `connect-rack`, I'm an awful Ruby programmer). It would automatically support the gRPC-Web and Connect protocols, and hopefully standard gRPC too (no idea if rails/rack/etc supports trailers). It should feel like writing any other HTTP code, and it should fit right into any existing web application. We'd probably end up writing a new protobuf runtime for Ruby, so the generated classes feel idiomatic. Now your Ruby code is accessible to curl, bash scripts, and plain fetch from the browser, but it's also accessible to a whole ecosystem of clients in other languages - without anybody writing the boring plumbing code over and over.
Protobuf and gRPC aren't perfect, by any stretch, but they're effective. At least IMO, they're also past critical mass in medium-to-large systems: Cap'n Proto _is_ nicer and better-designed, but it doesn't have the adoption and language support that Protobuf has (imperfect as the support may be). Once your backend is gRPC and protobuf, it's really convenient to have that extend out to the front-end and mobile - at least IMO, it's just not worth the effort to hand-convert to RESTful JSON or babysit a grpc-gateway that needs to be redeployed every time you add a field to a message.
They address your concerns in more detail in the Twirp release announcement (2018) - https://blog.twitch.tv/en/2018/01/16/twirp-a-sweet-new-rpc-f...
However, the semantics that Twirp exposes are different from gRPC. That means that it's impossible (or at least _very_ awkward) to have the same server-side code transparently support Twirp and gRPC clients. The Twirp ecosystem is smaller, and has more variable quality, than the gRPC ecosystem; when you choose Twirp for your server, you're betting that you'll never want a client in a language where Twirp is painfully bad.
Our hope is that the Connect protocol appeals to the same folks who like Twirp, and that the server-side Connect implementations make it easier to interop with the gRPC ecosystem.
The problem is gRPC, not protobuf.
So in general I completely agree. If you are exposing a Web API to consumers -- just keep doing RPC, with HTTP APIs, with normal HTTP verbs and JSON bodies or whatever. Even GraphQL, despite having a lot of pitfalls, and being "hip and fancy", still just works normally with cURL/JSON at the end of the day because it still works on these principles. And that matters a lot for consumers of Web APIs where some of your users might literally be bash scripts.
I agree that it can be a sharp edge for the tool if you don't see it coming... It can be convenient to deserialize a protobuffer into an in-memory representation for working with it. Proto doesn't do this for you because it would be unnecessary overhead if your code can just work with the protobuffers as-is, and protobuffer errs on the side of speed.
In proto2, strings were represented as *string in Go. In proto3, they are simply string, and "" is the default.
Had that happen more than once, and it made for an extremely confusing situation.
Load balancing also a huge pain.
I would like a language in between openapi, proto, and graphql
The graphql schema is the nicest but I don’t always want to deal with everything else. Ideally it would produce beautiful sdks in every language.
If you want code gen from API specs, you can have OAS specs that you create/edit with Stoplight Studio, and then generate types/clients/server-stubs for basically any language with OpenAPI Generator, just as you would with gRPC. And obviously can generate docs, etc. from them too. But unlike with gRPC, you can still use widely available REST middleware, browser dev tools request inspection works better, can curl your services, anyone can call them without special tooling, etc.
I wonder if all the speed gains people are seeing would work just fine with reasonable `Content-Type` and maybe an extra header for intended schema.
IMO, performance is the least of the reasons to use protobufs. And if you're working on simple systems in small teams (or solo), then JSON is a better fit (because it's simpler to understand, debug, and iterate with.) gRPC is definitely not a one-size-fits-all.
Also the reability of JSON in-transit is another factor to consider.
I often see this in arguments using in TypeScript vs plain JS debate. Is this really true in practice? Be good to get some more examples on this.
e.g. instead of writing myJsonPayload["magic string foo"][["magic string bae"], you call methods that actually exist like myMsg.getFoo().getBar(). This is especially helpful if someone who isn't you personally is also writing code that interacts with the data. I suppose that can slow down iteration, but for a decent sized project IMO it's a worthy tradeoff.
Another benefit is having nonhacky representations of numbers that don't exist in standard-compliant JSON, like 0 and infinity. It's also handy to be able to embed comments in your data, which wasn't doable last time I used JSON.
And if you are adding a field specifically for comments, you could do the same in json. For example, a field that has a question mark as the prefix is a comment field (a convention i like).
The advantage of protocol buf is, i think, the version being built in. You can make it backwards compatible to older clients. While the same could be done in json, it's manual and requires work and careful design.
In addition to the binary format, every proto message can also be represented in a human readable form[1]. This is not as efficient and probably not a good idea for data being sent over the wire as-is.. especially if it is only being written and read by computers.
However if you have small amounts of data at rest (like input for a unit test) it can be handy to add some comments explaining anything non-obvious.
On top of that, the proto's schema itself can have comments, and those comments can trickle down into the code generation for a given language so that you can read them when working with your messages. Maybe that's also possible with JSON schemas but idk what their tooling ecosystem is like
[1]: https://developers.google.com/protocol-buffers/docs/text-for...
I really like Protobuf as a pure serializer (not interchange format) to bytes for caching (Redis/Memcached). I had to prove it, again, recently to myself after all the performance improvements made to Json serialization in .NET.
Using some collections of real-world objects, with a serialized size ranging between 50-500K, PB was 2x faster than Json, and had payloads about 1/2 the size. Applying GZIP in the serialization brought the JSON size down significantly, but still not as small as PB. PB only compressed slightly, as expected. However, the time for compression made JSON(gzip) almost 3X slower.
Now, depending on your network speeds and payload size, maybe the time for compression makes sense for remote/distributed caches, but in that case, why wouldn't I use the smaller PB to begin with.
But done right, binary protocols are sometimes worth the marginal savings they provide. We switched over one of our APIs to use CBOR instead of json. It's a search API that we hit a lot and I wanted to cut down on the bytesize of the responses a little. The savings are not that impressive. But I'll take 10% when i can get it.
Otherwise, this was a pretty simple change. We use kotlinx serialization in a multi-platform library. Basically, all we did is configure it to use CBOR instead of json. https://github.com/Kotlin/kotlinx.serialization/blob/master/... Half hour job. Haven't looked at it since; just works. It supports protobuf as well but it looked like more hassle to set up so we went with CBOR instead.
Protobuf might be a more appealing choice if you want to support clients in other languages, or if you're interested in getting a bit more value out of your contract language (e.g., breaking change detection).
For personal projects, I typically have a single Go binary that serves (1) gRPC, (2) grpc-web (using the Improbable library), and (3) gprc-gateway. Along with protobuf-ts for the client, IMO it's quite a pleasant stack. Glad to see timostamm involved in protobuf-es :)
I'm curious: Can you say more about why you ported the code generator from Go to TypeScript (https://github.com/bufbuild/connect-web/commit/8002c2696aad0...)? Was it is easier to generate code this way, or did it just get too unwieldy to package Go binaries in NPM modules?
We ported the code generators to TypeScript because - as you guessed - it was a bit of a pain to package them for NPM. We also felt that it would be more approachable for TypeScript developers to read, which we hoped would contribute to a sense that all of this isn't actually all that complex. We were a bit worried that performance might be perceivably worse, but the difference wasn't significant.
However I do miss being able to inspect the traffic in devtools, and the TS sdk is still not ESM friendly and requires jumping through hoops to get working with vite.
So we ended up bundling it separately through esbuild (along with other google protobuf deps etc.) to a large single file ESM module that other projects can easily consume.
Buf seems to be a solution that handles all of this better. Very excited to try this out.
This is definitely a step up, nicely convenient. But also, it has downsides too. gRPC is incredibly well supported by a huge number of observability tools and service-layers/service-mesh systems. Few have support for gRPC-Web. Which if any have support for Connect-Web?
In general, it's just massively sad that the browser never got around to making HTTP Push useful for protocols. gRPC should have been positioned to do great things, was a very common-sense overlay atop HTTP2 that provided just a little more semantics to say how things ought work. It triangulated nicely on where we were going, at it's inception, back in 2014/2015. But the browser basically never evolved, never gave developers access to do the good stuff we thought we'd start to have access to. I still see brand new specs that shout out to the Low Level Extensibility Manifesto[1], but for some reason, in terms of the most base layer of the web, HTTP, this simple guidance has been roundly ignored & the capabilities were never delivered. So gRPC has been stuck with a pretty ok protocol, & frustratingly discompatible browser.
1. Embrace gRPC and protobuf 2. Extend with Connect, get everyone using this in their APIs and web apps 3. Drift away from established gRPC and/or protobuf standards, build platforms/business around this and trap as many folks as possible
As silly as it may seem, one thing that really sends this signal for me is the nice trademark symbol on the Buf name. their intention for all of this stuff is to build a business.
To retire in Scrooge McDuck-sized vaults of gold,* though, we need to make Protobuf super awesome so that individual developers and companies are willing to use it. It's not super-awesome today, and gRPC is often even worse. To everyone's benefit (I hope), we have the funding and runway to tackle this problem.
From our perspective, your plan looks more like:
(1) Meet everyone where they are, which is mostly Protobuf v3 and gRPC. Adopting a new implementation of a standardized wire protocol in some portion of your system is risky, but not crazy.
(2) Offer a graceful upgrade path to an RPC protocol we think is better, and genuinely solves many pain points with the current status quo.
(3) If we're wildly successful, influence the Protobuf language community whenever a v4 is under discussion. There are certainly warts to fix and nice features to add, but stability and widespread support have a powerful magic of their own.
Good intentions are hard to prove, of course. Hopefully releasing good, well-supported OSS code can be a first step in building some trust.
* Kidding about the piles of gold. I wouldn't say no, but this is the most fun I've had at work in a decade :)
As far as I can tell it's just HTTP, so... all of them?
Streams use features like HTTP2+ Push, & there's all manner of semantic meaning implicit in headers & schemas that regular http wont see that these tools pick up on. I want to be sold that the complex multi-directional multi-stream works fine, are fully observable, but I suspect seriously we're overreducing, not being honest, by discarding the difficulties of tuning observability to specific protocols here & saying general http support will buy us everything.
HTTP is broad, and some things are easier to observe than others. Something like streaming or websockets is by definition more complex than the most basic possible HTTP request/response. Your complaint boils down to "using features makes observability difficult", which is obviously true in some sense, but you're dismissive of what those features unlock.
You must despise graphql.
If you have gRPC services chances are you already have a L7 reverse-proxy like envoy running with grpc/grpc-web support whether doing it yourself, or as part of your cloud provider. So IMO their connect-go library is only useful if you directly expose the server on the internet, and only use the go programming language.
Unfortunately, melony is right: I've only really been able to focus on the C++ implementation of Cap'n Proto. Some of the others are solid (e.g. Go, Rust), but many are sort of half-baked weekend projects of random people, which were later abandoned. As a result it's tough for me to recommend Cap'n Proto in polyglot environments. Maybe someday I'll be able to find the time / resources to build out the ecosystem more fully. Great tooling is really important for any of this to work.
I'm also an investor in Buf FWIW. I think what they're doing is pretty cool.
That said, it’s definitely a step in the right direction, and I personally have never written an API where I even wanted the results to be cached.
On cacheability, do check out https://connect.build/docs/protocol#future-extensions - it’s something we continue to put thought into to make sure we get it right.
I see you’re working on generating idiomatic Typescript from Proto3 - but have you thought about generating Proto3 from Typescript? At Notion we have 1000s of lines of TS types and many TB of JSON stored in those shapes already. Rewriting all that to Protobuf by hand is kinda a non-starter. So we need to go the other direction at least initially.
I started on a Typescript type -> Proto3 compiler that’s very much still in the “toy” stage. I’m also working on compiling Typescript typed to Thrift, JSOMSchema and Python; I plan to experiment with Kotlin as well after the Proto3 target is a little less janky.
My work in progres branch is here, in case this is useful or you want to collaborate!
Proto3 compiler: https://github.com/justjake/ts-simple-type/blob/jake--compil...
Example input and output: https://github.com/justjake/ts-simple-type/blob/jake--compil...
On top of that, the increase in JS shipped to user's due to the size of the generated protobuf objects was unacceptable for anything that cares about keeping their bundle size manageable. Have you done anything to address this issue?
The generated TypeScript code is already pretty minimal because all serialization ops are implemented with reflection instead of generated code (which is only marginally slower than generated code in JS).
But you can also switch to generating JavaScript + TypeScript declaration files, which is truly minimal: JavaScript is an entire dynamic language, so we actually only generated a small snippet of metadata in the .js output, and create a class at run time with a function call. The generated typings (.d.ts) give you type safety, autocompletion in the IDE, and so on.
You can see the output here: https://github.com/bufbuild/protobuf-es/blob/main/packages/p...
https://github.com/bufbuild/connect-web/blob/main/packages/c...
I'm sure someone will chime in on the implementation details, but hopefully others can give it a try with their projects!
https://github.com/whatwg/fetch/issues/607
The rest of the protobuf world has gotten so much better. But the grpc on the web hacks feel bad. The awful option of running http3-over-webtransport-over-http3 is becoming alluring given the lack of browser care for the basics, and that's terrifying (it means the page loading & running it's own page/userland http3 stack). But there doesn't seem to be room for progress & innovation & even basic continuity/ways forward, with HTTP Push getting shoved out the airplane door.
The work here to build yet another protocol that is gRPC like to workaround limitations is heroic. It's unfortunate that protocol development in general is hamstrung by these awkward longstanding limitations in the browser.
At least as I understand it, HTTP Push isn't the problem. The key problem is that browsers don't want to expose HTTP trailers. I understand their reluctance - even putting aside the security concerns, it's a huge semantic change. Many HTTP response headers, like etags, are typically computed by buffering the response, running some compute, then sending headers and the buffered body. All of those would, presumably, now need to also be sendable as trailers. What if the server sends both an etag header and trailer, and they don't match? The rabbit hole goes pretty far. Here's the Chromium issue where the gRPC and Chrome teams debate the issue: https://bugs.chromium.org/p/chromium/issues/detail?id=691599. There's also a series of whatwg issues about exposing trailers in the fetch API: https://github.com/whatwg/fetch/issues/34.
For a protocol that sits above L7, the gRPC HTTP/2 protocol is liberal about diving past the semantics outlined in RFC 9110 and specifying transport-level details. (As an example, trailers-only responses must be sent in a _single_ HTTP/2 headers frame with the EOS bit set.) That degree of specificity unlocks some performance gains, but it makes it near-impossible to speak this protocol using an existing HTTP library - a traditional strength of building on top of L7. For broad adoption in a _lot_ of environments, gRPC-Web (and Connect) are IMO much better protocols.
Protocol Buffers are just a schema language. They're spiritually akin to OpenAPI, Thrift, Avro, and a bunch of other contract-type things. They're _also_ a binary serialization format that's usually faster and more compact than JSON. The intention is to use Protocol Buffers as a language-independent way of describing data bags and network APIs, and then use that schema/contract to generate code in the language you actually work in.
It's not that you _can't_ do that with websockets, webrtc, or just RESTful HTTP/1.1 - it's that you shouldn't _need_ to. The hand-written code to define the same data bag classes/interfaces/structs/etc in multiple languages, then wire them up to the same HTTP boilerplate isn't difficult or interesting, it's just toil. For all the times you're just calling an API, Protobuf lets you get on with the interesting work and skip the boilerplate. Ideally, things work well even if you're writing client code in a language that the server team doesn't have any expertise in.
If you're doing something unusual and need webrtc/websockets/etc, go for it!
So, I can do Eliza with it then...
I'll keep that in mind for the next time that I need to get an Eliza widget working ;)
I'm excited to see more ideas in the second category.
(gRPC/Web is protobufs over HTTP/1.1. Different than strict HTTP/1.1 transcoding, though, which is also possible for gRPC to do.)
I think grpc-gateway is absolutely what 99% of users want to do. You can generate client stubs (it will even output an OpenAPI spec) like you can do for gRPC + protos, but it's all HTTP/1.1 + JSON over the wire, so if you really want to hand craft curl requests, you can. (But with an OpenAPI spec, you'll really like just plugging it into some GUI tool to make these one-off requests. Even curl diehards like myself.) grpc-gateway is also up front about not supporting things browsers can't do, like bidirectional streaming. gRPC/Web always disappointed me on that front; "coming soon" for years.
In the past, I used grpc-gateway (in process, some minor downsides compared to running the gateway as a separate process) + gRPC for my service's internal admin API. It was very few lines of code whenever you wanted to add a new endpoint, and because of the OpenAPI spec, it became available for use in our admin GUI as soon as you pushed the new server code. If someone wanted to add a new endpoint, there was basically no tedium, edit the proto, "go generate ...", implement the generated interface with your actual functionality, push to production. We embedded the generated OpenAPI spec in the doc, so you could just point at app.example.com/OpenAPI.json and clients would auto-update to see your new endpoint.)
I actually converted in-place a hand-coded REST API; all the requests and responses and endpoints stayed the same (thanks to the power of the grpc-gateway annotations), but I deleted all the code in every handler that did the marshaling and unmarshaling. The result was something that was very easy to add to and maintain. I liked it a lot and would do it again without hesitation. (We used Retool as the GUI. Not sure I'd recommend it, but it worked fine. I've seen some HN posts about open source alternatives, those look awesome.)
I have also written several apps that use gRPC/Web. I didn't find the experience bad, but the Javascript bundle size is huge, so I never felt great about it. grpc-gateway should be the first thing you look at, your server code will be simple, and your client code will be small. And hey, if you have non-web clients, plain gRPC is great too.
XML is the language of the gods.
I encounter a lot of people stating your position, that reading JSON is faster than reading XML, for a human. I cannot speak to the subjective experience of anyone but myself of course, but IMO that's a load of bunk. Spent most of my career looking at both XML and JSON, and the only time I ever have a problem is when it's not formatted/indented. We have tools for this, of course.
Yet to encounter a situation where the performance of the XML parser was a limiting factor in the system I am working on. RapidXML and RapidJSON are both insanely fast if you use them as intended. If you are encountering performance issues from parsing XML you almost certainly want to move to a binary format anyway.
I work on low latency distributed systems where performance does matter. We use proto wire format mostly. Proto has its own human readable text format that I prefer because it's more convenient than JSON (no commas, unquoted keys), but I'd pick JSON over XML any day.
I'm very annoyed that all fields are optional. Which means I have to do additional validation on server side and in a language like rust it's double annoying because now you have `T` or `Option<T>` and it's confusing for some types:
- `string` will be rust's `String` except it is an empty string by default - All "custom" types are `Option<T>` even if that makes no sense semantically
So now you have to check if a string is empty, which I guess is technically a string anyway. However, now you have no idea, it is an empty string as a value or lack of value? You end up wrapping `string` into a `StringValue` and now linter is complaining that you shouldn't use those.
Overall parser for protobuf messages got simpler, but now enforcing safety is on server's implementation.
Which is even more crazy then missing parts have default value, when you consider how that behaves in some cases of mismatching schemas (it also can introduce annoying limitations when trying to create idiomatic libraries in some languages, especially such with "type-safety").
And then add in some of the "automatic type conversions it does".
And to top if of some edge cases tend to very often diverge in implementations of different languages, no matter what the original intention was.
And the result is uh, just uh, very very uh.
I understand why google uses them, it fits them. It probably a good idea given google scale problems, company and team structure and "skill level" of each member.
But that doesn't mean it's a good fit for everyone.
> That is projected upon it by people who do not understand it.
This is the the most Googley response possible. If it's so hard to understand, maybe Google should have done a better job of explaining it?
We do think it's the best option for protobuf code generation in TypeScript, though. It comes from quite a few years of experience with proto & TS: the primary author is also the author and maintainer of protobuf-ts, and we had the author of ts-proto review it and give us a thumbs-up before launch.
There _could_ be Bazel rules for it - we certainly write enough other Bazel integrations. Mind filing an issue on https://github.com/bufbuild/protobuf-es?
Does this new connect protocol expect to support all streaming mechanisms client streaming, server streaming and bidirectional?
We had to have several hundred lines of totally unnecessary manual type checking to validate input/output when that should be a major strength of protobufs.
- grpc-web (the standard binary protocol) with the optional TS emit
- grpc-web-text with the optional TS emit
- gprc-ts
- ts-proto
Which to use when, why and the issues with interop between backend/frontend server implementations. Also the streaming story isn't great. Also the need for Envoy as another piece of infra to manage isn't great either.
- no proxies - smaller bundle size - idiomatic TS code in both runtime and generated code - streaming - ... and more
What about servers in other languages? Are there any plans for f.e. Envoy support for going from Connect the frontend to gRPC on the backend? Like gRPC-Web to gRPC?
Right now, you're correct - the easiest way for another server to speak the Connect protocol is to use connect-go. We're planning to support more backend languages soon: Node.js is up next, with the JVM maybe afterwards. We'd also love to contribute (time, code, review, whatever) to any other backend implementations - the protocol is not that complex. File an issue on one of our projects or ping us in our Slack, it'll probably be an easy sell.
Envoy support is complicated. The honest truth is that we haven't looked into it all that much because it feels rude - Envoy's architecture requires first-party plugins to live in the main repo, and it feels impolite to attempt a C++ code dump for our new project. Maybe once Connect takes the world by storm :) Even if we did do that, a Connect-to-gRPC translation layer would only work for binary payloads - converting JSON to binary protobuf requires the schemas, and redeploying proxies every time the schema changes is an operational nightmare.
* Worth noting that gRPC-Web doesn't really have a specification. There's a narrative description of how gRPC-Web differs from standard gRPC, but they specifically mention that the protocol is defined by the reference implementation in Envoy.
Random thoughts: I personally would not mind requiring Protobuf payloads so the proxy portion can stay dumb — keeping proxies up to date sounds no fun. Losing inspectability (not having JSON) is a minus but not a deal breaker. Requiring servers to use a new library is a big lift for people, and hard to sell if what you have works. Having a proxy you can drop in the meantime, while getting client streaming at the same time, seems nice. In any case, having the shiny Typescript friendly libraries that can easily switch between gRPC-Web and Connect is nice, so thanks for that.
For what it’s worth, our servers are C++ and use Arrow Flight which why I’m even interested in client streaming at all (its DoPut method has a streaming argument). My current solution is to add a regular gRPC method that behaves similarly to DoPut but is a unary call for browser clients. Not terrible, but it’d be nice to not have to do that.
I may well take you up on filing an issue/pinging y’all on Slack.
Would love to hear alternatives which do the same thing. I know one possibility is OpenAPI specs generated from Django Rest Framework.
If you really need binary encoding to save some bandwidth, msgpack maybe good enough.
just in case it returns 403
Great work Buf team
And it sucks balls.
Good grief. Pretentious much?
And gRPC adds a bunch of additional complexity and failure modes, sure it's worth considering especially for larger projects. But a lot of projects get away just fine with a surprising small number of endpoints.
No matter who you are, though, it's not fun to hand-write boilerplate data types and fetch calls or quibble over which parameters should go in the path and which should be query params. Even when you're actually sending JSON on the wire and using regular HTTP/1.1 POSTs, Protobuf and connect-web make it easy to skip right to the interesting part of your code.