Hacker News new | past | comments | ask | show | jobs | submit login
TRPC: End-to-end typesafe APIs made easy (trpc.io)
123 points by namelosw on May 6, 2022 | hide | past | favorite | 56 comments



I wrote the initial proof of concept for tRPC a little over a year ago (though I'm no longer involved in the project). Seems like there's some confusion about how this works. A more complete description is available here[0] below but I'll put a brief explanation below as well.

- It's designed specifically for a full-stack TypeScript app. Your API is implemented as strongly typed server-side functions.

- The input type is specified using a schema library like Zod or io-ts.

- TypeScript infers the return type of these functions.

- You can then import the type signatures of your functions into your client. Just types, no runtime code! This is safe in typescript with the `import type` syntax. tRPC uses those type signatures to provide a typesafe client API, basically an SDK for your API. There's no codegen happening - the client uses TypeScript generics to provide a set of strongly typed "proxy functions" that can be "called" on the client. These proxy functions execute HTTPs requests under the hood: https://trpc.io/docs/rpc

The point is to avoid the boilerplate, codegen, and lack of DRYness associated with building a strongly typed API in TypeScript. You can do this with GraphQL but it relies on code-generation, requires complete buy-in, and (IMO) doesn't provide a great developer experience. TypeScript has a native way of representing types, so the goal is to avoid an intermediate representation (in the form of an OpenAPI spec or GraphQL schema) entirely. As you update your server code, the client's type signatures update instantaneously.

Another important goal is to make it easy to write DRY code when building APIs. Without tRPC, it's common to manually provide type definitions for the results of API calls. But those definitions can easily get out of sync with your actual server code. tRPC infers types from your code itself and provides a typed interface for your API thats doesn't require any manual type annotations.

[0] https://colinhacks.com/essays/painless-typesafety


This is very cool, thanks so much for explaining how this works and, of course, for initiating such a cool project!

As far as I can tell, tRPC basically bridges (hides) network communication and allows doing remote procedure calls almost like regular, type-safe function calls on the same machine within the same process.

Now I would give a fortune for having something like that also for cross-language communication and for communicating with other processes (binaries and shell scripts). I've been thinking about this a lot in recent years: In SWE we need to jump through many hoops only because it's so god damn hard to invoke a function or a whole API living somewhere else in a type-safe manner. Intermediate formats like JSON, protobuf, standards like REST, OpenAPI, and GraphQL, and even languages like SQL (and the various ORM libraries it inspired) are all a result of that to some degree: Once data leaves the safe haven of your (hopefully statically typed) programming language where the compiler/interpreter knows exactly what its type is and what the signature of the function is that you're trying to call, you're basically on your own.

I recently read about WASM Interface Types[0] and it seems like a step in the right direction, though unfortunately it will only bridge the – let's call it – type safety gap between processes, not between different machines in a network.

[0]: https://hacks.mozilla.org/2019/08/webassembly-interface-type...


What's the backwards compatibility story here? What changes to backend function argument and return types can I make without breaking currently-open webpages when I push a new backend instance?


So essentially, it's protobuf but it describes the API instead of a message (which arguably is also an API description) and is for TypeScript on both sides.


Exactly. But with faster iteration speed, more type inference, less overhead, and a TypeScript-native DX.


Not looking to actively bash anyones work here but I’m really confused on the value proposition here over something like gRPC which has a lot of substantial advantages from speed, to wide cross language support and has been battle tested in the most extreme scaling scenarios possible.

A truly huge amount of time, money and optimization went into a project like that and has been proven to work well for years now.

The JS ecosystem confuses me with things like this regularly just the other day I was watching a video where the lead devrel person from Vercel was trying to explain “edge functions” to a senior backend engineer that started falling apart under the most rudimentary questioning so much so that in the video they have this awkward hard cut and they rush to wrap up the interview. Link if anyone is interested https://youtu.be/yuxd2kurpzk

This gives me kind of similar vibes in that I just don’t really get the use case here unless it’s to intentionally stay strictly in the JS ecosystem for some reason?


You can't provide typesafety (with TypeScript) across the frontend and backend for multiple languages without codegen. There's no intention for this to be strictly JS, but there's no easy way to offer this for multiple languages.

There will be an openapi generator some time in the future so that could help with generating clients for other languages (https://github.com/trpc/trpc/issues/1724).


I think maybe that’s the part I’m not getting.

I don’t have a problem with the code generation personally, if anything I appreciate having something I can go and inspect if things go wrong so avoiding it isn’t an obvious plus to me.


Using TS type inference to its fullest is a major part of tRPC so I don't think it has much of a benefit to you if you're ok with generation.

In my experience, you can iterate more quickly without generation. I've used GraphQL and having a separate system to generate code and types was not a good experience. If not done properly, your schema and types can also easily be out of sync.


Never worked with GraphQL only gRPC which in my setup at least has an extremely tight feedback look in that code gen is almost instant and triggered on a file save of a proto file (which is where you define your interfaces in that particular ecosystem) which again sounds like maybe where my confusion is coming from, none of the pain points you mentioned have ever been a problem for me but if it works for you then more power to you, not here to say you’re wrong :)


So what problem does this solve? Protobuf + grpc already provide types for tons of languages. As in, messages are generated to models/structs/classes what have you, depending on the language…

OpenAPI already exists and anyone can write a spec today and have code generated (servers, clients, models) today. Same principle as protobuf but the message and service description is much more verbose.

> tRPC to OpenAPI generator

So, … another level of indirection? Why would I ever pick this up when I can just use OpenAPI directly?


If you want a similar experience to this but more optimized for backend development using Go, I've been building https://github.com/encoredev/encore for the past few years!


You can do this with GraphQL too:

https://genql.vercel.app/

https://github.com/graphql-editor/graphql-zeus

I did a 5 min talk about these newer breeds of codegen tools (where it's a single client SDK that does automatic return type inference based on the input args), they're really neat:

https://www.youtube.com/watch?v=7n3MeMFHiMk

(Skip to 2:14 to see the autocomplete/type-safety)


It's not the same though. tRPC requires no code generation.

With tRPC if I change the return type of an API, I get instant feedback on my frontend code with errors and autocompletion.

Is my understanding correct?


Correct! Instead of codegen, it relies on the TS language server to enforce schema compliance.


This is one of the best things about typescript. You can do this kind of thing without much special tooling at all - https://www.evantahler.com/blog/post/2020-10-16-typescript-f...


tRPC has been a lifesaver for us at Ping.gg

Never had such a seamless experience building and deploying “full stack” apps. Multiple classes of bugs just, like, don’t happen now?

Helped build up a lot of the GraphQL tech at twitch, and I still love it, but man do I not miss all the work maintaining the “contract” between front and back. I know we will have to move off tRPC eventually, but I’m hyped at how far it’s taken us already


> I know we will have to move off tRPC eventually

Is that when you can no longer get by with only NodeJS/TypeScript on the backend?

I've been thinking about that in a greenfield project I'm trying tRPC out.


This project looks interesting! I have used a similar project called tsoa [1] and have appreciated that it generates an OpenAPI spec that can be consumed readily by many other clients, even outside the JS ecosystem. Does TRPC have plans for a similar feature set?

[1] https://tsoa-community.github.io/docs/


Yup, with the optional `output`-property we actually have all the type information needed to generate an openapi-schema.

See this for more info: https://github.com/trpc/trpc/issues/1724


Thanks for the link! That looks really stellar actually. Excited to see how it shapes up.


The docs don't say anything about the wire protocol - is this based on REST/JSON, or something proprietary? If proprietary, is it spec'd and documented? Support for implementations in other languages?



Only supports using Javascript/node servers it seems, so sadly it's not really usable for me.

The type exchange seems to rely on shared code between the frontend and the backend to exchange types, rather than using schemas like other systems.

Not implementing server-side events seems like a shame because they're a very capable mechanism for subscriptions without resorting to websockets.


TRPC is designed for systems where the front and back ends are both TypeScript. In that case it's simpler to have them share code than to go through an intermediary type system such as GraphQL.


If you're using OpenAPI, you could use this to generate TypeScript interfaces:

https://github.com/bcherny/json-schema-to-typescript

It works really well


I don't believe there's anything really preventing the use of server-side events. The Websocket implementation is via an adapter and special client so I think the same could be done for SSE (probably achievable in an external package too).


I am really not getting this. If you run Typescript on both ends, can't you just share the source code that defines the types?


You kinda can share function signatures with TypeScript's import type.

And that's what tRPC does but adds some generics in the middle to make things seamless.

Explanation here: https://colinhacks.com/essays/painless-typesafety


You could argue that this is actually adding a lot of (nicely knit) seams.


I think that's a good characterization!


Basically tRPC lets you define a router containing all your endpoints in a single structure. Then you can import the type definition of your router and make typed API calls without needing to wire everything together with types.


I’ve never used this, but it looks like a very similar idea to Haskell’s Servant library [https://docs.servant.dev/en/stable/]. Could someone who knows more enlighten me as to how correct this comparison is?


They seem similar. With Servant you start with a type definition describing your API (using type-level functions), from their docs:

type UserAPI1 = "users" :> Get '[JSON] [User]

In tRPC based on the example it looks like you start by defining and instance then grab the type and export it.


Hi, Servant user here: Haskell's type system is sound, which means that you can have type safety. TypeScript's type system is unsound, so every guarantee that you may think you have wrt. type safety go out of the window at the moment you start the typechecker.


Hey, don't be this way. Haskell's type system is also unsound (see: undefined, unsafePerformIO). Typescript has done an amazing job bolting an advanced static type system on top of a dynamically typed language. The edge cases of that type system exist, and are a bit more prevalent than Haskell's, but there exist a number of libraries in Typescript that take inspiration from Haskell libraries. This question is about the differences between the design of these two ways of statically representing APIs and not about the fundamental differences in the type systems of their host languages.


There's a huge gulf here. undefined doesn't introduce unsoundness, and unsafePerformIO isn't part of the language standard, it's strongly discouraged, and rarely used. I've been writing Haskell professionally for 10 years, for example, and I've never written unsafePerformIO.


I like this idea very much. Shameless plug and I don't want to be the Rust fanboy, but I've played with something similar in Rust:

https://github.com/julienr/liveboard-rs

Basically it uses actix for the backend and yew (Vue-like rust frontend framework) for the frontend. This enables one to share types (and helper functions) between both, which is great:

https://github.com/julienr/liveboard-rs/blob/master/shared/s...

That being said, I think maturity-wise, Typescript is probably a better bet for this right now, so I'll definitely look at trpc for $dayjob.


Awesome to see my lil' baby on the frontpage!


> without schemas

What is a type definition if not a schema?


Conceptually, yes. But type definitions are exported by declarative lines in the same code that runs. Basically the schema is imperative code that runs actual validations, the tRPC methodology simply looks at the inferred types from that code to enforce consistency between client and server rather than parsing an actual static schema artifact (e.g. a .gql file).


I'm not seeing the appeal of this over Smithy, which can target multiple frontend and backend languages. What am I missing? What is wrong with codegen tools? Especially when they help you design strong contracts upfront, rather than ad-hoc API design as their demo shows.


Nice! I wrote something similar for Next.js a while ago. The pattern is so much more ergonomic than everything else I've used before.

https://npmjs.com/package/next-rpc


I recently gave a talk about our experience with tRPC. So far it's looking very promising!

https://www.youtube.com/watch?v=k1TCueEhhJo


interesting this came up because I'm looking to do exactly this -- but my first instinct is simply to share declaration types from my server with client as a dependency


> Currently GraphQL is the dominant way to implement typesafe APIs in TypeScript (and it's amazing!).

Disagree here and it’s a bit disconcerting to see this as an opening premise.


How does this compare to gRPC? That the wire protocol is actually http+json instead of binary? I wasn't able to find much on their website


tRPC offers typesafety across your frontend and backend without needing to generate any code. This makes it so updates in your backend "router" are immediately reflected in your frontend code. You can't use gRPC with TypeScript without codegen. tRPC may be more familiar to TS/Node.js devs too.


I love this. This is the future


Does this work with NestJS?


It can be made to work with any framework with a proper adapter. NestJS is quite opinionated though so I suspect the two will not work well together.


you can't have type-safety with an unsound type system, silly.


Could you elaborate on this? What part of tRPC do you think is unsound due to the type system?


TypeScript's type system is unsound in a mathematical sense: https://effectivetypescript.com/2021/05/06/unsoundness/ Though this has little bearing on practicality which is probably why the original comment is at the bottom of the pile.

He's also using a stricter mathematical definition of "type safety" whereas tRPC means it in the colloquial way it's used in the TypeScript ecosystem (that is, a fully typed interface between client and server, ideally that's non-duplicative and inferred directly from your code instead of being manually defined).


Thanks for clarifying!


interesting. Kinda like fastify json schema.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: