Skip to main content

GraphQL vs REST vs tRPC: Choosing the Right API Paradigm

Created: February 23, 2026 Larry Qu 6 min read

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

External Resources

REST

GraphQL

tRPC

Comments

Share this article

Scan to read on mobile