REST vs. GraphQL vs. tRPC: Picking the Right API Style
A deep-dive technical comparison of REST, GraphQL, and tRPC API styles, looking at schema enforcement, type-safety, and network payload sizes.
David K.
Senior Frontend Engineer
Choosing an API architectural style is one of the most critical design decisions in modern software engineering. The boundary between the frontend and the backend dictates compile-time type-safety, network payload efficiency, and developer velocity. While REST remains the baseline protocol for public services, GraphQL and tRPC have emerged as powerful alternatives for application-specific APIs. This article provides a deep-dive technical comparison of REST, GraphQL, and tRPC, examining their schema enforcement models, type-safety guarantees, and real-world network payload behaviors.
REST: The Resource-Based Industry Standard
Representational State Transfer (REST) has been the foundation of web services for over two decades. Built directly on top of standard HTTP specifications, REST represents the system state as resources identified by uniform resource identifiers (URIs). Clients interact with these resources using standard HTTP verbs: GET, POST, PUT, DELETE, and PATCH.
Advantages of Resource-Centric Boundaries
The primary advantage of REST is its absolute decoupling. Because REST relies on a universal protocol (HTTP) and standard media types (typically JSON), any system capable of sending an HTTP request can consume a REST API. This makes REST the default choice for public-facing third-party integrations. Additionally, REST leverages native web infrastructure, allowing CDNs, reverse proxies, and browsers to cache HTTP GET responses out of the box using standard headers like Cache-Control.
The Problem: Over-Fetching and Schema Drifts
However, REST has inherent limitations in complex client-side applications. Because endpoints return fixed data structures, clients frequently encounter over-fetching (receiving more data fields than the UI actually needs, wasting bandwidth) or under-fetching (requiring multiple round-trip requests to different endpoints to compile the data for a single screen). Furthermore, because REST does not enforce a strict compile-time contract between client and server, schema drifts are common—a backend engineer modifying a database field can easily break a frontend application without triggering compiler errors.
GraphQL: Client-Defined Queries and Schema Declarations
Developed by Facebook, GraphQL shifts control of the data contract from the server to the client. Rather than exposing multiple resource-based endpoints, a GraphQL server exposes a single endpoint (typically /graphql) and a strongly typed schema defined using the Schema Definition Language (SDL).
Eliminating Network Payload Waste
With GraphQL, the client specifies exactly what fields it requires in a single query document. The server parses this query, executes resolver functions to fetch only the requested data, and returns a JSON payload matching the query shape. This completely eliminates over-fetching and under-fetching. A mobile client on a slow connection can query only the user's name, while a desktop dashboard can query the user's entire purchase and setting history in a single network round-trip.
The Overhead: Schema Maintenance and Caching Complexity
This flexibility comes with significant costs. Because all requests are typically sent as HTTP POST payloads to a single endpoint, standard HTTP-level caching (like CDN caching) is impossible. Developers must implement complex client-side normalization caches (like Apollo Cache) or server-side persisted queries. Additionally, maintaining a unified GraphQL schema across a large enterprise requires substantial tooling and governance (such as GraphQL Federation), introducing architectural complexity.
tRPC: Zero-API Type Sharing in TypeScript Monorepos
tRPC (TypeScript Remote Procedure Call) represents a radical departure from both REST and GraphQL. Instead of defining a separate schema file or building API endpoints, tRPC allows developers to share TypeScript types directly between the backend server and frontend client, bypassing the API boundary entirely at compile time.
The Type-Safe Monorepo Advantage
tRPC is designed specifically for TypeScript monorepos (using tools like Turborepo, Nx, or yarn workspaces). In a tRPC setup, you write router procedures on the server using a library like Zod to validate inputs. The frontend then imports the type declarations of the server's router. The tRPC client uses these types to generate autocomplete paths and validate parameters inside the IDE. If you change a parameter name on the server, the frontend fails to compile instantly, guaranteeing absolute end-to-end type safety with zero code-generation steps.
Limit of Scope: The TypeScript Lock-In
The primary constraint of tRPC is that it is tightly coupled to the TypeScript ecosystem. If you decide to rewrite your frontend in Swift for iOS, or your backend in Go, you lose the benefits of tRPC. It is not suitable for public APIs, as external developers cannot import your live TypeScript compilation context. It is a highly optimized tool for internal application development within unified workspaces.
Deep Technical Comparison
| Feature | REST | GraphQL | tRPC |
|---|---|---|---|
| Schema Contract | Ad-hoc (or OpenAPI Specification Tooling/OpenAPI docs) | Strict Schema (SDL) file | TypeScript Server Router Types |
| Type Safety | Manual (or via generator tools) | Code-generated from SDL | Native Compile-time (Automatic) |
| Network Efficiency | Variable (Over-fetching common) | High (Client dictates payload) | High (RPC endpoints are precise) |
| HTTP Caching | Native (Browser/CDN level) | Complex (Application level) | Native (when using HTTP GET queries) |
| Ideal Use Case | Public APIs, microservices | Multi-client data hubs, mobile apps | Internal TypeScript web applications |
Code Implementation Comparison
To highlight the practical differences, let's look at how to implement a basic User query across all three architectures.
1. REST Implementation (Express)
// Express Server Endpoint
app.get('/api/users/:id', (req, res) => {
const user = db.getUser(req.params.id);
res.json(user); // Fixed payload shape
});
// Client-Side Fetch
const res = await fetch('/api/users/123');
const user = await res.json(); // Type is 'any' unless manually cast
2. GraphQL Implementation
# GraphQL Schema SDL
type User {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
}
# Client Query Document
query GetUser($id: ID!) {
user(id: $id) {
name # Client fetches ONLY name
}
}
3. tRPC Implementation
// Server Router (Express/Next.js)
const appRouter = router({
getUser: publicProcedure
.input(z.string())
.query(async ({ input }) => {
return db.getUser(input);
}),
});
export type AppRouter = typeof appRouter;
// Client Usage
const user = await trpc.getUser.query('123');
// 'user' is fully typed based on server database return value!
Frequently Asked Questions
Is tRPC limited only to TypeScript projects?
Yes. The core mechanism of tRPC relies on importing TypeScript types across client and server workspaces. If your frontend or backend is written in another programming language, you cannot leverage tRPC's automatic type safety, and you should use REST or GraphQL instead.
Does GraphQL completely replace REST for enterprise applications?
No. GraphQL is an orchestration layer. Many enterprise systems use REST APIs for internal microservice communication because of its simplicity and easy network caching, while placing a GraphQL gateway in front of client-facing applications to manage data querying efficiently.
How do the caching strategies differ between REST and GraphQL?
REST relies on standard HTTP caching headers (like ETag or Max-Age) which can be processed by default browser engines and global CDNs. GraphQL queries are typically sent as HTTP POST requests, which means CDNs cannot cache them out of the box; caching must be managed inside the application memory or via specialized API gateways.
Can I use tRPC for a public API that third-party developers will consume?
No, tRPC is not designed for public API consumption. Because it requires shared TypeScript compilation contexts, third-party developers using other languages or external build systems will not be able to resolve your types. For public consumption, export OpenAPI/OpenAPI Specification Tooling documents from your REST endpoint or expose a GraphQL schema.
Conclusion
The choice between REST, GraphQL, and tRPC is a trade-off between decoupling, client flexibility, and development speed. Use REST if you are building public APIs or decoupling microservices. Use GraphQL if you are designing complex client applications that run on multiple platforms with varying data needs. Choose tRPC if you are building an internal TypeScript web application within a monorepo, where speed and type-safety are your highest priorities.
Enjoyed this read?
Get monthly updates on privacy engineering and web performance straight to your inbox.