REST vs GraphQL vs tRPC — Full Comparison

Sanjeev SharmaSanjeev Sharma
4 min read

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

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

FeatureRESTGraphQLtRPC
Learning CurveEasyModerateEasy
Type SafetyNoYesYes
CachingBuilt-inHardHard
Over-fetchingCommonSolvedN/A
N+1 ProblemCommonDataLoaderN/A
ToolingGoodExcellentGood
Public APIsStandardGrowingNot recommended
ImplementationSimpleComplexSimple

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

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro