REST API, tRPC, or GraphQL? What would you choose for your new mobile app startup?
Recently, I decided to bring one of my countless startup ideas to life š¤, and I got stuck on choosing a communication protocol between the mobile app and the backend. Unusually for me, I chose the path of least resistance and decided not to overthink architecture and so on ā just to start building the product. Knowing that it likely wonāt ever see the light of day, I didnāt worry much about optimizations, architecture, folder structures, or developer experience. I just wanted to start coding the product in a way that was reasonably convenient for me.
So, in short ā this was supposed to be a mobile app with a separate backend. I donāt like building serverless apps because they limit me a bit. Sometimes itās hard to implement file size/type validators, notification logic, cron jobs, etc. I prefer building a separate backend from the start so that the project is frontend-independent. That way I can change the UI anytime without having to move or rewrite a lot of business logic. But thatās not the point of this story.
REST API - the old way
As usual, I decided to use a REST API. I wrote some standard endpoints using the Hono framework. I couldāve used Express, but since the developer behind Express also created Hono, I figured there was some reasoning behind it. Everything was fine, the backend worked, so I moved on to the mobile app. Thatās when problems started appearing. I used react-query
as the client to communicate with the backend. Pretty quickly, I realized I was missing generated types. āWriting them manually is really dumb. I donāt see the point in duplicating types when I could just generate them using some toolā - I thought.
OpenAPI - "workaround" for type safety
Thatās why I decided to integrate my backend with OpenAPI. Luckily, Hono has integration with @hono/zod-openapi
, which allows defining endpoints strictly using Zod schemas.
I refactored the backend a bit and got to work on the mobile app. On the frontend, I used the orval
library, which parsed the OpenAPI schema and generated types and typed hooks for react-query
. That was better, I thought. But the types had weird names like GetUsersResponseOpenApi200
or something like that. I get it ā the backend can return different statuses and errors ā but for a startup Iām building just to test an idea, that was overkill. I didnāt want to handle every error or add custom statuses to Zod schemas in @hono/zod-openapi
, and then deal with a mess of types generated by orval
. I didnāt like it ā it looked messy. I still wanted to simplify the DX so I could focus more on business logic than type definitions and schema configurations.
OpenAPI Swagger UI
But, it worth mentioning that the integration with Hono and Swagger UI is pretty cool (via @hono/swagger-ui
). I could easily test the backend endpoints, see the request/response schemas, and even try out the endpoints directly from the Swagger UI. That was a nice touch. But as a developer, I focus more on the code than the UI. I donāt need a fancy UI to see what my endpoints returns. Types should speak for themselves. So, while the Swagger UI was nice, it didnāt really solve my problem.
tRPC - hyped "workaround" for REST API
At that point, I decided to try tRPC, which would give me powerful typing out of the box without complex configuration or auto-generating types.
Monorepo - the only place where tRPC makes sense
Itās worth noting that Iām working in a monorepo, so I can easily import the main app router type on the frontend. In my setup, though, development became easier. I had to refactor the backend a bit again, but now I know for sure: REST API is dead for me š (*in terms of developing mobile apps). Since tRPC communicates over HTTP, itās essentially a typed REST API. If you write a backend in tRPC and later want to change the frontend to another language (not TypeScript), you can still make requests to the backend ā you just wonāt have type safety. But in that case, youād probably have to define an OpenAPI spec and tweak the backend a bit anyway. I mostly use TypeScript for everything, so Iām not worried about it.
If youāre using separate repos for frontend and backend, you could theoretically still do this ā youād just need to locally link the backend package or set up automatic publishing. But yeah, itās a bit of extra work and arguably not easier than using OpenAPI.
Additionally, since Iām working in a monorepo, it made sense to create a shared package with Zod schemas to avoid duplicating code. Very convenient ā I recommend doing the same.
Enums - missing piece - the downside
However, one small issue remains. I havenāt found a way to share enums between the frontend and backend. I use Prisma, which generates types and enums. But the problem is I canāt use them on the frontend without adding Prisma as a dependency ā which I donāt actually use there. As a workaround, I just duplicated the enums into a shared package and used them on the frontend.
Another option would be to create a separate shared package that only exports the generated enums. But thatās a lot of manual, one-time work and a bit of overkill. At least in Prisma version 6.7.0, it now generates separate modules (enums, types, etc.), which might simplify this setup later. But thatās for another time (never).
GraphQL - goat š
Did I stick with tRPC? For now, yes. But I kinda regret not going with my favorite ā GraphQL. I gave in to the hype around these modern tools and wanted to try something new ā just to once again confirm that GraphQL is the GOAT. I just thought setting up a GraphQL backend would be more complicated than tRPC ā which turned out to be true. Also, thereās still no great frontend client with a DX similar to react-query
. Sure, thereās apollo-client
, but if youāve ever tried manually updating a cached object (instead of just doing refetchQuery
), you know that react-query
and apollo-client
are poles apart. I personally miss good type inference on the cache operations.
Pros of GraphQL
- Backend-agnostic
- Great type generation, typed hooks, and enums (which really makes life easier)
- Flexibility: I can fetch only the fields I need, not everything
- Resolvers: I can define a separate resolver for each field, which is super handy. Technically I do the same in tRPC when I have calculated fields. But the downside is, if I call a procedure, I fetch the calculated field even if I donāt need it. I could move it to a separate procedure, but thatās more manual work.
Regarding breaking changes ā with GraphQL, we change the schema and add @deprecated annotations. In theory, Iāll do the same with tRPC (havenāt reached that point yet), and the IDE should highlight deprecated methods.
I wonāt go over all pros and cons of these approaches ā that wasnāt the point of the article. I just wanted to say: donāt overthink it. Thereās no perfect solution. Youāre better off spending that time writing documentation, a business plan, or doing marketing instead of rewriting your backend for the hundredth time ā which youāll probably rewrite again in a few weeks. Of course, itās good to understand different approaches, but in solo projects (even commercial ones), itās often overkill. I believe good typing can sometimes replace good architecture (though that thought probably needs elaboration ā but if you get it, you get it). Finding balance is an art we all still need to learn.
Summary
-
REST API ā dead end in TS world š„“. I'm not saying it's obsolete, but it's not convenient for fully-typed monorepos. Yes, you have tons of tools for autogenerating types, even for setting up the entire REST API from PostgreSQL schemas and other cool stuff... But, I'd only use it if I know for sure Iāll be writing the frontend in something other than TypeScript, or if Iāll be writing microservices in different languages. Otherwise, Iād avoid it.
-
tRPC ā seems fine, at least for small startups. If you donāt have a ready-made GraphQL backend setup to copy, Iād go with tRPC. Especially since GraphQL might be overkill for your tiny project that youāll abandon in a few days. And if youāre not using a monorepo, consider either migrating to one or sticking with GraphQL/REST.
-
GraphQL ā powerful. Never had real issues with it (aside from manually updating caches in
apollo-client
ā it works, but type safety is awful. In theory, you could try usingreact-query
+graphql-request
, and also codegen. I havenāt tried it yet, so maybe Iāll write a short review on that another time). But itās a bit of a hassle to set up. I just wish there was a better client for it. Besides that, youāll have to write a lot of boilerplate code and deal with the complexity of setting up the backend. But once you do, itās great.
So, if you already have a working GraphQL project ā just copy it, update the packages, and start your new business. No need to waste time configuring it all from scratch again. I regret not realizing this earlier.
P.S. While writing this article, I barely resisted the urge to rewrite that damn backend to GraphQL instead of tRPC one more time...
P.S. Don't use this article as a guide for your startup. It's just a personal opinion. I don't know what I'm doing, and I don't want to be responsible for your decisions. Just do whatever you think is best for you.
P.S. This "comparison" was written based on mobile app and backend development. For other web projects, the situation might be different.