REST vs GraphQL vs tRPC — Full Comparison
Advertisement
Three major API patterns dominate backend development. Each has tradeoffs. Understanding them helps you choose the right tool.
REST (Representational State Transfer)
REST uses HTTP methods and status codes to manage resources:
// REST endpoints
GET /users // List users
GET /users/:id // Get user
POST /users // Create user
PUT /users/:id // Update user
DELETE /users/:id // Delete user
Pros:
- Simple, familiar, cacheable
- HTTP semantics are standard
- Easy to debug
- Great tooling
Cons:
- Over-fetching data
- Under-fetching requires multiple requests
- Versioning challenges
- No built-in type safety
// REST implementation
app.get("/users", (req, res) => {
res.json({ users: [] }); // Always returns all fields
});
app.get("/users/:id/posts", (req, res) => {
// Separate request needed for nested data
res.json({ posts: [] });
});
GraphQL
GraphQL is a query language. Clients request specific fields:
# GraphQL - client gets exactly what it asks for
query {
user(id: "1") {
name
email
# No posts unless requested
}
}
query {
user(id: "1") {
name
posts {
title
content
}
}
}
Pros:
- Request exactly what you need
- Single request for nested data
- Strongly typed schema
- Excellent developer tools
Cons:
- Complex to implement
- Query complexity challenges
- Caching harder than REST
- Steep learning curve
- REST (Representational State Transfer)
- GraphQL
- tRPC
- Comparison Table
- Performance Comparison
- When to Use Each
- Use REST When
- Use GraphQL When
- Use tRPC When
- Hybrid Approach
- Real-World Decision Matrix
- FAQ
tRPC
tRPC is a new paradigm: RPC over HTTP with TypeScript:
// tRPC - type-safe RPC
import { initTRPC } from "@trpc/server";
const t = initTRPC.create();
const router = t.router({
user: t.procedure.input(z.object({ id: z.string() })).query(async (opts) => {
return await getUser(opts.input.id);
}),
createUser: t.procedure
.input(z.object({ name: z.string(), email: z.string() }))
.mutation(async (opts) => {
return await db.users.create(opts.input);
}),
});
// Client - fully type-safe
const user = await trpc.user.query({ id: "1" });
// user: User (type is inferred from server)
Pros:
- End-to-end type safety
- Simple, familiar RPC model
- Excellent DX
- No code generation
- Small bundle size
Cons:
- Less well-known
- Smaller ecosystem
- Requires TypeScript
- Not REST semantics
Comparison Table
| Feature | REST | GraphQL | tRPC |
|---|---|---|---|
| Learning Curve | Easy | Moderate | Easy |
| Type Safety | No | Yes | Yes |
| Caching | Built-in | Hard | Hard |
| Over-fetching | Common | Solved | N/A |
| N+1 Problem | Common | DataLoader | N/A |
| Tooling | Good | Excellent | Good |
| Public APIs | Standard | Growing | Not recommended |
| Implementation | Simple | Complex | Simple |
Performance Comparison
REST: GraphQL: tRPC:
1 request 1 request 1 request
200KB data 50KB data 60KB data (inferred types only)
~20ms ~25ms ~20ms
When to Use Each
Use REST When
- Building public APIs
- Simple, predictable data structures
- Multiple client platforms
- API caching is critical
- Standard HTTP semantics matter
// Perfect REST use case
app.get("/posts", (req, res) => {
res.json(allPosts);
});
app.get("/posts/:id", (req, res) => {
res.json(getPost(req.params.id));
});
Use GraphQL When
- Complex data relationships
- Multiple client needs
- Rapid client changes
- Internal APIs
- Strong type requirements
# Perfect GraphQL use case
query UserDashboard {
user(id: "1") {
name
posts { title }
followers { name }
analytics { views clicks }
}
}
Use tRPC When
- Full-stack TypeScript
- Internal microservices
- Rapid development
- Type-safe is critical
- Small teams
// Perfect tRPC use case
export const router = t.router({
dashboard: t.procedure.query(async () => ({
user: await getUser(),
posts: await getPosts(),
analytics: await getAnalytics(),
})),
});
Hybrid Approach
Many use REST for public APIs and GraphQL/tRPC for internal services:
// Public REST API
app.get("/api/v1/users/:id", (req, res) => {
res.json(getPublicUserData(req.params.id));
});
// Internal GraphQL API
const internalRouter = new ApolloServer({
typeDefs: fullSchema,
resolvers,
});
Real-World Decision Matrix
Simple CRUD? → REST
Complex relationships? → GraphQL
Full-stack TypeScript? → tRPC
Public API? → REST
Internal only? → tRPC or GraphQL
Rapid changes? → GraphQL or tRPC
Performance critical? → REST
FAQ
Q: Can I use multiple APIs together? A: Yes! Public REST, internal tRPC/GraphQL is common.
Q: Which is most popular? A: REST by far. GraphQL growing. tRPC newer but gaining traction in TypeScript community.
Q: Can I migrate between them? A: Yes, but it's work. Choose wisely at the start.
REST, GraphQL, and tRPC each solve different problems. REST is simple and proven. GraphQL solves complex data queries. tRPC brings type safety to full-stack development. Choose based on your specific needs, not trends.
Advertisement