tRPC – Build and consume typesafe APIs without schemas or code generation
trpc.io
trpc.io
I think if you're defining a JSON API, that TypeScript is a natural fit since its types line up with with JSON - ie, number is the same in both and if you want something special like an integer with more precision, then you have to model it the same way in your JSON as your TypeScript interfaces (ie, a string or array). This makes TS good even for backends and frontends in other languages. You can also convert TS interfaces to JSON Schema for more interoperability.
I use `null` for that purpose, and it's been pretty reliable. What are the situations where that falls down?
So if you want to convert your TS interfaces to JSON Schema, you may need to provide additional constraints via JSDoc and use a generator that understands those annotations. But your TS interfaces cannot express those constraints directly.
There are a number other related complications surrounding these more expressive schema definitions - like building types from them and interacting with them at runtime.
I'll "just" use the type system built into my programming language until the pain of supporting multiple languages is more expensive than installing JSONSchema tooling and messing with code generation.
There’s a learning curve to these things. It always starts with type FunctionIWroteTodayArgs = …, which is useless and tells you nothing.
After a few iterations (this takes years) people gradually realize that the goal is to describe your domain and create types/interfaces/apis that are reusable and informative, not just a duplication of your code. You then get more useful types and things start really flying.
I guess what I’m saying is work on that with your team, not on ripping out tRPC.
If you’re talking about decoupled services, that’s about business domain composition more than type description. And those types benefit from a higher level description/reusability/transportability.
It’s similar to the problems you run into by writing the wrong kind of tests – the ones that essentially just duplicate your code instead of validating input/output at boundaries.
I don't see how declaring an http client server side and consuming it client-side can be a worse thing.
We use the same pattern of creating services that then every consumer can use (a web interface, a cli, etc) and the fact that those things never get to break is a massive improvement over anything I've seen in the past.
Make devs slower so they code smarter?
More friction is just that, it just frustrates devs it doesn't make them code better.
tRPC enables good teams to ship faster. Less friction means doing what the team was already going to do anyway, but faster.
IMO, at a minimum, you have your API layer model, your internal model, and your database model with a mapping layer at each boundary.
I rarely have problems when I structure it that way, but when working on applications that pass the same model from their API down to their database, or vice-versa, it's always full of the same types of problems that comes with tight coupling.
I think this is only really possible in a relatively small subset of programming languages, even among those with static typing. At a minimum it seems like it would require a type system that didn't allow optional properties (by default) and did distinguish between nullable and non-nullable types.
Unless I'm missing something. Would love to see some examples of this done right. Or at least examples of languages you've done this in.
there is a “REST Wrapper” project out there, but learning that was even needed was … fun
I don't mind it, we found other ways to test
Out of curiosity, how do you add a memcache to tRPC if you dont want to write directly to the prisma database
Would that help your team?
Happy to give you a demo if you reach out on Twitter dms or email (alex@trpc.io)
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 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
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:
client = createClient<GeneratedSchema>();
client.get("/my/autocompleted/endpoint/{some_param}", { params: { path: { some_param: "foobar" } } })
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:
def my_wrapped_endpoint(...) -> ResponseModel:
pass
client.my_wrapped_endpoint(some_param="foobar")
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.
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.
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.
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).
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.
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.
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
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.
Zod and tRPC are some of the most important projects in the future of TS imho, I think we're gonna see a beautiful bloom of tRPC inspired DX across the TS space in coming years.
Two projects already have clearly have tRPC DNA attacking different use cases are Ping's UploadThing (https://github.com/pingdotgg/uploadthing) and our Lusat (https://github.com/lusatai/lusat).
Another annoyance is that the type of validation errors varies depending on what kind of schema you’re checking against. This makes it unpredictable to handle errors and there’s always too many edge cases.
Lots of room for improvement.
The new valibot.dev looks cool but I haven't tried it yet.
[0] https://github.com/fabian-hiller/valibot/blob/e6c53c8a4b0033...
Thanks for any insight!
But schemas for what?
From: https://blog.logrocket.com/schema-validation-typescript-zod/
Most common use case I’ve seen is using schema validation for user input (like forms) so you don’t send junk off to the api, you can validate data at runtime (whereas typescript checks your types statically when compiled).
Matt is awesome! If you have any more questions I’ll do my best to answer them.
Fields have a lifecycle. When they're introduced, no clients or servers know about them. Clients and servers aren't restarted all at once. They won't have the same version of the types. Data will be written using one version of a type and read using a different version.
If you can guarantee all binaries, running programs, and data gets upgraded (no persistent data exists) then it might not be a problem. As soon as you have multiple organizations involved, guaranteeing all apps and servers sync to the latest version of a library, rebuild, and redeploy will be difficult.
Static type checking assumes no version skew. All the code in the binary got built with the same version of the library defining the types. It's a closed world.
Of course, the demographic that most desires what tRPC does is the least likely to think about that sort of stuff.
1. Plenty of people deploy their frontend and their backend separately. 2. Even if frontend and backend are deployed at the same time, there's still the case where the user has the old version of the client loaded in their browser as a new backend gets deployed. I've seen some web apps tackle this by periodically checking if a new version is available and either refreshing the page or prompting the user to refresh the page.
Edit: RSC = React Server Components - which come with their own read/write data philosophy.
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
(or please define/link acronyms folks may not know!)
I quite remember tRPC being the only unknown part of the stack when I started there, and I was a bit scared, but tbh it was pretty easy to pick up and it's an awesome abstraction to make the server of fullstack codebases in typescript.
I always liked GraphQL but if you're working solo it doesn't make that much sense to have the GraphQL api as contract. With external devs a little bit more.
+1 to OpenAPI and being able to generate code, SDK's, docs, automagically.
Unicode Character “♥” (U+2665)
♳
This project has done incredible things for TypeScript
You 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.
We built a T3 app (tRPC, Next.js, Tailwind, TypeScript, Prisma) together (if you'd like to check it out https://github.com/stytchauth/stytch-t3-example).
Type safe APIs while working in TypeScript is just so helpful.
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.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)
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.
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.
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 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?)
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.
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.
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.
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
In my case, we generate a Typescript API client for a React app. Very nice companion to Golang.
If it is same language I just had common library implementing all the types for server and client, no need to get any more fancy than that, crossing the language barrier is the problem for type safety.
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?
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.
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
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.
This is mildly horrifying to consider without good tooling support.
Thanks to the creator. Literally made me a more efficient developer!
[1] https://hackathon.camp/ [2] https://sheetsinterview.com/
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);)
1) Java remoting itself
2) GWT's "emulation" of java remoting to the client side
It was SO very, very fast (and safe) to add a new backend interaction.
Plus I loved that you could be so evil as to just serialize a java class, send it over RPC, to be executed on the other side. Security nightmare (even though it was fixable if you really wanted to), but damn, you can do absolutely everything with that.
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:
{ "id": 3, "method": "subscription.stop" }
but instead as: [3,1]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.
also it means we can remove the npm library of types that was previously used to sync types between FE/BE (for this reason alone i love it really)
I found the Docs somewhat lacking when i was trying to get the first project going but now I've used it for 6 months and i don't need to look at the docs it's been great. (remember to have exactly the same versions installed on the FE/BE and the same tsconfig makes it easier too, spent too long on these obvious fixes... :P )
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.
+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.
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.
.input(z.object({ name: z.string() }))
What the heck is "z"?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.