The Runtime Theory
Backend Engineering

REST vs gRPC vs GraphQL: Choosing Your API

The real tradeoffs between API protocols: serialization overhead, streaming, type safety, and tooling for each approach.

The Runtime Theory Team12 min read#rest#grpc#graphql#api-design#protocol#protobuf
On this page

REST, gRPC, and GraphQL solve different problems. REST is the universal standard. gRPC optimizes for performance and type safety. GraphQL optimizes for client flexibility. The right choice depends on your team, performance requirements, and client diversity.

REST: The Universal Standard

REST uses HTTP methods with resource-oriented URLs and JSON serialization:

python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
 
app = FastAPI()
 
class User(BaseModel):
    id: int
    name: str
    email: str
 
@app.get("/users/{user_id}", response_model=User)
async def get_user(user_id: int):
    user = await db.fetch_user(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user
 
@app.post("/users", response_model=User, status_code=201)
async def create_user(user: User):
    return await db.create_user(user)
 
# REST advantages:
# - Universal: every language, framework, and tool supports it
# - Cacheable: HTTP caching (ETags, Cache-Control) works automatically
# - Browser-native: works without client libraries
# - Simple: curl for debugging, OpenAPI for documentation
json
// REST response — includes all fields, even if client doesn't need them
{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com",
  "created_at": "2026-01-15T10:30:00Z",
  "profile": {
    "avatar_url": "https://cdn.example.com/avatars/123.jpg",
    "bio": "Software engineer",
    "settings": { ... }
  },
  "organizations": [ ... ],
  "recent_activity": [ ... ]
}
// Problem: over-fetching (client only needs name and email)
// Problem: under-fetching (need separate call for profile)

gRPC: Performance and Type Safety

gRPC uses Protocol Buffers for serialization and HTTP/2 for transport:

protobuf
// user.proto
syntax = "proto3";
package users;
 
service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc CreateUser (CreateUserRequest) returns (User);
  rpc ListUsers (ListUsersRequest) returns (stream User);
}
 
message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  google.protobuf.Timestamp created_at = 4;
}
 
message GetUserRequest {
  int32 id = 1;
}
go
// Server implementation (Go)
type userServer struct {
    pb.UnimplementedUserServiceServer
}
 
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    user, err := s.db.GetUser(req.Id)
    if err != nil {
        return nil, status.Errorf(codes.NotFound, "user %d not found", req.Id)
    }
    return &pb.User{
        Id:    int32(user.ID),
        Name:  user.Name,
        Email: user.Email,
    }, nil
}
 
// Streaming: server streams results as they're found
func (s *userServer) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
    users, err := s.db.ListUsers(req.Filter)
    if err != nil {
        return err
    }
    for _, user := range users {
        if err := stream.Send(&pb.User{
            Id:    int32(user.ID),
            Name:  user.Name,
            Email: user.Email,
        }); err != nil {
            return err
        }
    }
    return nil
}

GraphQL: Client-Driven Data Fetching

GraphQL lets clients specify exactly what data they need:

graphql
type Query {
  user(id: ID!): User
  users(filter: UserFilter): [User!]!
}
 
type User {
  id: ID!
  name: String!
  email: String!
  profile: Profile!
  organizations: [Organization!]!
}
 
# Client request: only fetch what's needed
query {
  user(id: 123) {
    name
    email
    profile {
      avatarUrl
    }
  }
}
 
# Response: exactly what was requested
{
  "data": {
    "user": {
      "name": "Alice",
      "email": "alice@example.com",
      "profile": {
        "avatarUrl": "https://cdn.example.com/avatars/123.jpg"
      }
    }
  }
}
python
# GraphQL server (Python with Strawberry)
import strawberry
from typing import Optional
 
@strawberry.type
class User:
    id: int
    name: str
    email: str
 
@strawberry.type
class Query:
    @strawberry.field
    async def user(self, id: int) -> Optional[User]:
        return await db.fetch_user(id)
 
    @strawberry.field
    async def users(self, limit: int = 10) -> list[User]:
        return await db.list_users(limit)
 
schema = strawberry.Schema(query=Query)
 
# N+1 problem: each user triggers separate DB query
# Solution: DataLoader batches queries per request
from strawberry.dataloader import DataLoader
 
async def load_users(ids: list[int]) -> list[User]:
    return await db.get_users_by_ids(ids)
 
user_loader = DataLoader(load_fn=load_users)

Performance Comparison

python
# Benchmark: 1000 requests for user profile with 5 related entities
#
# REST (3 calls needed: /users/123, /users/123/profile, /users/123/orgs):
#   Avg latency: 45ms (3 round trips)
#   Payload: 2.1 KB
#   Server CPU: 100% baseline
#
# gRPC (1 call, all fields):
#   Avg latency: 8ms (1 round trip, binary)
#   Payload: 0.2 KB (protobuf)
#   Server CPU: 60% (less serialization)
#
# GraphQL (1 call, specified fields):
#   Avg latency: 12ms (1 round trip, JSON)
#   Payload: 0.4 KB (only requested fields)
#   Server CPU: 80% (query planning overhead)
 
# For mobile on 3G:
# REST: ~300ms (3 round trips at 100ms each)
# gRPC: ~120ms (1 round trip, small payload)
# GraphQL: ~150ms (1 round trip, moderate payload)

tradeoff / REST vs gRPC vs GraphQL

For most teams, REST is the right default. Add gRPC for performance-critical internal communication. Add GraphQL when client diversity creates over/under-fetching problems. Don't use all three for the same API.

Use REST for public APIs (universal compatibility). Use gRPC for internal service-to-service (performance). Use GraphQL for client-facing APIs with diverse data needs (mobile + web from same backend).

Synthesis

REST, gRPC, and GraphQL optimize for different constraints. REST maximizes interoperability. gRPC maximizes performance and type safety. GraphQL maximizes client flexibility. The best choice depends on your client types, performance requirements, and team expertise. Most production systems use REST for external APIs and gRPC for internal communication.