Skip to main content
โšก Calmops

GraphQL vs REST vs tRPC: Choosing the Right API Paradigm

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