gRPC vs REST vs GraphQL — Pick the Right Protocol
REST = resource-based HTTP. gRPC = contract-first binary RPC. GraphQL = client-driven query API.
When to use
- REST: public APIs, browser clients, simple CRUD
- gRPC: internal service-to-service (performance, typed contracts, streaming)
- GraphQL: flexible client needs, multiple frontends needing different shapes
Tradeoffs
- gRPC requires protobuf tooling and isn't browser-native
- GraphQL N+1 query problem requires dataloaders; REST suffers over/under-fetching
| Feature | REST | gRPC | GraphQL |
|---|---|---|---|
| Protocol | HTTP/1.1 or HTTP/2 | HTTP/2 | HTTP/1.1 or HTTP/2 |
| Schema | OpenAPI (optional) | Protobuf (required) | SDL (required) |
| Browser support | Native | Requires grpc-web proxy | Native |
| Streaming | SSE / WebSocket | Bidirectional native | Subscriptions |
| Best for | Public APIs, CRUD | Internal services, performance | Flexible client queries |
- Go
- Python
// REST handler
func GetUserREST(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
user := userRepo.Find(id)
json.NewEncoder(w).Encode(user)
}
// gRPC handler (proto: rpc GetUser(UserRequest) returns (UserResponse))
func (s *Server) GetUser(ctx context.Context, req *pb.UserRequest) (*pb.UserResponse, error) {
user := s.repo.Find(req.Id)
return &pb.UserResponse{Id: user.ID, Name: user.Name}, nil
}
// GraphQL resolver
func (r *Resolver) User(ctx context.Context, args struct{ ID string }) (*UserResolver, error) {
user := r.repo.Find(args.ID)
return &UserResolver{user}, nil
}
# REST handler (Flask)
@app.get("/users/<user_id>")
def get_user_rest(user_id: str):
user = user_repo.find(user_id)
return jsonify(user)
# gRPC handler (proto: rpc GetUser(UserRequest) returns (UserResponse))
class UserService(pb2_grpc.UserServiceServicer):
def GetUser(self, request, context):
user = user_repo.find(request.id)
return pb2.UserResponse(id=user.id, name=user.name)
# GraphQL resolver (Strawberry)
@strawberry.type
class Query:
@strawberry.field
def user(self, id: str) -> User:
return user_repo.find(id)
Gotcha: gRPC streaming beats WebSockets for bidirectional service-to-service. But it's opaque to standard HTTP proxies and load balancers unless you configure L7 support.