REST vs GraphQL vs tRPC: Picking Your API Approach
Compare REST vs GraphQL vs tRPC on team boundaries, client diversity, and type safety to pick the right API design for your next project.

Teams pick API styles for the wrong reasons. REST gets picked because it's familiar. GraphQL gets picked because it felt impressive at a conference. tRPC gets picked because a popular Next.js starter included it. None of those are good reasons. The decision should come down to three concrete questions: who owns the clients, how many different clients need the same data, and how much do you value end-to-end type safety? The wrong choice on REST vs GraphQL vs tRPC won't kill your project, but it will accumulate friction that compounds over months.
We've built enough APIs at Laxaar to have opinions here. Each style solves a real problem, and each one carries a real cost. This post walks through what each actually does well, where each breaks down, and which signals should push you toward one over the others.
What you'll learn
- What REST, GraphQL, and tRPC actually are
- The team boundary question
- Client diversity and over-fetching
- Type safety end to end
- Performance and caching realities
- Side-by-side comparison
- When to pick which one
- Frequently Asked Questions
What REST, GraphQL, and tRPC actually are
REST is an architectural style for HTTP APIs where resources live at URLs, and standard HTTP verbs (GET, POST, PUT, DELETE) express intent. It's not a protocol or a spec. It's a set of constraints. Most APIs called "REST" are actually just JSON-over-HTTP with some URL conventions. That's fine. The result is an API any HTTP client can talk to, with caching that works out of the box.
GraphQL is a query language and runtime. Clients describe the exact shape of data they want in a typed query, and the server returns exactly that shape. A single /graphql endpoint replaces dozens of REST endpoints. Facebook built it to solve a specific problem: mobile clients on slow connections that couldn't afford to fetch fields they didn't need.
tRPC is a TypeScript-first RPC library. You define server-side functions (called procedures) with Zod schemas, and the client calls them as if they were local async functions. No code generation step, no schema file, no separate type definition. The TypeScript types flow directly from server to client through inference. It requires TypeScript on both ends.
The team boundary question
This is the factor most comparisons skip. Who owns the API, and who owns the clients?
If your backend and frontend are owned by separate teams (or separate companies), REST or GraphQL is the right answer. Both produce a contract that can be versioned, documented, and consumed by teams that don't share a repo. A third-party integrator can read your REST docs or introspect your GraphQL schema without touching your server code.
tRPC collapses that boundary by design. The client and server share a TypeScript monorepo, and the types are inferred directly. That's a superpower inside a single full-stack team, but it's essentially unusable when the client is a mobile team, a partner, or a public developer audience. You can't ask a Python backend to consume a tRPC router. You can ask it to hit a REST endpoint or send a GraphQL query.
Our take at Laxaar: the team boundary test eliminates tRPC from consideration for any public or partner-facing API. It belongs on internal full-stack TypeScript products where one team owns the whole stack.
Client diversity and over-fetching
GraphQL was born from the over-fetching problem. A REST endpoint for /users/:id returns everything about a user regardless of whether the client needs two fields or twenty. On desktop that's usually fine. On a mobile client on a 3G connection downloading a list of 100 users, it matters.
GraphQL lets every client ask for exactly the fields it needs. A mobile screen asking for a user's name and avatar sends a tight query. A desktop admin panel asking for address, billing history, and settings sends a different query against the same type system. One endpoint, one schema, multiple clients each getting their own shape.
REST handles this with sparse fieldsets (?fields=name,avatar) or by designing separate endpoints for different client needs: the Backend for Frontend (BFF) pattern. Both work. Neither is as clean as GraphQL's native solution.
tRPC doesn't have a native answer to client diversity because it's designed for one TypeScript client. If you need an iOS app, an Android app, and a web app to all consume the same API, tRPC's tight coupling becomes a constraint, not a feature.
The honest trade-off: GraphQL's query flexibility shifts complexity to the client. Junior frontend teams often struggle to write efficient queries, leading to N+1 patterns on the server side unless you add a DataLoader layer. REST's fixed endpoints are simpler to reason about and easier to cache.
Type safety end to end
Type safety across an API boundary is hard. REST has OpenAPI/Swagger for schema definition, and tools like openapi-typescript to generate TypeScript types from those specs. The problem is drift: the spec and the actual implementation get out of sync, and the generated types become stale lies.
GraphQL does better. The schema is a first-class runtime artifact, not a documentation afterthought. graphql-code-generator can watch your schema and generate TypeScript types for every query and mutation. Schema changes break generated types at build time. That's real safety, but it requires a codegen step in your build pipeline and discipline to keep queries in .graphql files rather than inline strings.
tRPC wins on type safety for the use cases it covers, and it wins cleanly. No generation step. You define a router with Zod schemas, and the client gets fully inferred TypeScript types for every procedure's input and output. Change a return type on the server and the client's TypeScript breaks at the call site before you ship anything. That's the best developer experience in this category, with one asterisk: you have to be in a TypeScript monorepo.
// Server: define a procedure with Zod validation
export const appRouter = router({
user: router({
byId: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return db.user.findUnique({ where: { id: input.id } });
}),
}),
});
// Client: fully typed, no codegen needed
const user = await trpc.user.byId.query({ id: "123" });
// user is typed as the exact return type of the query
The code above works only because client and server share the same TypeScript project. That's tRPC's core constraint and its core strength.
Performance and caching realities
HTTP caching is REST's underrated advantage. GET requests are cacheable at the network layer — CDN, reverse proxy, browser — with no application-level work. A well-designed REST API benefits from this for free.
GraphQL typically sends POST requests (even for reads), which CDNs don't cache by default. Persisted queries and GET-based query execution can recover some of this, but they add configuration overhead. Apollo and Relay have sophisticated client-side caches, but they're application-level caches that don't replace network-level caching.
tRPC uses HTTP under the hood (queries map to GET, mutations to POST), so it gets HTTP caching for query procedures. React Query integration is first-class, so client-side caching is well-handled. It's not as pluggable as a dedicated GraphQL client cache for complex relational data, but for most applications it's more than adequate.
For high-traffic public APIs where CDN cache hit rate affects cost, REST's caching story is a real advantage. For internal product APIs with authenticated requests where caches are bypassed anyway, the difference shrinks.
Side-by-side comparison
| Criterion | REST | GraphQL | tRPC |
|---|---|---|---|
| External/public API | Excellent | Good | Not suitable |
| Third-party clients | Excellent | Good | Not suitable |
| Mobile + web + other clients | Good (BFF pattern) | Excellent | No |
| End-to-end type safety | Possible (codegen) | Good (codegen) | Excellent (native) |
| Over-fetching control | Manual | Native | N/A |
| HTTP caching | Excellent | Limited | Good |
| Learning curve | Low | Medium | Low (TypeScript teams) |
| Tooling maturity | Excellent | Excellent | Growing |
| Full-stack TypeScript monorepo | Good | Good | Excellent |
When to pick which one
Pick REST when you're building a public API, a partner integration surface, or an API that non-TypeScript clients need to consume. It's the default that works everywhere, caches well, and any developer can figure out from a URL and HTTP verb.
Pick GraphQL when you have multiple client types (web, mobile, third-party) that each need different data shapes from the same domain model. The query flexibility pays off when client diversity is real, not hypothetical. Be prepared for the DataLoader, codegen, and schema governance overhead. It's worth it at scale, but it's not free.
Pick tRPC when you're building a full-stack TypeScript application where one team owns both client and server, and you want the best possible developer experience with zero codegen. It fits Next.js, Remix, or any TypeScript monorepo perfectly. Our web development services team defaults to tRPC for internal product APIs on TypeScript stacks precisely because the type safety removes an entire category of runtime bugs.
The less-discussed hybrid worth knowing: many production systems use all three. A public REST API for external partners, GraphQL for the web and mobile product, and tRPC for internal service-to-service calls within a TypeScript backend. Choosing one for your whole system is a simplification that fits most teams, but don't assume the choice has to be permanent or universal.
If you're starting a new SaaS product or MVP and your team is TypeScript-native, tRPC is worth serious consideration. The friction of adding GraphQL schema governance and codegen to an early-stage product is often a poor use of iteration time.
Frequently Asked Questions
Can you mix REST and GraphQL in the same project?
Yes, and many production systems do. A common pattern is to keep REST for webhooks, file uploads, and third-party integrations while using GraphQL for the main product API. The two styles don't conflict; they're just different route handlers on the same server.
Does GraphQL always replace multiple REST endpoints?
Not always. Simple CRUD resources with a single client are often cleaner as REST. GraphQL earns its complexity when clients have genuinely different data shape needs. If your web and mobile apps always need the same data, GraphQL's flexibility is overhead you're paying for without benefit.
Is tRPC production-ready?
Yes. tRPC v11 is used in production by many teams and powers real products at significant scale. The ecosystem around it (React Query integration, Next.js adapter, Zod validation) is mature. The main risk isn't stability. It's lock-in to a TypeScript-only stack, which is a legitimate constraint for some teams.
How does tRPC handle versioning?
It doesn't have a built-in versioning mechanism, because it's designed for internal use where you control both ends. If you need to support multiple versions of a client simultaneously (common for mobile apps that can't be force-updated), tRPC's tight coupling becomes a liability. REST or GraphQL with explicit versioning or schema evolution policies handles this better.
Can GraphQL subscriptions replace WebSockets?
GraphQL subscriptions run over WebSockets and provide real-time push semantics similar to raw WebSockets. The benefit is that subscriptions follow the same type system as your queries. The trade-off is added infrastructure complexity (you need a WebSocket-capable server and subscription transport). For simple real-time needs, server-sent events over REST are often simpler.
The right API style is the one that matches your actual team structure and client diversity, not the one with the most GitHub stars this year. If you're unsure which approach fits your project, the Laxaar team is happy to work through the architecture with you. Reach out through our API development services page or get a quote for your next build.
Working on something like this?
Get a fixed scope, timeline, and price within one business day — no obligation.


