I've used tRPC and Next.js for a couple of personal projects and it's been a great experience. Hard to beat on iteration speed, especially when used with a pre-configured template like Create T3 App: https://create.t3.gg/.
I think patterns and best practices are still yet to be figured out. I believe you can create caller in a server component and it should work (https://trpc.io/docs/server/server-side-calls#create-caller), but the pages router appears to be a battle-tested solution.
Thank you I only briefly read about them before and didn't consider them properly.
Will try out calling the database directly there, but I kinda liked how with tRPC I can add validation/auth checks etc/ will have to see how I develop my own strategy for that w server actions.
I am up voting this because its a good idea and you have really nice web site.
I wrapped up something like this myself a few weeks ago, so I am in a position to offer some suggestions other people may not think about.
The most common mistake I see with RPC, especially WebSockets, using Node.js is that they must be based off of HTTP. My attempt is based upon WebSockets, RFC 6455, where RPC is a generic term for socket based communication streams not specified against any single frame definition scheme. Since these technologies are raw sockets that include their own conventions for handshakes, optional frame header definitions, and possible security conventions there is no need for HTTP. In the OSI model HTTP is layer 7 where TCP sockets are layer 4. In Node that means just using the net/tls libraries opposed to the http based descendant libraries. Since, in Node, the http library descends from from the net library and https descends from tls which descends from net http always imposes overhead that TCP based sockets do not require.
There are three benefits for executing a socket server over HTTP:
1) You are only using port 443
2) Simple and familiar implementation from Node
3) HTTP 1.1 is session-less, which allows anonymous untrusted connections. That is how the web works, but its less ideal for a security focused implementation.
The reasons to not do this include CPU cost. Running sockets over HTTP increases processing overhead which reduces the number of concurrent streams you can offer and substantially slows down processing of incoming frames. In order to reduce your execution to a single port without sacrificing performance you could run HTTP over your socket implementation which allows you to customize your approach to security, but that would also require writing your own HTTP libraries.
My issue with websockets is, they are still being blocked in corporate proxies. I particualarly see this in banks or research-places, where sockets are blocked to prevent data leaking out of the network.
The reason they might be blocked by big banks is due to deep packet inspection. How that works is that the bank intercepts certificates on TLS socket establishment for normal web traffic so that the bank proxy becomes a formal "main in the middle". They do this to provide deep packet inspection on all encrypted traffic that goes out and comes in. That level of packet inspection is more challenging with something like RPC/WebSockets because its a binary stream, where HTTP just uses plain text for its header data.
I can remember using Reddit, when I still used Reddit, when starting at the bank and I remember Reddit making heavy use of WebSockets that worked just fine from within the bank even though I was behind the bank's proxy. This was more than 6 years ago, but I believe the WebSocket traffic at that mega bank was just relayed through proxy just like the HTTP traffic and I also want to say Reddit served the WebSocket traffic different from the HTTP traffic, but I cannot remember for sure.
I don't think that is correct. HTTP is managed by applications, but TCP packets are managed in the OS kernel. That distinction determines your options of approach and how things interact when parsing transport negotiation mechanisms.
Does end-to-end have to be agnostic to the backend to you? Do you feel the same way about end-to-end encryption? Would you say "end-to-end encryption as long as the backend is rust/go/c"? Why not, what's the difference? Most projects exist in a ecosystem, and in this case using the same language on both ends makes total sense don't you think? They pull from the same repository, use the same language servers, and run mostly the same code.
Until very recently single-language frontend + backend stacks were not the norm.
As tRPC is in competition with solutions like GraphQL or REST APIs where the backend can be implemented in a big number of languages, I thought that limitation was worth pointing out.
We’ve use an API style similar to tRPC at Notion, although our API predated tRPC by 4 years or so.
You can build this kind of thing yourself easily using Typescript’s mapped types, by building an object type where the keys are your API names, and the values are { request, response } types. Structure your “server” bits to define each API handler as a function taking APIs[“addUser”][“request”] and returning Promise<APIs[“addUser”][“response”]>. Then build a client that exposes each API as an async function with those same args and return type.
We use this strategy for our HTTPS internal API (transport over POST w/ JSON bodies), real-time APIs (transport over Websockets), Electron<>Webview APIs (transport over electron IPC), and Native (iOS, Android)<>Webview APIs (transport over OS webview ipc).
For native APIs, the “server” side of things is Swift or Kotlin, and in those cases we rewrite the request and response types from Typescript by hand. I’m sure at some point we’ll switch to a binary based format with its own IDL, but for a single cross-language API that grows slowly the developer experience overhead of Protobuf or similar hasn’t seemed worth it.
Is there some trick to doing validation of request data using this process? That's a valuable part of using something like tRPC, JSON Schema + type generation, zod, etc.
I’m not happy with the design decision in that codebase to try to “simplify” Typescript types before compiling, and probably won’t continue that implementation, but we have a few internal code generators that consume TS types and output test data builders and model clases we use in production.
I want to open source some of those bits but haven’t found the time.
Deepkit looks really cool, but it’s so complex on the inside and leverages a forked/patched Typescript and requires full typecheck before emit.
What happens if the Deepkit guy retires? What if I want to run my code without waiting for 11 minutes of typechecking? What if there’s a bug somewhere in there?
There’s way too much risk for me to consider Deepkit for production.
In our current project with a TS frontend and Python backend, we use an OpenAPI schema as the source of truth and openapi-typescript-codegen [0] to interface with it on the client side. While not perfect, it provides a very nice interface to our API with request/response typings.
I also wrote a 10-line mock API wrapper that you can call as mockApi<SomeService["operationName"]>((request) => response), and it will type-check that your mock function implements the API correctly and return a function that looks exactly like the real API function.
Can second this approach. At a past job we did the same, except to connect the frontend to a Go backend.
I really like that the openAPI approach is language agnostic, and makes it relatively simple to support SDKs for many other languages if needed. For any company where the API itself is a product, OpenAPI is great.
We use OpenAPI internally for communication between systems. It eliminates an entire category of bugs, and we can offload testing of schema conformance entirely to third-party tools. We've never had a bug due to a mistake in calling an internal API that I know of. And we get both internal documentation and a web UI for free via SwaggerUI and Redoc.
Good zero-config OpenAPI support is one of the best features of the FastAPI framework in Python. The "fast" part refers to the speed of basic product up and running.
Another great library to generate TS types from OpenAPI is https://github.com/drwpow/openapi-typescript . It provides the types as single objects you access via indexing, which is pretty nice. There's a partner library to generate a typed fetch client.
I'm basically wondering what would help us produce (automatically?) a Python HTTP client based an OpenAPI spec, and achieve a development experience similar to using drwpow/openapi-typescript.
drwpow/openapi-typescript will generate the whole schema, along with paths, request and response models, as well as path and query parameters. In the IDE, all client methods will be type-safe and autocompleted.
koxudaxi/datamodel-code-generator seems to generate the response models, but not much else. It seems we would have to manually write wrapper methods for each endpoint, manually specify the parameters and request models, and use a type hint for the return value of each method with the generated response model.
Regrettably, I wasn't able to readily find a matching openapi example, likely cause they really seem to believe their kid-gloves format is the future (or lock in, depending on how bitter one is feeling)
---
confusingly, their repo has a "YCombinator 2023" badge on it that just links to the badge itself. Some Algolia for Launch HN didn't cough up anything, but there was a Show HN I found: https://news.ycombinator.com/item?id=34346428
This is good feedback that we need to provide more examples starting with OpenAPI instead of with the Fern Definition. For some context, we convert the OpenAPI spec into a Fern definition and then pass that into the code generators.
If you want to see some real world examples, check out these links:
That's seems to be completely missing validation. Typescript types are at their worst when they are lies, and the actual shape of the data is something completely different.
To me, if the server has updated it's schema and the client has old code, the server responds with an error, the user sees "something went wrong".
And if the validation fails on the client side, the user sees "something went wrong"
And if the client isn't doing validation of what's returned, and an error gets thrown because it tried to access a now missing field, the user sees ... "something went wrong"
Intentionally maintaining multiple versions of an API, or making sure your API changes are backwards compatible (like adding new fields and marking old ones as deprecated), those are solutions but definitely require organizational effort.
With an explicit error you get from validation (like Zod, as used in tRPC), you could notice that your client is out of date and either refresh automatically or prompt the user to refresh[1].
The benefit from validation is that the error is easily recognizable & can be handled nicely, even in just one place. Without validation, you get things that Typescript claims are impossible, for example exceptions on code paths that look like they cannot throw, and you can't easily tell why any of that happened -- that's a recipe for miserable debugging. Of course, with less busy, less critical, sites you might not notice or care.
[1]: Refresh could cause loss of user input (e.g. text just entered), depending on the app that might matter and need browser-side storage or something along the lines of the "local first" manifesto. Easy kludge for simple apps with smart users is to make the user copy the text, click refresh, paste.
Isn't that only for one case of errors though (server not matching expected response)?
One solution I thought was clever, was a friend's single-page app would hard-reload on navigation, if there was an update to the assets. Not sure how feasible that would be for schema errors though.
Between that and catching 400 Bad Requests coming back from the server, you're pretty much catching any communication errors resulting from version skew.
SvelteKit can poll to check server version and refresh, both on timer and if assets are 404. But as you implied, that doesn't help with API evolution and e.g. users interacting with a form (apart from increasing the likelihood the page will refresh soonish due to navigation).
Similarly, we built a typed-client for inter-service communication using lambdas at Robocorp.
The requests and responses are inferred from the interface (published following semver) defined in Zod (including the errors that are following HTTP-like conventions: 401, 409…).
It also includes “hooks” for pre and post processing on the server side (effectively middlewares)
This is the way. tRPC adds unnecessary complexity over simply inferring types. My theory is that no well maintained and promoted library adopting this approach has emerged, and that’s why you don’t see it discussed very often.
Yeah, I’m reading their sample code and wondering if this is just type-imbuing wrappers on top of XHR calls. It even asks you to provide the generic argument in the invocation (this sucks for trying to keep your dependency tree in order).
I have used tRPC for two ~50k loc web applications and love it. The DX is incredible. But I feel like tRPC had its hype time some time ago, so RSC’s hype quickly caught up. These days everyone speaks about whether to use or not to use RSC while there is such a great stable solution like tRPC out there. I am not against RSC, but there is way too much discussion about it. tRPC is a super pragmatic way to create applications with these days!
Edit: RSC = React Server Components - which come with their own read/write data philosophy.
it is. But RSC make many library authors and maintainers question whether their library is still needed in a RSC world and if so how they can support it. Same story with tRPC https://github.com/trpc/trpc/issues/3297
Believe it or not, not everyone is even using a flavour of React.
One nice side effect of using tRPC is that you can pack up and move to a different framework on the front-end if desired, and a lot of the tRPC work can be used directly
It’s not the whole point. Making async calls, handling errors as usual and subscribing on channels has better ergonomics (DX) than bare requests, http streams or websockets.
Telefunc is another alternative without the boilerplate where on the frontend you can just import and execute the backend functions remotely.
https://telefunc.com/
Does anyone know if it's possible to build the ergonomics and DX of tRPC on top of gRPC-Web? tRPC is TypeScript/Node.js-based while gRPC-Web backends can be built in most languages.
DX for front or back end? The beauty of tRPC is that the types are derived/inferred from the backend runtime code (like, as you type). It would be nigh impossible to do that with grpc(-web) using proto files as the source of truth.
It's possible there's a project out there which could automatically produce proto files from something like zod, json-schema, etc. which could be directly interpreted by TS to provide similar (as you type) DX while still allowing some other language backend to consume the derived proto files (though the DX there would be less than ideal).
If you're just looking for similar TS clients/interfaces for grpc-web then I'd recommend https://github.com/timostamm/protobuf-ts which operates on plain JS objects (no new MyMessage().serialize(), instead the code generator mostly produces TS interfaces for you to work against: const myMessage: MyMessage = pojoConformingToInterface; const binary = MyMessage.toBinary(myMessage);)
With tRPC, changing an attribute of a class in your backend immediately tells you what broke in the frontend in your IDE. No need to recompile or regenerate anything.
Also, when developing your frontend, you'll have immediate autocomplete. Again without code generation or compilation.
That alone should save you dev time and prevent a ton of frontend bugs.
It slows down the iteration cycle - when you make a change to your API definitions you'd need to push the change to either a schema repository or into production, then regenerate the API - at which point, if you've accidentally introduced a breaking change, your frontend is already broken - you can work around this with CI that detects breaking changes, but it requires a fair amount of work. Having them in a monorepo means that breaking API changes can fail CI and never make it into production.
I'm a big fan of tRPC. It's amazing how it pushed TypeScript only stacks to the limit in terms of DX. Additionally, it made the GraphQL community aware of the limitations and tradeoffs of the Query language. At the same time, I think tRPC went through a really fast hype cycle and it doesn't look like we're seeing a massive move away from REST and GraphQL to RPC. That said, we see a lot of interest in RPC these days as we've adopted some ideas from tRPC and the old NextJS. In our BFF framework (https://wundergraph.com/) we've combined file based routing with RPC. In addition to tRPC, we're automatically generating a JSON Schema for each operation and an OpenAPI spec for the whole set of operations. People quite like this approach because you can easily share a set of RPC endpoints as an OpenAPI spec or postman collection. In addition, there are no discussions around HTTP verbs and such, there's only really queries, mutations and subscriptions. I'm curious what other people's experiences are with GraphQL, REST and RPC style APIs? What are you using these days and how many people/teams are involved/using your apis?
> it made the GraphQL community aware of the limitations and tradeoffs of the Query language
Could you expand on that? Our graphql types are generated from our federated schema whenever it changes, and response types for queries / mutations are generated whenever you save a file.
With tRPC and similar frameworks, you infer client types from server definitions without including actual server code into the client. As much as I like GraphQL, one thing that IDEs really suck at is recognizing when generated code changes. E.g. with Jetbrains IDEs it sometimes takes forever until a change to the generated code is actually picked up by intellisense. VSCode is a little bit better regarding this. When you infer types in tRPC style, this whole problem is gone. You can even jump between the client usage and the server implementation. That said, this is not without a cost. Large Typescript codebases can slow down the typescript autocompletion and this approach only works best when both client and server are written in typescript and ideally in the same codebase.
The thing that always strikes me is that verbs and paths are pretty tiny details (and also easy to abstract away using endpoint consumer generation libs) - then what else do you really get compared to your normal average web api? You still need to perform everything in each call to the backend anyway.
> When it runs on the client, variables from the surrounding scope that you use in the server-side function are captured, serialized, and sent to the server. Since anyone can send any request to the server, you have to validate everything that comes from the surrounding scope.
This is mildly horrifying to consider without good tooling support.
On the next iteration, we're planning on providing a `createServerQuery` function which will _not_ capture the closure and return a function that can take an arbitrary number of arguments to be serialized. `createServerQuery` will have a required `validator` option that will also run on the server to validate those arguments.
I was really hyped about tRPC until I started using Remix I still think tRPC is great and for most projects you don't need more (specifically GraphQL).
OpenAPI, JSON-RPC, and JSONSchema have existed for a pretty long time now. The industry has more or less standardized on JSON for data interchange and parsers/generators for JSON abound in every programming language, so it makes sense to do all this stuff with JSON.
I've never seen IDL before but I looked up some examples and it does seem useful for RPC calls. But I'm not exactly eager to switch, it looks like it's meant for a much more powerful form of RPC than I'm willing to touch, and no tooling I know of supports it.
The JS ecosystem of course is still affected by hipster disease (everyone seems to think their own wacky idea is groundbreaking). But the existence of a framework like this, built on well-established JSON-based standards shouldn't be surprising.
Yes, but those systems have been obscure and uncommon for longer than a lot of today's programmers were even in the field. In the intervening time we all standardized on JSON and rediscovered or reinvented similar concepts all using JSON. Fine. It's great to understand and learn from prior inventions, but it's not like we're all going to switch back to IDL.
Consider that JSON being ubiquitous immediately makes it easier to adopt compared to a custom description language. If people actively used IDL today, there would probably be a lot of demand for a JSON variant or subset.
I'd make similar arguments about using JSON vs S-expressions for data interchange, but JSON works well with both Javascript and HTTP and everyone standardized on those, and maps cleanly to basic data structures in just about every modern programming language.
These JSON-based tools are actually very much like Lisp in that both the interface specification and the data are expressed in exactly the same format/language. This is not true of a lot of these older standards, and seems to have been the failed promise of XML.
IDL does look like it maps nicely to typed function calls in most languages, but it lacks the advantage of being expressed in a standard format/language that is already well-supported for other tasks, and seemingly doesn't impose any requirements on how the data itself is transmitted.
For an example of why language/format matters, consider the tool c2ffi (https://github.com/rpav/c2ffi). It generates a JSON description of a C header file. The header file itself is a pain to parse and extract information from. But once you have a tool to do it and put that information in a standard format, you can now build an FFI wrapper in just about any other language in at least semi-automated fashion. It makes the easy parts easier, compared to other systems like SWIG and GObject where the interface format is totally custom and you're mostly reliant on a single implementation to make it all work.
If anything, let's be grateful that the good ideas of the past are being rediscovered and reinvented in a way that might grant them more longevity and broad usefulness than they had in their first life. Did you use IDL? What was your experience like? How would you compare it to something like gRPC?
That’s an interesting statement, because from my vantage point “longevity” seems to be way, way down the priority list for pretty much any technology in JavaScript world. (The major exception being pure JSON.) It feels like if you open any JS codebase from more than 18 months ago, half the libraries will be deprecated or abandoned (not just the version, the entire library). Major patterns and frameworks reinvent themselves incompatibly on a biannual basis.
The purpose of an RPC IDL (protobuf being a “modern” example) is that you define the interface and encoding in a way that will still be functional when your “standard language” is long forgotten or unrecognizably different.
I'm not too surprised. Most people for whom TypeScript is an option won't have been exposed to generations of RPCs that fail to deliver simplicity. So it isn't going to be on their radar.
I've been using gRPC and REST'ish+JSON APIs for years now and what I find puzzling is that REST+JSON tends to mean a lot more work, less pleasing code than gRPC, and yet people prefer it. Not because it leads to simpler, better, faster less error prone code (it doesn't. Quite the opposite), but, I think, because people feel they can understand it.
The people over at https://buf.build have been doing a great job trying to tame gRPC btw. The protoc toolchain from Google has been uniquely horrible to work with, but the 'buf' tool from said company has been a real life-saver. Not to mention their Buf Schema Registry, which admittedly I have only used on a few projects so far, but should migrate more projects to.
Though in general, I feel that RPC mechanisms that are too closely tied to a given language are a waste of time. But that's me. tRPC isn't something I'd get into even if I was doing server side TS. It just doesn't seem like a good long term choice.
People sticking with REST+Json usually don't want to have a compilation step for the API exchanges layer. That means more validation and worse tooling, but also better legacy and potentially future compatibility, and way easier debugging at any stage.
I feel this is the same debate between scripting languages and compiled languages, both provide different trade-offs and I don't think we'll see one completely disappear at any point in time.
> People sticking with REST+Json usually don't want to have
> a compilation step for the API exchanges layer.
Hmm. I'm not sure what you are saying.
The way people tend to try to consume REST APIs is by generating client (and/or server code) from a spec. For instance OpenAPI 2.0. At least on Go the tooling for that leaves something to be desired, and the generated code isn't exactly beautiful. So you either depend on a library that someone generates from the spec, and then shares, or you generate it yourself.
(We do this for a bunch of languages for our APIs, and the clients are ... not uniformly beautiful :-))
If we're talking about writing the REST client by hand...well...I'm not sure why one would want to do that. (Nor am I sure that's what you meant, so I'm not accusing you of that).
My current workflow (in Go) for consuming gRPC interfaces is to use the buf.build package proxy. Which means I just add an import, run go mod tidy, and I'm good. I don't even need the tooling installed. As I think it should be.
I suppose this could be done for OpenAPI 2.0 too. You stuff OpenAPI 2.0 specs in and you get code for a bunch of languages out. (Someone must surely have built this already?)
"I feel that RPC mechanisms that are too closely tied to a given language are a waste of time" - THIS RIGHT HERE! - The whole point is to allow people using different languages to collaborate - your analysts using python/R should be able to talk to your Java/C++/Rust/C#/Go folks and web-frontends (and server sometimes) Typescript/Javascript/etc.
One of the worst example (in the past) was the python pickle. People have overused it, and it's not even compatible between some python releases. There are many other examples - where something works really neat, but only for that language, or even that language major or even just minor release.
There is protobuf, cap'n'proto, flat buffers, fidl, thrift, etc. - many better choices than just one that works only for your language.
I agree that sticking to just a library may be not a good choice. Some standard would be better. But few last times I discussed it, some folks just slapped RPC stigma on everything, even if it was just your regular REST-y-ish calls underneath. That’s absurd. POST json receive json back is okay. Wrap it into an `await server.<method>(<args>)` call and now it’s an RPC mudball. The current top commenter is removing tRPC for tight coupling. I wonder what prevents them from tight coupling over http, or over just function calls when on the same “side”. It’s a mindset that they are removing, not a specific technology.
Careful what you wish for. Standards that aren't quite good enough can be worse than not having standards. Because then at least then people will keep looking for something that might be better.
Is there some way to reduce message size in WebSocket scenarios? It looks like it uses JSON-RPC under the hood with strings for message names etc. I'm looking for ways to improve the throughput of an embedded WS server. One idea I had was for clients to negotiate a number-based encoding for these parts to reduce all unnecessary traffic. Is there any way to implement this with tRPC?
It might not beat a bespoke hand-crafted protocol that minimizes message size, but your use of the word "negotiate" makes me think that that's not what you're taking about...
The embedded device the server is running on has a very weak CPU. Since I'm trying to optimize throughput this would have quite an impact. It's fine if the first few messages are slow, the speed just has to pick up after initialization is done, which is why negotiation is okay for me. So e.g. the client requesting IDs for every method on first connection would be fine, and would keep complexity down.
I imagine you could write a custom tRPC link to send the data over something like MQTT which may be better for an embedded client - there's prior art in the likes of electron-trpc, showing how tRPC could be adapted to non-HTTP transports. Not sure how that interacts with the JSON-RPC bits of the subscription protocol though, if there's no way around it then plain MQTT may be better suited to your use case.
Thank you for the suggestions! I am specifically looking to optimize frontend-to-server communication, so WebSocket is a must. Is there some way I could inject a custom transform function between TRPC and the WebSocket? I could copy the existing WebSocket link and add my transform in there, but it would of course be easier if I could just do my own wire handling with the existing code.
Essentially I'd like to inject a custom respond[0] encoder and a custom parseMessage[1] decoder, and same for the client.
Update: I was able to get this working by wrapping WebSocket and WebSocketServer! The API surface for the wrapper seems to be pretty minimal. If I do use TRPC in the rewrite of the embedded server, I'll implement a custom message scheme as an alternative and benchmark them.
I will add wrappers for WebSocket/WebSocketServer in the future to allow using this without the library having to support it, like in the PoC with trpc.
I find it really sad that these efforts always stop at working in language X, but no formal specification exists that would make it possible to create an interoperable version in a different language. I love TypeScript, but for various reasons one may need a different backend language on the server side. Yes, OpenAPI is a thing, but I have yet to see an OpenAPI spec + code generator that works out of the box and doesn't need a whole lot of fiddling and workarounds. I also understand that it's hard to create something truly interoperable, my previous job was building a usecase-specific typing system, but adding yet more single-language ?RPC implementations isn't really helping. Obligatory XKCD reference: https://xkcd.com/927/
I think part of why tRPC shines is because it's tightly coupled to TypeScript (and especially Zod, its schema validation library of choice - many of its features map 1:1 onto TypeScript concepts that don't exist in many other languages), which means it can avoid many of the issues that OpenAPI generators have. I'd also like to see a good TS-first OpenAPI client - Fern [0] is probably the closest I've seen.
In general in my experience, when you take away the constraint of inter-language interop, you get much smoother DX because you can take advantage of language features. A good example would be the lack of native date/time types in JSON - valuable for interop because different languages handle dates differently, but painful when you're going to/from the same language. Web applications are a special case, because the client-side is effectively constrained to either JavaScript or WebAssembly (except you'd still need at least some JS in the latter case), so it follows that you'll get the best DX if you have JS or TS on both sides of the stack, especially if you can set up your project in a way that lets you share code between both. Not always an option, but I've always felt more productive (as a full-stack dev) when I've been using TS on both the client and server, compared to TS on the client and another language on the backend.
:wave: Hey Mark -- I'm one of the primary contributors to Fern.
+1 to your comment about how needing to support multiple languages results in not being able to leverage certain language specific features. We've tried the best to manage the trade-offs here, but there's a limit to what you can do.
If you have feedback on how we can improve the TypeScript client, feel free to comment here or create an issue on our repo (https://github.com/fern-api/fern)!
I prefer building an OpenAPI-compatible API over tRPC to avoid being stuck on Typescript. I don’t fault the creators of tRPC for their decisions. It’s their project, and they don’t have to build interoperability. You can always use gRPC for that.
I would prefer they focus on doing their one thing well than trying to please everyone only to end up pleasing no one.
I've just started using tRPC, and I'm in love with it so far! I use GraphQL at work, but I always felt like the boilerplate is too much for my hobby projects, and I wasn't really happy with the code-first frameworks that I could find (especially with the ones that support proper Relay integration).
Thanks a lot for the creators! Also if anyone is enticed by this, I highly recommend trying it out!
Oh yeah, I've heard about it but by that point I was using tRPC which fits my needs for now. I'll be looking at it later though when I'll need a GraphQL api! Thanks for letting me know!
I've personally been using ts-rest for my projects, which is traditional REST but allows one to autogenerate types and API docs from a zod schema.
tRPC seems cool, but seems riskier to go with an alternative to REST or GraphQL, especially if you intend to make your API public and/or have multiple consumers of your API.
tRPC is not at all intended to ever be a public API, the way REST or GraphQL is. It's meant to be a tight, private connection between your client and your server.
If you use tRPC in your stack but want a public API, you will almost certainly need to build that out separately, via traditional REST, or GraphQL, or maybe gRPC. This feels all kinds of wrong initially, and there are some obvious and not so obvious disadvantages to this, but honestly it's not all bad when you get down to it.
At an open source company I worked briefly, we used tRPC, and I was tasked with making the Enterprise API,
We went with a nextjs app, abstracted away our tRPC routers into a package and our monorepo, and used tRPC different routers both from our webapp and the API apps.
This works great!
You end up not repeating your logic all over the place, and make thin wrappers on your nextjs api endpoints or whatever to handle the differences between implementations
I’m also singing ts-rest.com’s praise these days. Admittedly a small project with <25 endpoints atm, but holy crap, I am saving SO MUCH TIME on implementations, documentation, and bug hunting.
I have separate packages (in a monorepo) for the “contract definition” and the “API implementation” (which is currently NextJS, but likely Express in the future — and the migration path looks like very smooth sailing)
Request/response validation, OpenAPI spec, fetch & react-query clients without any additional effort. Mocks for tests auto-generate based on contract definition as MSW. It almost feels too good to be true.
So what do you do when you decide that you don't want to use JavaScript anymore on either side of tRPC? (switch to something else on the server, or write a native mobile app, etc)
The way I've been approaching this lately is TypeScript on the frontend, then a thing TypeScript layer on the backend (via Deno), with those two pieces connected with tRPC. The real backend guts are in Rust (or whatever), and the backend tRPC layer talks to the Rust stuff with gRPC.
So something like this:
[(TS web client) <--tRPC--> (TS thin backend)] <--gRPC--> (Rust service)
This is a bit awkward, but honestly worth it for what you get with tRPC. One thing that took some getting used to is with tRPC the line between "client" and "server" gets blurry, which makes me uncomfortable for all sorts of reasons but in practice works well enough to make it not worth worrying about for now.
Because you lose all the stuff that’s nice about tRPC.
The experience of building a tRPC app is very different from building an app that talks to a traditional REST API. The front and back end with tRPC are very tightly bound. In a way the backend part of your tRPC app becomes the real consumer of your actual API.
Okay. I see 3 main benefits of the tRPC experience.
1. You don't need to manually write HTTP routes on the server side.
2. You don't need to manually write request code on the client side.
3. There's a type contract between both.
I don't see how this "thin server" approach helps with any of these.
1. You're no longer writing HTTP routes, but instead you're writing gRPC. You haven't eliminated the work, you've just changed the technology. If you happen to be integrating with a pre-existing gRPC deployment, why re-invent the wheel with custom TS code instead of using one of the many gRPC->HTTP transcoders?
2. Modern web frameworks have so many abstractions on this pattern that it's a non-issue at this point.
3. Your thin server is effectively a mapper from gRPC to HTTP endpoints. That's the type of thing you can build a spec from. If you've used a transcoder and have a spec, you can codegen your client library with the correct types.
I think tRPC works best for making highly cohesive full stack apps. Using it as a middleman for your backend seems weird to me.
I'm honestly pretty happy with TypeGraphQL. TypeGraphQL works code-first and lets you integrate request-scoped DI for resolvers, which makes writing more complex resolves significantly more pleasant.
Admittedly for the web front end I couldn't find a satisfactory tool so I built typed-graphql-builder (https://typed-graphql-builder.spion.dev/). You do have to run it if your backend schema changes, but not when your queries change as the queries are written in typescript and inferred on the fly. (I should probably write a watch mode for the cli, that should largely take care of the rest of the toil when quickly prototyping)
There are more important problems to solve when trying to boot up a self-sustaining startup or project than rewriting your app in a different technology for no apparent reason.
I’m 99% convinced this is one of those things that will quickly go out of fashion and leave behind thousands of projects using “that legacy fox thing the previous devs wanted to use”
222 comments
[ 2.1 ms ] story [ 261 ms ] threadYou don't need it, but you certainly can prefer to use it regardless?
Hoping react-server-components <> trpc gets solved soon
https://nextjs.org/docs/app/building-your-application/data-f...
Will try out calling the database directly there, but I kinda liked how with tRPC I can add validation/auth checks etc/ will have to see how I develop my own strategy for that w server actions.
https://github.com/pingdotgg/zact
I wrapped up something like this myself a few weeks ago, so I am in a position to offer some suggestions other people may not think about.
The most common mistake I see with RPC, especially WebSockets, using Node.js is that they must be based off of HTTP. My attempt is based upon WebSockets, RFC 6455, where RPC is a generic term for socket based communication streams not specified against any single frame definition scheme. Since these technologies are raw sockets that include their own conventions for handshakes, optional frame header definitions, and possible security conventions there is no need for HTTP. In the OSI model HTTP is layer 7 where TCP sockets are layer 4. In Node that means just using the net/tls libraries opposed to the http based descendant libraries. Since, in Node, the http library descends from from the net library and https descends from tls which descends from net http always imposes overhead that TCP based sockets do not require.
There are three benefits for executing a socket server over HTTP:
1) You are only using port 443
2) Simple and familiar implementation from Node
3) HTTP 1.1 is session-less, which allows anonymous untrusted connections. That is how the web works, but its less ideal for a security focused implementation.
The reasons to not do this include CPU cost. Running sockets over HTTP increases processing overhead which reduces the number of concurrent streams you can offer and substantially slows down processing of incoming frames. In order to reduce your execution to a single port without sacrificing performance you could run HTTP over your socket implementation which allows you to customize your approach to security, but that would also require writing your own HTTP libraries.
The reason they might be blocked by big banks is due to deep packet inspection. How that works is that the bank intercepts certificates on TLS socket establishment for normal web traffic so that the bank proxy becomes a formal "main in the middle". They do this to provide deep packet inspection on all encrypted traffic that goes out and comes in. That level of packet inspection is more challenging with something like RPC/WebSockets because its a binary stream, where HTTP just uses plain text for its header data.
I can remember using Reddit, when I still used Reddit, when starting at the bank and I remember Reddit making heavy use of WebSockets that worked just fine from within the bank even though I was behind the bank's proxy. This was more than 6 years ago, but I believe the WebSocket traffic at that mega bank was just relayed through proxy just like the HTTP traffic and I also want to say Reddit served the WebSocket traffic different from the HTTP traffic, but I cannot remember for sure.
Why is that a problem?
As tRPC is in competition with solutions like GraphQL or REST APIs where the backend can be implemented in a big number of languages, I thought that limitation was worth pointing out.
You can build this kind of thing yourself easily using Typescript’s mapped types, by building an object type where the keys are your API names, and the values are { request, response } types. Structure your “server” bits to define each API handler as a function taking APIs[“addUser”][“request”] and returning Promise<APIs[“addUser”][“response”]>. Then build a client that exposes each API as an async function with those same args and return type.
We use this strategy for our HTTPS internal API (transport over POST w/ JSON bodies), real-time APIs (transport over Websockets), Electron<>Webview APIs (transport over electron IPC), and Native (iOS, Android)<>Webview APIs (transport over OS webview ipc).
For native APIs, the “server” side of things is Swift or Kotlin, and in those cases we rewrite the request and response types from Typescript by hand. I’m sure at some point we’ll switch to a binary based format with its own IDL, but for a single cross-language API that grows slowly the developer experience overhead of Protobuf or similar hasn’t seemed worth it.
I’ve also spent some time on a Typescript type to X compiler. My first prototype is open source and targets Thrift, Proto3, Python, and JSON schema: https://github.com/justjake/ts-simple-type/tree/main/src/com...
I’m not happy with the design decision in that codebase to try to “simplify” Typescript types before compiling, and probably won’t continue that implementation, but we have a few internal code generators that consume TS types and output test data builders and model clases we use in production.
I want to open source some of those bits but haven’t found the time.
https://deepkit.io/
What happens if the Deepkit guy retires? What if I want to run my code without waiting for 11 minutes of typechecking? What if there’s a bug somewhere in there?
There’s way too much risk for me to consider Deepkit for production.
I also wrote a 10-line mock API wrapper that you can call as mockApi<SomeService["operationName"]>((request) => response), and it will type-check that your mock function implements the API correctly and return a function that looks exactly like the real API function.
[0]: https://github.com/ferdikoomen/openapi-typescript-codegen
I really like that the openAPI approach is language agnostic, and makes it relatively simple to support SDKs for many other languages if needed. For any company where the API itself is a product, OpenAPI is great.
Good zero-config OpenAPI support is one of the best features of the FastAPI framework in Python. The "fast" part refers to the speed of basic product up and running.
Apologies if I've misunderstood your comment
https://koxudaxi.github.io/datamodel-code-generator/
I'm basically wondering what would help us produce (automatically?) a Python HTTP client based an OpenAPI spec, and achieve a development experience similar to using drwpow/openapi-typescript.
drwpow/openapi-typescript will generate the whole schema, along with paths, request and response models, as well as path and query parameters. In the IDE, all client methods will be type-safe and autocompleted.
Example:
koxudaxi/datamodel-code-generator seems to generate the response models, but not much else. It seems we would have to manually write wrapper methods for each endpoint, manually specify the parameters and request models, and use a type hint for the return value of each method with the generated response model.Example:
I'd like to avoid any manual wrapping or at least minimize the amount of it. Basically: to achieve a similar experience to what we have in TypeScript.I hope my question is a bit clearer now. I'm not that familiar with Python, so I will appreciate any guidance regarding this problem. :)
For all its issues, the JS world typically provides an excellent developer experience and takes types far more seriously than python.
Hope you find something that helps, and if you do I’d love to hear about it.
http://buildwithfern.com
For those wanting less talk, moar code: https://github.com/fern-api/fern-java/blob/0.4.2-rc3/example... -> https://github.com/fern-api/fern-java/blob/0.4.2-rc3/example...
Regrettably, I wasn't able to readily find a matching openapi example, likely cause they really seem to believe their kid-gloves format is the future (or lock in, depending on how bitter one is feeling)
---
confusingly, their repo has a "YCombinator 2023" badge on it that just links to the badge itself. Some Algolia for Launch HN didn't cough up anything, but there was a Show HN I found: https://news.ycombinator.com/item?id=34346428
This is good feedback that we need to provide more examples starting with OpenAPI instead of with the Fern Definition. For some context, we convert the OpenAPI spec into a Fern definition and then pass that into the code generators.
If you want to see some real world examples, check out these links:
- https://github.com/vellum-ai/vellum-client-generator/tree/ma... -> https://github.com/vellum-ai/vellum-client-python
- https://github.com/Squidex/sdk-fern/tree/main/fern/api/opena... -> https://github.com/Squidex/sdk-node
Worth calling out that you can go from the Fern Definition -> OpenAPI anytime to prevent lock in.
https://github.com/mikew/transmission-material-ui/blob/maste...
https://github.com/mikew/transmission-material-ui/blob/maste...
https://github.com/transmission/transmission/blob/main/docs/...
Seems like you're just inserting yourself and ranting about typescript where it's not really applicable.
There's others expressing the same concerns, for example https://news.ycombinator.com/item?id=37101393
To me, if the server has updated it's schema and the client has old code, the server responds with an error, the user sees "something went wrong".
And if the validation fails on the client side, the user sees "something went wrong"
And if the client isn't doing validation of what's returned, and an error gets thrown because it tried to access a now missing field, the user sees ... "something went wrong"
Intentionally maintaining multiple versions of an API, or making sure your API changes are backwards compatible (like adding new fields and marking old ones as deprecated), those are solutions but definitely require organizational effort.
All totally expressible via Typescript though!
The benefit from validation is that the error is easily recognizable & can be handled nicely, even in just one place. Without validation, you get things that Typescript claims are impossible, for example exceptions on code paths that look like they cannot throw, and you can't easily tell why any of that happened -- that's a recipe for miserable debugging. Of course, with less busy, less critical, sites you might not notice or care.
[1]: Refresh could cause loss of user input (e.g. text just entered), depending on the app that might matter and need browser-side storage or something along the lines of the "local first" manifesto. Easy kludge for simple apps with smart users is to make the user copy the text, click refresh, paste.
One solution I thought was clever, was a friend's single-page app would hard-reload on navigation, if there was an update to the assets. Not sure how feasible that would be for schema errors though.
SvelteKit can poll to check server version and refresh, both on timer and if assets are 404. But as you implied, that doesn't help with API evolution and e.g. users interacting with a form (apart from increasing the likelihood the page will refresh soonish due to navigation).
The requests and responses are inferred from the interface (published following semver) defined in Zod (including the errors that are following HTTP-like conventions: 401, 409…).
It also includes “hooks” for pre and post processing on the server side (effectively middlewares)
One thing that worked surprisingly well: codegen TypeScript types from your database and use those in your API schema.
Edit: RSC = React Server Components - which come with their own read/write data philosophy.
(or please define/link acronyms folks may not know!)
React and Angular won by being backed by FAANG first, people loves to choose FOSS backed by big names for some reason gives them pause
One nice side effect of using tRPC is that you can pack up and move to a different framework on the front-end if desired, and a lot of the tRPC work can be used directly
It's possible there's a project out there which could automatically produce proto files from something like zod, json-schema, etc. which could be directly interpreted by TS to provide similar (as you type) DX while still allowing some other language backend to consume the derived proto files (though the DX there would be less than ideal).
If you're just looking for similar TS clients/interfaces for grpc-web then I'd recommend https://github.com/timostamm/protobuf-ts which operates on plain JS objects (no new MyMessage().serialize(), instead the code generator mostly produces TS interfaces for you to work against: const myMessage: MyMessage = pojoConformingToInterface; const binary = MyMessage.toBinary(myMessage);)
With tRPC, changing an attribute of a class in your backend immediately tells you what broke in the frontend in your IDE. No need to recompile or regenerate anything.
Also, when developing your frontend, you'll have immediate autocomplete. Again without code generation or compilation.
That alone should save you dev time and prevent a ton of frontend bugs.
Could you expand on that? Our graphql types are generated from our federated schema whenever it changes, and response types for queries / mutations are generated whenever you save a file.
> you infer client types from server definitions without including actual server code into the client
Our types are generated into their own package, so there's no chance of importing server code.
For REST APIs there's ts-rest (https://ts-rest.com), zodios (https://www.zodios.org) and Hono (https://hono.dev)
If your team uses multiple languages, there's Fern: https://www.buildwithfern.com
The people on the Discord are very helplful
https://ts-rest.com/docs/comparisons/rpc-comparison
This is mildly horrifying to consider without good tooling support.
Pop fashion industry.
I've never seen IDL before but I looked up some examples and it does seem useful for RPC calls. But I'm not exactly eager to switch, it looks like it's meant for a much more powerful form of RPC than I'm willing to touch, and no tooling I know of supports it.
The JS ecosystem of course is still affected by hipster disease (everyone seems to think their own wacky idea is groundbreaking). But the existence of a framework like this, built on well-established JSON-based standards shouldn't be surprising.
I think you kind of made his point for him.
Proving my point.
Search for Sun RPC, DCE RPC, XDR.
Take note when they came up, and everything in between until today.
Consider that JSON being ubiquitous immediately makes it easier to adopt compared to a custom description language. If people actively used IDL today, there would probably be a lot of demand for a JSON variant or subset.
I'd make similar arguments about using JSON vs S-expressions for data interchange, but JSON works well with both Javascript and HTTP and everyone standardized on those, and maps cleanly to basic data structures in just about every modern programming language.
These JSON-based tools are actually very much like Lisp in that both the interface specification and the data are expressed in exactly the same format/language. This is not true of a lot of these older standards, and seems to have been the failed promise of XML.
IDL does look like it maps nicely to typed function calls in most languages, but it lacks the advantage of being expressed in a standard format/language that is already well-supported for other tasks, and seemingly doesn't impose any requirements on how the data itself is transmitted.
For an example of why language/format matters, consider the tool c2ffi (https://github.com/rpav/c2ffi). It generates a JSON description of a C header file. The header file itself is a pain to parse and extract information from. But once you have a tool to do it and put that information in a standard format, you can now build an FFI wrapper in just about any other language in at least semi-automated fashion. It makes the easy parts easier, compared to other systems like SWIG and GObject where the interface format is totally custom and you're mostly reliant on a single implementation to make it all work.
If anything, let's be grateful that the good ideas of the past are being rediscovered and reinvented in a way that might grant them more longevity and broad usefulness than they had in their first life. Did you use IDL? What was your experience like? How would you compare it to something like gRPC?
Android Binder, XPC, COM and gRPC are yet again another set of IDL quite present in current times.
JSON? I thought everyone was using YAML now. /s
The purpose of an RPC IDL (protobuf being a “modern” example) is that you define the interface and encoding in a way that will still be functional when your “standard language” is long forgotten or unrecognizably different.
I've been using gRPC and REST'ish+JSON APIs for years now and what I find puzzling is that REST+JSON tends to mean a lot more work, less pleasing code than gRPC, and yet people prefer it. Not because it leads to simpler, better, faster less error prone code (it doesn't. Quite the opposite), but, I think, because people feel they can understand it.
The people over at https://buf.build have been doing a great job trying to tame gRPC btw. The protoc toolchain from Google has been uniquely horrible to work with, but the 'buf' tool from said company has been a real life-saver. Not to mention their Buf Schema Registry, which admittedly I have only used on a few projects so far, but should migrate more projects to.
Though in general, I feel that RPC mechanisms that are too closely tied to a given language are a waste of time. But that's me. tRPC isn't something I'd get into even if I was doing server side TS. It just doesn't seem like a good long term choice.
I feel this is the same debate between scripting languages and compiled languages, both provide different trade-offs and I don't think we'll see one completely disappear at any point in time.
Hmm. I'm not sure what you are saying.
The way people tend to try to consume REST APIs is by generating client (and/or server code) from a spec. For instance OpenAPI 2.0. At least on Go the tooling for that leaves something to be desired, and the generated code isn't exactly beautiful. So you either depend on a library that someone generates from the spec, and then shares, or you generate it yourself.
(We do this for a bunch of languages for our APIs, and the clients are ... not uniformly beautiful :-))
If we're talking about writing the REST client by hand...well...I'm not sure why one would want to do that. (Nor am I sure that's what you meant, so I'm not accusing you of that).
My current workflow (in Go) for consuming gRPC interfaces is to use the buf.build package proxy. Which means I just add an import, run go mod tidy, and I'm good. I don't even need the tooling installed. As I think it should be.
I suppose this could be done for OpenAPI 2.0 too. You stuff OpenAPI 2.0 specs in and you get code for a bunch of languages out. (Someone must surely have built this already?)
One of the worst example (in the past) was the python pickle. People have overused it, and it's not even compatible between some python releases. There are many other examples - where something works really neat, but only for that language, or even that language major or even just minor release.
There is protobuf, cap'n'proto, flat buffers, fidl, thrift, etc. - many better choices than just one that works only for your language.
Careful what you wish for. Standards that aren't quite good enough can be worse than not having standards. Because then at least then people will keep looking for something that might be better.
It might not beat a bespoke hand-crafted protocol that minimizes message size, but your use of the word "negotiate" makes me think that that's not what you're taking about...
Essentially I'd like to inject a custom respond[0] encoder and a custom parseMessage[1] decoder, and same for the client.
[0]: https://github.com/trpc/trpc/blob/main/packages/server/src/a...
[1]: https://github.com/trpc/trpc/blob/main/packages/server/src/a...
Here's a gist with the wrappers (transporting the data as a JSON array instead of a dict): https://gist.github.com/TimonLukas/7c757a3b234344ad71e6bd5a6...
It's not ideal insofar that I have to parse the stringified JSON again. Some kind of real "encoder/decoder" parameter could be an amazing help.
This will reduce message size quite a bit, since the data isn't sent as:
but instead as:I will add wrappers for WebSocket/WebSocketServer in the future to allow using this without the library having to support it, like in the PoC with trpc.
In general in my experience, when you take away the constraint of inter-language interop, you get much smoother DX because you can take advantage of language features. A good example would be the lack of native date/time types in JSON - valuable for interop because different languages handle dates differently, but painful when you're going to/from the same language. Web applications are a special case, because the client-side is effectively constrained to either JavaScript or WebAssembly (except you'd still need at least some JS in the latter case), so it follows that you'll get the best DX if you have JS or TS on both sides of the stack, especially if you can set up your project in a way that lets you share code between both. Not always an option, but I've always felt more productive (as a full-stack dev) when I've been using TS on both the client and server, compared to TS on the client and another language on the backend.
[0]: http://buildwithfern.com/
+1 to your comment about how needing to support multiple languages results in not being able to leverage certain language specific features. We've tried the best to manage the trade-offs here, but there's a limit to what you can do.
If you have feedback on how we can improve the TypeScript client, feel free to comment here or create an issue on our repo (https://github.com/fern-api/fern)!
I would prefer they focus on doing their one thing well than trying to please everyone only to end up pleasing no one.
In my case, we generate a Typescript API client for a React app. Very nice companion to Golang.
tRPC seems cool, but seems riskier to go with an alternative to REST or GraphQL, especially if you intend to make your API public and/or have multiple consumers of your API.
If you use tRPC in your stack but want a public API, you will almost certainly need to build that out separately, via traditional REST, or GraphQL, or maybe gRPC. This feels all kinds of wrong initially, and there are some obvious and not so obvious disadvantages to this, but honestly it's not all bad when you get down to it.
We went with a nextjs app, abstracted away our tRPC routers into a package and our monorepo, and used tRPC different routers both from our webapp and the API apps.
This works great!
You end up not repeating your logic all over the place, and make thin wrappers on your nextjs api endpoints or whatever to handle the differences between implementations
I have separate packages (in a monorepo) for the “contract definition” and the “API implementation” (which is currently NextJS, but likely Express in the future — and the migration path looks like very smooth sailing)
Request/response validation, OpenAPI spec, fetch & react-query clients without any additional effort. Mocks for tests auto-generate based on contract definition as MSW. It almost feels too good to be true.
The way I've been approaching this lately is TypeScript on the frontend, then a thing TypeScript layer on the backend (via Deno), with those two pieces connected with tRPC. The real backend guts are in Rust (or whatever), and the backend tRPC layer talks to the Rust stuff with gRPC.
So something like this:
This is a bit awkward, but honestly worth it for what you get with tRPC. One thing that took some getting used to is with tRPC the line between "client" and "server" gets blurry, which makes me uncomfortable for all sorts of reasons but in practice works well enough to make it not worth worrying about for now.The experience of building a tRPC app is very different from building an app that talks to a traditional REST API. The front and back end with tRPC are very tightly bound. In a way the backend part of your tRPC app becomes the real consumer of your actual API.
1. You don't need to manually write HTTP routes on the server side.
2. You don't need to manually write request code on the client side.
3. There's a type contract between both.
I don't see how this "thin server" approach helps with any of these.
1. You're no longer writing HTTP routes, but instead you're writing gRPC. You haven't eliminated the work, you've just changed the technology. If you happen to be integrating with a pre-existing gRPC deployment, why re-invent the wheel with custom TS code instead of using one of the many gRPC->HTTP transcoders?
2. Modern web frameworks have so many abstractions on this pattern that it's a non-issue at this point.
3. Your thin server is effectively a mapper from gRPC to HTTP endpoints. That's the type of thing you can build a spec from. If you've used a transcoder and have a spec, you can codegen your client library with the correct types.
I think tRPC works best for making highly cohesive full stack apps. Using it as a middleman for your backend seems weird to me.
Admittedly for the web front end I couldn't find a satisfactory tool so I built typed-graphql-builder (https://typed-graphql-builder.spion.dev/). You do have to run it if your backend schema changes, but not when your queries change as the queries are written in typescript and inferred on the fly. (I should probably write a watch mode for the cli, that should largely take care of the rest of the toil when quickly prototyping)