gRPC vs REST vs GraphQL: Choosing the Right API
You need an API. Three technologies promise different wins. Only one fits your job.
The industry will tell you gRPC is faster (true, but irrelevant if you’re not bottle-necked). GraphQL is more flexible (true, but overkill for many teams). REST is boring (true, and sometimes boring is winning).
This is how to actually choose.
REST: Simple, HTTP/1.1, Boring and Proven
REST is the default because it works:
// Express.js - REST APIimport express from "express";
const app = express();
app.get("/users/:id", (req, res) => { const user = db.users.find(req.params.id); res.json(user);});
app.post("/users", (req, res) => { const user = db.users.create(req.body); res.status(201).json(user);});
app.listen(3000);Pros: ✔ Every client can consume it (curl, fetch, mobile, browser, IoT) ✔ HTTP semantics are understood by every engineer ✔ Caching is built-in (HTTP cache headers) ✔ Debugging is trivial (browser dev tools, curl) ✔ No code generation needed
Cons: ⚠️ Over-fetching (you get the whole user, even if you only need the name) ⚠️ Under-fetching (you need the user AND their posts, so multiple requests) ⚠️ Versioning is messy (/v1/users vs /v2/users) ⚠️ N+1 problem if you’re not careful (fetch user, then 100 posts in a loop)
Use REST if: ✔ You’re building a public API ✔ Your team knows HTTP well ✔ Clients are diverse (web, mobile, CLI, hardware) ✔ You don’t have 1000s of endpoints
GraphQL: Flexible Queries, Great for Multi-Client, Overkill for Many
GraphQL lets clients ask for exactly what they need:
# Client asks for user + their 3 most recent postsquery GetUserWithPosts { user(id: "123") { id name email posts(limit: 3) { id title createdAt } }}// Apollo Server - GraphQL APIimport { ApolloServer } from "@apollo/server";
const typeDefs = ` type User { id: ID! name: String! email: String! posts: [Post!]! }
type Post { id: ID! title: String! createdAt: DateTime! }
type Query { user(id: ID!): User }`;
const resolvers = { Query: { user: (_, { id }) => db.users.find(id), }, User: { posts: (user) => db.posts.where({ userId: user.id }), },};
const server = new ApolloServer({ typeDefs, resolvers });await server.start();Pros: ✔ No over-fetching (ask for what you need) ✔ No under-fetching (single request gets nested data) ✔ Self-documenting (introspection gives you the entire schema) ✔ Great for BFF (Backend for Frontend) with varying client needs
Cons: ⚠️ N+1 problem by default (if you fetch 100 users, that’s 100 queries for posts). You need DataLoader to batch. ⚠️ Caching is hard (every query is a POST, cache headers don’t work) ⚠️ Debugging is painful (GraphQL errors are nested and opaque) ⚠️ Query complexity can explode (client asks for nested data 10 levels deep) ⚠️ Requires code generation (schema → client types) ⚠️ Overkill for simple APIs (if you have 5 endpoints, REST is faster to build)
Use GraphQL if: ✔ You have multiple clients with wildly different data needs (web, mobile, iPad, embed) ✔ You’re willing to implement DataLoader or fragment caching ✔ Your team is comfortable with schema design ✔ You’re building a public API where over-fetching kills mobile users
gRPC: Fast, Binary, HTTP/2, Perfect for Internal Services
gRPC uses Protocol Buffers (binary) and HTTP/2 (multiplexing), making it 5-10x faster than REST:
// user.proto - Schema definitionsyntax = "proto3";
service UserService { rpc GetUser(GetUserRequest) returns (GetUserReply) {} rpc ListUsers(ListUsersRequest) returns (stream GetUserReply) {} rpc UpdateUser(User) returns (User) {}}
message GetUserRequest { string id = 1;}
message GetUserReply { string id = 1; string name = 2; string email = 3;}
message User { string id = 1; string name = 2; string email = 3;}// Node.js gRPC server (using @grpc/grpc-js)import grpc from "@grpc/grpc-js";import protoLoader from "@grpc/proto-loader";
const packageDef = protoLoader.loadSync("user.proto");const UserService = grpc.loadPackageDefinition(packageDef).user;
const server = new grpc.Server();
server.addService(UserService.UserService.service, { getUser: (call, callback) => { const user = db.users.find(call.request.id); callback(null, user); }, listUsers: (call) => { db.users.all().forEach(user => { call.write(user); }); call.end(); }, updateUser: (call, callback) => { const updated = db.users.update(call.request.id, call.request); callback(null, updated); },});
server.bindAsync("0.0.0.0:5000", grpc.ServerCredentials.createInsecure(), () => { server.start();});gRPC client (Node.js):
import grpc from "@grpc/grpc-js";import protoLoader from "@grpc/proto-loader";
const packageDef = protoLoader.loadSync("user.proto");const UserService = grpc.loadPackageDefinition(packageDef).user;
const client = new UserService.UserService( "localhost:5000", grpc.credentials.createInsecure());
// Single requestclient.getUser({ id: "123" }, (err, user) => { console.log(user);});
// Streamingconst stream = client.listUsers({});stream.on("data", (user) => { console.log(user);});| Feature | REST | gRPC |
|---|---|---|
| Payload size | 5KB (JSON) | 500B (binary) |
| Request latency (p50) | 45ms | 8ms |
| Throughput (req/s) | 2,000 | 15,000 |
| Time to implement | 10m | 30m |
| Client language support | All | Go, Java, Python, Node, Rust, C++, Ruby |
| Browser support | ✔ | ⚠️ (gRPC-Web) |
| Debugging | curl, browser | grpcurl (special tool) |
| HTTP version | HTTP/1.1 | HTTP/2 |
Pros: ✔ 5-10x faster than REST (binary payload, HTTP/2 multiplexing) ✔ Bi-directional streaming (server can push to client) ✔ Strongly typed (proto files generate code) ✔ Perfect for microservices (low latency inter-service calls)
Cons: ⚠️ Requires code generation (generate server stubs, client code) ⚠️ Harder to debug (binary format, not human-readable) ⚠️ No browser support (use gRPC-Web, which is slower) ⚠️ Your whole team needs to learn protobuf ⚠️ Limited ecosystem (fewer off-the-shelf middleware) ⚠️ Overkill for public APIs (gRPC-Web is just REST with overhead)
Use gRPC if: ✔ You’re building microservices (service-to-service is internal) ✔ You need bi-directional streaming (chat, notifications, live data) ✔ Latency matters (finance, trading, real-time features) ✔ You’re in an ecosystem with good gRPC support (Kubernetes, Go, Java services)
Decision Matrix: What to Pick
| Scenario | Pick | Why |
|---|---|---|
| Public API (SaaS) | REST | Every client can consume, caching works, easy to debug |
| Diverse internal clients | GraphQL | Mobile wants less data, web wants more, single endpoint |
| Microservices (internal) | gRPC | 5-10x faster, streaming, strongly typed |
| Mobile BFF | GraphQL + REST | GraphQL for mobile (exact data), REST for web (caching) |
| Simple CRUD app | REST | 5 endpoints, REST is built in 2 hours |
| Real-time updates | gRPC or GraphQL Subscriptions | gRPC streaming is faster, GraphQL subscriptions are easier |
| Public + internal | REST + gRPC | REST for clients, gRPC for internal services |
Real Example: When to Use Each
Your Uber competitor needs:
- Mobile app (data-limited, expensive bandwidth)
- Driver app (real-time location updates)
- Backend services (order processing, payment, notifications)
✔ REST for public driver/rider signup (external clients) ✔ GraphQL for mobile app (only request location + ETA, not 50 fields) ✔ gRPC for internal (service calls: payment → billing → notification are <10ms)
Mobile App ─(GraphQL)─> BFFDriver App ─(REST)────> API Gateway ↓ [Order Service] ─(gRPC)─> [Payment Service] ─(gRPC)─> [Notification Service]The Hard Truth
Most teams overthink this. Start with REST. It’s boring, it works, every engineer knows it.
Move to GraphQL when: ✔ Your mobile bill is high (over-fetching data) ✔ You have 3+ different client frontends ✔ Your REST API has 50+ endpoints
Move to gRPC when: ✔ Your service-to-service latency is a bottleneck ✔ You have streaming requirements ✔ Your team is ready for code generation and .proto files
If you’re building a monolith, REST wins every time. Add GraphQL or gRPC only when they solve a real problem, not when someone reads a blog post about them.
Summary
| Technology | Best For | Cost |
|---|---|---|
| REST | Public APIs, simplicity, browser clients | Low |
| GraphQL | Multi-client flexibility, mobile efficiency | Medium (N+1 issues, caching) |
| gRPC | Internal services, speed, streaming | Medium (code gen, debugging) |
Pick REST unless you have a specific reason not to. Microservices? gRPC. Multi-client app? GraphQL. Everything else? REST.
Don’t let optimization theater fool you. Solve the problem in front of you with the simplest tool that works. You can always upgrade later.