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/posts not /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/posts not /api/post
  • Support filtering and pagination on collection endpoints
  • Use HATEOAS links to help clients discover related resources
  • Version your API: /api/v1/posts or Accept: 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.