Every new project faces the same question: how should services communicate? REST is the default choice, but GraphQL and gRPC exist for good reasons. Picking the wrong protocol means either over-fetching data across slow mobile connections or wrestling with complex schemas for a simple CRUD API.
This guide gives you the knowledge to make the right choice for each situation, with real examples and honest tradeoffs.
REST: The Universal Standard
REST (Representational State Transfer) uses HTTP verbs and resource-based URLs. It is the most widely understood API style and the right default for most web applications.
# REST API design for a blog platform
# Resources and endpoints:
GET /api/posts # List all posts
GET /api/posts/42 # Get post by ID
POST /api/posts # Create a new post
PUT /api/posts/42 # Replace entire post
PATCH /api/posts/42 # Partial update
DELETE /api/posts/42 # Delete post
# Nested resources:
GET /api/posts/42/comments # Comments on post 42
POST /api/posts/42/comments # Add comment to post 42
# Filtering, sorting, pagination:
GET /api/posts?status=published&sort=-created_at&page=2&limit=20
REST Response Design
// GET /api/posts/42
{
"id": 42,
"title": "Database Indexing Secrets",
"slug": "database-indexing-secrets",
"content": "...",
"author": {
"id": 1,
"name": "Vishal Anand",
"avatar_url": "/images/avatar.jpg"
},
"tags": ["database", "postgresql"],
"created_at": "2026-04-27T10:00:00Z",
"updated_at": "2026-04-27T12:30:00Z",
"_links": {
"self": "/api/posts/42",
"comments": "/api/posts/42/comments",
"author": "/api/users/1"
}
}
REST Best Practices
- Use nouns for resources, not verbs:
/api/postsnot/api/getPosts - Use HTTP status codes correctly: 201 for created, 204 for no content, 404 for not found, 422 for validation errors
- Use plural resource names:
/api/postsnot/api/post - Support filtering and pagination on collection endpoints
- Use HATEOAS links to help clients discover related resources
- Version your API:
/api/v1/postsorAccept: application/vnd.api.v1+json
REST Limitations
- Over-fetching: GET /api/posts returns all fields even if you only need titles
- Under-fetching: To show a post with author details and comments, you need 3 separate requests
- No standard query language: Filtering syntax varies between every API
GraphQL: Ask for Exactly What You Need
GraphQL lets clients specify exactly which fields they want in a single request. The server returns precisely that shape — nothing more, nothing less.
Schema Definition
# GraphQL schema
type Post {
id: ID!
title: String!
slug: String!
content: String!
excerpt: String
author: User!
comments: [Comment!]!
tags: [String!]!
createdAt: DateTime!
}
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Comment {
id: ID!
body: String!
author: User!
createdAt: DateTime!
}
type Query {
post(id: ID!): Post
posts(status: PostStatus, limit: Int, offset: Int): [Post!]!
user(id: ID!): User
}
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
}
Client Queries
# Get exactly what the homepage needs (one request)
query HomePosts {
posts(status: PUBLISHED, limit: 10) {
id
title
slug
excerpt
author {
name
avatar_url
}
tags
createdAt
}
}
# Get a single post with comments (one request instead of three)
query PostDetail {
post(id: "42") {
title
content
author {
name
bio
}
comments {
body
author {
name
}
createdAt
}
}
}
GraphQL Best Practices
- Use DataLoader to batch and cache database queries (solves the N+1 problem)
- Limit query depth and complexity to prevent abuse (malicious nested queries)
- Use persisted queries in production to prevent arbitrary query injection
- Paginate with cursor-based pagination (Relay-style connections) for large datasets
GraphQL Limitations
- Caching is harder: No URL-based caching (every request is POST to /graphql)
- Complexity on the server: Resolver chains, N+1 queries, authorization per field
- File uploads require workarounds: GraphQL is text-based, no native file support
- Overkill for simple APIs: If you have 5 endpoints with fixed shapes, REST is simpler
gRPC: High-Performance Service Communication
gRPC uses Protocol Buffers (binary serialization) over HTTP/2. It is designed for service-to-service communication where performance and type safety matter more than human readability.
Protocol Buffer Schema
// post.proto
syntax = "proto3";
package blog;
service PostService {
rpc GetPost(GetPostRequest) returns (Post);
rpc ListPosts(ListPostsRequest) returns (ListPostsResponse);
rpc CreatePost(CreatePostRequest) returns (Post);
rpc StreamUpdates(StreamRequest) returns (stream PostUpdate);
}
message Post {
string id = 1;
string title = 2;
string content = 3;
User author = 4;
repeated string tags = 5;
google.protobuf.Timestamp created_at = 6;
}
message User {
string id = 1;
string name = 2;
string email = 3;
}
message GetPostRequest {
string id = 1;
}
message ListPostsRequest {
int32 page_size = 1;
string page_token = 2;
string status_filter = 3;
}
Python gRPC Server
import grpc
from concurrent import futures
import post_pb2
import post_pb2_grpc
class PostServicer(post_pb2_grpc.PostServiceServicer):
def GetPost(self, request, context):
post = db.get_post(request.id)
if not post:
context.abort(grpc.StatusCode.NOT_FOUND, "Post not found")
return post_pb2.Post(
id=post.id,
title=post.title,
content=post.content,
)
def StreamUpdates(self, request, context):
"""Server-side streaming: push updates in real-time"""
for update in event_bus.subscribe("post_updates"):
yield post_pb2.PostUpdate(
post_id=update.id,
action=update.action,
)
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
post_pb2_grpc.add_PostServiceServicer_to_server(PostServicer(), server)
server.add_insecure_port('[::]:50051')
server.start()
gRPC Advantages
- 10x faster serialization than JSON (binary Protocol Buffers)
- HTTP/2: multiplexing, header compression, bidirectional streaming
- Strong typing: Code generation from .proto files prevents contract mismatches
- Streaming: Server streaming, client streaming, and bidirectional streaming built in
gRPC Limitations
- Not browser-friendly: Requires gRPC-Web proxy for browser clients
- Not human-readable: Binary format makes debugging harder (use grpcurl for CLI testing)
- Schema evolution is rigid: Adding required fields can break backward compatibility
- Smaller ecosystem: Fewer tools, tutorials, and community resources than REST
Head-to-Head Comparison
| Feature | REST | GraphQL | gRPC |
|---|---|---|---|
| Data Format | JSON (text) | JSON (text) | Protobuf (binary) |
| Transport | HTTP/1.1 or HTTP/2 | HTTP/1.1 or HTTP/2 | HTTP/2 only |
| Browser Support | Native | Native | Via gRPC-Web proxy |
| Caching | HTTP caching (CDN, browser) | Complex (no URL-based cache) | No HTTP caching |
| Type Safety | OpenAPI/Swagger (optional) | Schema (built-in) | Protobuf (built-in, strict) |
| Streaming | SSE (server only) | Subscriptions (via WebSocket) | Bidirectional (native) |
| Payload Size | Large (verbose JSON) | Medium (only requested fields) | Small (binary encoding) |
| Learning Curve | Low | Medium | High |
| Best For | Public APIs, web apps | Mobile apps, complex UIs | Microservices, internal APIs |
API Versioning Strategies
# REST: URL versioning (most common)
GET /api/v1/posts
GET /api/v2/posts # Breaking change? New version
# REST: Header versioning
GET /api/posts
Accept: application/vnd.myapi.v2+json
# GraphQL: No versioning needed!
# Deprecate fields instead of creating new versions
type Post {
title: String!
headline: String! @deprecated(reason: "Use 'title' instead")
}
# gRPC: Package versioning in .proto
package blog.v1; # Original
package blog.v2; # Breaking changes
Decision Framework
- Building a public API? → REST (universal, cacheable, well-understood)
- Mobile app with complex data needs? → GraphQL (request exactly what you need, save bandwidth)
- Microservices talking to each other? → gRPC (fast, typed, streaming)
- Real-time bidirectional communication? → gRPC or WebSocket
- Simple CRUD with 5-10 endpoints? → REST (do not over-engineer)
- Dashboard with many data sources? → GraphQL (aggregate data from multiple services in one query)
- Need maximum performance between services? → gRPC (binary protocol, HTTP/2 multiplexing)
Key Takeaways
- REST is the right default for most web APIs — simple, cacheable, universally supported
- GraphQL shines when clients have varied data needs — mobile vs. desktop, different pages needing different fields
- gRPC is for service-to-service — when performance and type safety matter more than human readability
- You can mix protocols: REST for public API, gRPC between microservices, GraphQL for your mobile app
- Do not pick based on hype — pick based on your actual constraints (client diversity, performance needs, team expertise)
- Good API design matters more than protocol choice — a well-designed REST API beats a poorly designed GraphQL API every time
The best API is the one your consumers can understand and use efficiently. For most teams building web applications, REST gets you 90% of the way. Add GraphQL or gRPC when you have a specific problem they solve better — not because they are trendy.