{"schemaVersion":"1.0","type":"TechArticle","types":["Article","TechArticle"],"slug":"rest-vs-graphql-vs-trpc-how-to-choose-an-api-style-you-won-t-regret-bemt8","url":"https://zyvop.com/rest-vs-graphql-vs-trpc-how-to-choose-an-api-style-you-won-t-regret-bemt8","title":"REST vs GraphQL vs tRPC: How to Choose an API Style You Won’t Regret","subtitle":"Three API styles, three different bets. Here is what each one optimizes for, where each one breaks, and how to pick without regret.","tldr":"REST, GraphQL, and tRPC solve the same problem with different trade-offs. This guide compares them on type safety, caching, performance, tooling, and team fit, with diagrams, code, and a decision framework for your next project.","keywords":["API Design","rest","tRPC","graphql","TypeScript","Versus"],"entities":["Anshu Pathak","API Design","rest","tRPC","graphql","TypeScript","Versus","ZyVOP"],"keyTakeaways":["Every few months, a team somewhere restarts the same argument.","Someone proposes GraphQL because the mobile app is drowning in requests.","Someone else points out that REST has worked fine for twenty years."],"headings":["The short version","The real axis: coupling","One feature, three implementations","REST","GraphQL","tRPC","REST in depth","GraphQL in depth","tRPC in depth","Head-to-head","A decision framework","You do not have to pick just one","Three mistakes worth avoiding","The verdict"],"outboundLinks":[],"contentText":"Every few months, a team somewhere restarts the same argument. Someone proposes GraphQL because the mobile app is drowning in requests. Someone else points out that REST has worked fine for twenty years. A third person, usually a TypeScript enthusiast, says none of it matters because tRPC gives you autocomplete across the network boundary. They are all right, and that is the problem. These three technologies are not competing answers to the same question. Each one is a bet about where the contract between client and server should live, and who gets to change it. REST puts the contract in HTTP itself: URLs name things, verbs say what to do, status codes say what happened. GraphQL puts the contract in a schema, and lets the client describe exactly the shape of data it wants. tRPC puts the contract in your TypeScript types, and lets the compiler enforce it. This guide compares them honestly: what each is good at, where each one hurts, and a decision framework you can apply to your next project in about five minutes. The short version Dimension REST GraphQL tRPC Where the contract lives HTTP conventions, optionally an OpenAPI spec A typed schema (SDL) Shared TypeScript types Type safety Via codegen from OpenAPI Via codegen from the schema Inferred, no codegen HTTP caching Native and excellent Possible, but you work for it Possible, but not idiomatic Over/under-fetching Common without extra design Solved by design Controlled by your procedures Public API fit Excellent Good, with caveats Limited; OpenAPI add-ons exist Language support Any Any TypeScript natively; others via OpenAPI Learning curve Lowest Highest Low, if you know TypeScript Client/server coupling Loose Negotiated Intentionally tight Best for Public APIs, many consumers, cache-heavy reads Many clients with different data needs Full-stack TypeScript apps you own end to end If you remember only one thing: the right choice is mostly determined by who your consumers are, not by which technology is newer. The real axis: coupling Most comparisons rank these on features. A more useful lens is coupling, because coupling is the thing you are really choosing. flowchart LR C[\"Where does the contract live?\"] --&gt; R[\"REST: HTTP conventions + OpenAPI\"] C --&gt; G[\"GraphQL: a typed schema\"] C --&gt; T[\"tRPC: TypeScript types\"] R --&gt; R2[\"Loose coupling. Any language, any client.\"] G --&gt; G2[\"Negotiated coupling. Client picks the shape.\"] T --&gt; T2[\"Tight coupling. One compiler checks both sides.\"]Loose coupling is a feature when you cannot control your consumers. A public API, a partner integration, or a mobile app version that stays on users’ phones for two years all need a contract that survives independent deploys. Tight coupling is a feature when you control both sides and deploy them together. It removes an entire category of bugs, because a breaking change becomes a red squiggle in your editor instead of a production incident. Neither is better in general. What hurts is a mismatch between the coupling you chose and the reality you live in. One feature, three implementations To make this concrete, take a feature every app has: show a user’s profile with their three most recent orders. REST // GET /users/42?include=recentOrders app.get(\"/users/:id\", async (req, res) =&gt; { const user = await db.user.findUnique({ where: { id: req.params.id } }); if (!user) return res.status(404).json({ error: \"not_found\" }); const body: Record&lt;string, unknown&gt; = { id: user.id, name: user.name }; if (req.query.include === \"recentOrders\") { body.recentOrders = await db.order.findMany({ where: { userId: user.id }, orderBy: { createdAt: \"desc\" }, take: 3, }); } res.set(\"Cache-Control\", \"private, max-age=30\").json(body); });Clean, cacheable, and understandable by anyone who has used HTTP. Notice the include parameter, though. The moment clients need different shapes, you start inventing a query language: include, fields, expand, and so on. That pressure is exactly what produced GraphQL. GraphQL type User { id: ID! name: String! orders(last: Int = 10): [Order!]! } type Order { id: ID! total: Float! } type Query { user(id: ID!): User }query Profile($id: ID!) { user(id: $id) { name orders(last: 3) { id total } } }The client asks for exactly what it needs, in one round trip. The cost moves to the server, where every field is a resolver, and a naive orders resolver will run one database query per user in a list. This is the famous N+1 problem, and the standard fix is batching with a DataLoader. tRPC // server const t = initTRPC.create(); export const appRouter = t.router({ profile: t.procedure .input(z.object({ id: z.string() })) .query(({ input }) =&gt; db.user.findUnique({ where: { id: input.id }, include: { orders: { orderBy: { createdAt: \"desc\" }, take: 3 } }, }) ), }); export type AppRouter = typeof appRouter; // client const user = await trpc.profile.query({ id: \"42\" }); user?.orders[0]?.total; // fully typed: no codegen, no schema fileThere is no schema file and no generation step. The client imports the type of the router, and TypeScript infers every input and output. Rename total to amount on the server and the client stops compiling. That feedback loop is the entire pitch, and it is a compelling one. REST in depth REST is not just “JSON over HTTP.” Done well, it uses the web’s built-in machinery: resources identified by URLs, verbs with defined semantics (GET is safe, PUT and DELETE are idempotent), status codes, content negotiation, and caching headers. Where REST wins Caching. CDNs, browsers, and reverse proxies understand Cache-Control, ETag, and conditional requests out of the box. For read-heavy workloads this is a performance and cost advantage no other style matches without extra work. Universality. Every language, every platform, and every tool (curl, Postman, the browser) speaks it. Integrating with your API takes minutes. Operational simplicity. Logs, rate limits, WAF rules, and monitoring all map naturally onto URL and method pairs. Public API credibility. If outsiders will build on your API, REST with an OpenAPI spec is the lowest-friction contract you can offer. Where REST hurts Over-fetching and under-fetching. Fixed response shapes mean some clients receive fields they do not need while others make several calls to assemble one screen. Contract drift. Without OpenAPI, the docs lag the code. With OpenAPI, you must treat the spec as a first-class artifact and generate clients and types from it, or you lose type safety at the boundary. Endpoint sprawl. Screens that need bespoke data tempt teams into either many tiny endpoints or a few bloated ones. Modern REST done right: design the spec first (or generate it from validated route definitions), generate typed clients, use ETags, paginate with cursors, and define a consistent error format such as RFC 9457 problem details. GraphQL in depth GraphQL is a query language and runtime with a strongly typed schema at the center. Clients send a query describing the data they want, and the server returns exactly that shape. Where GraphQL wins Client-driven data shapes. Web, iOS, Android, and partner clients can each ask for exactly what they need without a new endpoint per screen. A single, introspectable schema. The schema is the documentation, and tooling such as GraphiQL, code generators, and client libraries all build on it. Composition across teams. Federation lets many services contribute to one graph, so a frontend team can query across domains without knowing which backend owns what. Frontend ergonomics. Normalized client caches, like those in Relay and Apollo Client, keep UI state consistent when the same entity appears in many places. Where GraphQL hurts Caching gets harder. Most requests are POSTs to a single URL, so you lose default HTTP caching. Persisted queries, GET for queries, and normalized client caches recover much of it, but it is work. N+1 and unbounded cost. Because the client controls the query, one request can fan out into thousands of database calls or demand a deeply nested graph. You need DataLoader-style batching, depth and complexity limits, and cost-aware rate limiting. Plan for this before launch rather than after an incident. Authorization is per field, not per endpoint. That is more flexible and also gives you more places to forget a check. Error handling is unconventional. A response can be HTTP 200 and still carry errors, which surprises monitoring tools built around status codes. Operational and learning overhead. Schema design, resolvers, and tooling take real time to learn well. The sharpest summary: GraphQL moves complexity from the client to the server. That is a good trade when you have many clients and a platform team, and a poor one for a small app with a single frontend. tRPC in depth tRPC is less a protocol than a way to call server functions from a client with end-to-end type safety. You define procedures (queries, mutations, subscriptions) on a router, validate inputs with a library such as Zod, and the client infers everything from the router’s type. Where tRPC wins Zero-friction type safety. No schema language, no codegen, no sync step. Changes propagate in your editor instantly. Developer velocity. Adding an endpoint means writing a function. Refactors are safe because the compiler finds every call site. Runtime validation built in. Input schemas double as validation and as the source of types, so your types and your runtime checks cannot disagree. A natural fit for modern full-stack TypeScript. It pairs well with TanStack Query and with frameworks like Next.js. Where tRPC hurts TypeScript on both ends. If you have a Swift, Kotlin, Go, or Python consumer, tRPC does not help them directly. Not a public API. There is no standard, language-neutral contract for outsiders to depend on. The tRPC project now ships an OpenAPI generator (currently in alpha), and community packages such as trpc-to-openapi expose REST-style endpoints, which can bridge the gap. But once you are maintaining an external contract, ask whether REST should have been the surface from the start. Deploy skew. Types are only checked at build time. If an old client (a cached web bundle, or a mobile app) calls a newer server, the compiler cannot save you. tRPC shines when client and server ship together. Size of the type graph. Very large routers can slow editor and compiler performance, which teams typically address by splitting routers. Non-JSON types. Dates, Maps, and BigInts need a transformer such as superjson. Head-to-head Performance. All three are fast enough for most apps, and the bottleneck is almost always the database. The meaningful differences are round trips and payload size. GraphQL and tRPC (with request batching) can collapse many calls into one. REST can match that with well-designed composite endpoints or HTTP/2 multiplexing. Type safety. tRPC infers types, but only inside TypeScript. GraphQL and OpenAPI-based REST generate them from a contract, in any language. Versioning and evolution. GraphQL’s additive model with @deprecated fields is purpose-built for evolving without versions. REST typically versions through URLs or headers, and with care you can evolve it additively too. tRPC has no built-in versioning story. Security. Each has its own failure mode. REST problems tend to be missing authorization on individual endpoints. GraphQL adds field-level authorization on top of the query abuse covered above. tRPC procedures are real HTTP endpoints that anyone can call directly, so treat every procedure as public input: validate everything and enforce auth in middleware. Real-time. GraphQL has subscriptions in the spec. tRPC supports subscriptions too, over WebSockets or server-sent events. REST pairs with SSE, WebSockets, or webhooks. None of these is a real differentiator. Team and hiring. REST is understood by nearly everyone. GraphQL has a steeper learning curve but a large ecosystem. tRPC is easy for TypeScript developers but smaller and more opinionated. A decision framework Start with your consumers, then your stack, then your data shape. flowchart TD A[\"Who consumes your API?\"] --&gt;|\"Third parties or the public\"| R[\"REST with OpenAPI\"] A --&gt;|\"Only clients you build\"| B[\"Is every client TypeScript?\"] B --&gt;|\"Yes\"| C[\"Do many clients need very different data shapes?\"] B --&gt;|\"No\"| D[\"Do many clients need very different data shapes?\"] C --&gt;|\"No\"| T[\"tRPC\"] C --&gt;|\"Yes\"| G[\"GraphQL\"] D --&gt;|\"Yes\"| G D --&gt;|\"No\"| R2[\"REST with generated clients\"]And the rules of thumb behind it: Public or partner-facing API? Choose REST. A single TypeScript app (Next.js, a monorepo, a web app plus a TypeScript backend)? Choose tRPC. Several clients (web, iOS, Android, partners) with genuinely different data needs, or many teams contributing to one product surface? Choose GraphQL. A mixed stack, a small team, and no strong reason to deviate? Choose REST with an OpenAPI spec and generated types. It is boring in the best way. You do not have to pick just one The strongest architectures are often hybrids. tRPC internally, REST externally. Use tRPC for your own web app, and expose a deliberate, versioned REST API for integrators. GraphQL as a backend-for-frontend. Keep your services speaking REST or gRPC, and put a GraphQL layer in front to shape data for clients. REST for edge-cached content, something richer for the app. Public, cacheable pages call REST endpoints, while the authenticated app uses tRPC or GraphQL. The key is a clear boundary. Each surface should have one audience and one contract. Three mistakes worth avoiding Choosing GraphQL to “avoid versioning” and then shipping breaking changes anyway. The additive model only works if you keep the discipline and track field usage. Calling it REST when it is really RPC over HTTP. There is nothing wrong with endpoints like POST /sendInvoice, but be honest about the style and keep semantics consistent. Optimizing the wrong bottleneck. If your pain is slow SQL, no API style will fix it. The verdict REST is the safest default and the best choice for anything others will build on. GraphQL earns its complexity only when many clients and teams need different views of the same data. tRPC is the fastest way to build a type-safe full-stack TypeScript app. Start from your consumers and your deploy model, and the right answer is usually obvious. Where it is not, pick the style whose failure mode you can live with: REST’s verbosity, GraphQL’s operational weight, or tRPC’s coupling.","contentHash":"sha256:5192636df85525e94a338cd7203492add78fd0e3974c726ae741f904d6cdab3b","authorName":"Anshu Pathak","authorUrl":"https://zyvop.com/author/anshu","authorSameAs":["https://www.linkedin.com/in/anshu-pathak-zyvop/"],"category":"Versus","tags":["API Design","rest","tRPC","graphql","TypeScript"],"audience":"Software engineers and developers building applications with Versus","tone":"Instructional, practical, code-first","readingTimeMinutes":9,"wordCount":2338,"faqs":null,"primaryTopic":"Versus","publishedAt":"2026-10-18T06:20:26.128Z","updatedAt":"2026-10-18T06:20:26.128Z","canonicalUrl":"https://zyvop.com/rest-vs-graphql-vs-trpc-how-to-choose-an-api-style-you-won-t-regret-bemt8"}