Introduction
Choosing the right API paradigm is one of the most important architectural decisions you’ll make. Each approachโREST, GraphQL, and tRPCโhas distinct strengths and trade-offs. This guide helps you understand when to use each.
Understanding the Three Paradigms
REST (Representational State Transfer)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ REST API โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Core Principles: โ
โ โข Resources identified by URLs โ
โ โข HTTP verbs (GET, POST, PUT, DELETE) โ
โ โข Stateless requests โ
โ โข Standardized response formats (JSON) โ
โ โ
โ Example: โ
โ GET /users/123 โ Get user 123 โ
โ POST /users โ Create new user โ
โ PUT /users/123 โ Update user 123 โ
โ DELETE /users/123 โ Delete user 123 โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
GraphQL
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ GraphQL โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Core Principles: โ
โ โข Single endpoint for all queries โ
โ โข Client requests exactly what they need โ
โ โข Strongly typed schema โ
โ โข Hierarchical data fetching โ
โ โ
โ Example: โ
โ POST /graphql โ
โ { โ
โ query { โ
โ user(id: "123") { โ
โ name โ
โ email โ
โ posts { title } โ
โ } โ
โ } โ
โ } โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
tRPC
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ tRPC โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Core Principles: โ
โ โข Type-safe API without code generation โ
โ โข Share TypeScript types between client and server โ
โ โข Zero-schema API definitions โ
โ โข Works seamlessly with Next.js, React โ
โ โ
โ Example: โ
โ // Client code (fully typed!) โ
โ const user = await trpc.user.getById.query("123") โ
โ // Knows exact return type without any setup โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Comparison Matrix
# Feature comparison
comparison:
data_fetching:
rest:
pattern: "Multiple endpoints"
overfetching: "Often"
underfetching: "Sometimes"
n_plus_1: "Common problem"
graphql:
pattern: "Single endpoint"
overfetching: "Minimal"
underfetching: "No"
n_plus_1: "Solved with dataloader"
trpc:
pattern: "Multiple procedures"
overfetching: "Minimal"
underfetching: "No"
n_plus_1: "Your responsibility"
typing:
rest:
types: "Manual or code gen"
shared: "Requires setup"
graphql:
types: "Code generation"
shared: "GraphQL Code Generator"
trpc:
types: "Automatic"
shared: "Native TypeScript"
learning_curve:
rest: "Low"
graphql: "Medium-High"
trpc: "Low-Medium"
caching:
rest: "HTTP caching"
graphql: "Client-side caching"
trpc: "React Query integration"
REST: When to Use
Best Use Cases
rest_scenarios:
- "Simple CRUD operations"
- "Resource-oriented data models"
- "When HTTP caching is important"
- "Team is unfamiliar with GraphQL"
- "Public APIs for external consumers"
- "Microservices with clear boundaries"
REST Example
// Express REST API
import express from 'express';
const app = express();
app.get('/api/users', async (req, res) => {
const users = await db.user.findMany();
res.json(users);
});
app.get('/api/users/:id', async (req, res) => {
const user = await db.user.findUnique({
where: { id: req.params.id }
});
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
app.post('/api/users', async (req, res) => {
const user = await db.user.create({
data: req.body
});
res.status(201).json(user);
});
GraphQL: When to Use
Best Use Cases
graphql_scenarios:
- "Complex data relationships"
- "Mobile apps with limited bandwidth"
- "Frontend-driven data requirements"
- "Rapid prototyping and iteration"
- "Aggregating multiple data sources"
- "When you need flexible queries"
GraphQL Example
// GraphQL with Apollo Server
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
const typeDefs = `
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
author: User!
}
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
createUser(name: String!, email: String!): User!
}
`;
const resolvers = {
Query: {
users: () => db.user.findMany(),
user: (_, { id }) => db.user.findUnique({ where: { id } }),
},
User: {
posts: (parent) => db.post.findMany({
where: { authorId: parent.id }
}),
},
Mutation: {
createUser: (_, args) => db.user.create({ data: args }),
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server);
GraphQL Query Examples
# Get user with posts (exactly what you need)
query GetUserWithPosts {
user(id: "123") {
name
email
posts {
title
}
}
}
# Multiple resources in one request
query DashboardData {
currentUser {
name
notifications {
message
}
}
trendingPosts {
title
likes
}
}
tRPC: When to Use
Best Use Cases
trpc_scenarios:
- "TypeScript/Next.js projects"
- "When type safety is critical"
- "Internal APIs (not public)"
- "Simple data requirements"
- "When you want zero boilerplate"
- "Full-stack TypeScript teams"
tRPC Example
// Server: Define router
// src/server/api/routers/user.ts
import { z } from 'zod';
import { createTRPCRouter, publicProcedure } from '../trpc';
export const userRouter = createTRPCRouter({
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input, ctx }) => {
return ctx.db.user.findUnique({
where: { id: input.id }
});
}),
getAll: publicProcedure.query(({ ctx }) => {
return ctx.db.user.findMany();
}),
create: publicProcedure
.input(z.object({
name: z.string().min(1),
email: z.string().email()
}))
.mutation(({ input, ctx }) => {
return ctx.db.user.create({
data: input
});
}),
});
// Client: Use with full type safety
// src/pages/users.tsx
import { api } from '@/utils/api';
export default function UsersPage() {
// Fully typed - IDE knows exact return type!
const { data: users, isLoading } = api.user.getAll.useQuery();
const createUser = api.user.create.useMutation();
if (isLoading) return <div>Loading...</div>;
return (
<div>
{users?.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
Decision Guide
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ API Choice Decision Tree โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Start with these questions: โ
โ โ
โ 1. Is this a public API? โ
โ YES โ REST or GraphQL (industry standard) โ
โ NO โ Continue โ
โ โ
โ 2. Using TypeScript + Next.js? โ
โ YES โ Consider tRPC (best DX) โ
โ NO โ Continue โ
โ โ
โ 3. Complex data relationships? โ
โ YES โ GraphQL โ
โ NO โ Continue โ
โ โ
โ 4. Simple CRUD operations? โ
โ YES โ REST โ
โ NO โ GraphQL or REST โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Performance Considerations
# Performance comparison
rest:
pros:
- "HTTP caching works out of the box"
- "Simple to cache at CDN level"
- "Stateless = easy to scale"
cons:
- "N+1 queries common"
- "Over-fetching common"
graphql:
pros:
- "Request exactly what's needed"
- "Dataloader solves N+1"
cons:
- "Complex queries can be slow"
- "Caching requires more setup"
trpc:
pros:
- "No over-fetching by design"
- "Uses React Query caching"
cons:
- "Not cached at HTTP level"
- "Best with internal APIs"
Key Takeaways
- REST - Best for public APIs, simple CRUD, when HTTP caching matters
- GraphQL - Best for complex data, mobile apps, flexible client needs
- tRPC - Best for TypeScript internal APIs, best developer experience
Comments