Loading content...
When designing service-to-service communication, two paradigms dominate: REST and gRPC. Both solve the same fundamental problem—enabling distributed systems to communicate—but they make radically different trade-offs.
REST (Representational State Transfer) embraces HTTP as an application protocol, uses human-readable formats like JSON, and prioritizes simplicity and universal compatibility. It's the foundation of the Web and public APIs.
gRPC treats HTTP as a transport layer, uses binary Protocol Buffers, and prioritizes performance and type safety. It's the choice for internal microservice communication at scale.
Understanding both paradigms—their philosophies, mechanics, and trade-offs—is essential for designing distributed systems. The choice isn't always obvious, and hybrid architectures that use both are common.
By the end of this page, you will understand REST's architectural principles and how they map to HTTP, gRPC's design goals and implementation, the concrete trade-offs between them, and guidelines for choosing the right approach for different scenarios.
REST was introduced by Roy Fielding in his 2000 doctoral dissertation as an architectural style for building distributed hypermedia systems. It is not a protocol or specification—it's a set of constraints that, when followed, yield desirable properties like scalability, simplicity, and modifiability.
The Six REST Constraints
Client-Server: Separation of concerns between user interface and data storage. Clients and servers evolve independently.
Stateless: Each request contains all information needed to process it. No client context is stored on the server between requests.
Cacheable: Responses explicitly indicate cacheability. Proper caching eliminates redundant requests, improving efficiency.
Uniform Interface: A standardized way to interact with resources. This is REST's most distinguishing feature.
Layered System: Intermediate layers (proxies, load balancers, caches) can be added without clients knowing.
Code on Demand (optional): Servers can extend client functionality by transferring executable code (e.g., JavaScript).
| Constraint | What It Means | Architectural Benefit |
|---|---|---|
| Stateless | No session state on server | Horizontal scaling, fault tolerance |
| Cacheable | Explicit cache controls | Reduced latency, lower server load |
| Uniform Interface | Standard resource operations | Simplicity, discoverability |
| Layered | Transparent intermediaries | CDNs, security layers, load balancing |
| Client-Server | Decoupled components | Independent evolution |
The Uniform Interface
REST's uniform interface has four components:
/users/123 # User resource
/users/123/orders # Orders for user 123
/products/456 # Product resource
Manipulation through representations: Clients work with representations (JSON, XML) of resources, not the resources themselves.
Self-descriptive messages: Each message contains enough information to describe how to process it (Content-Type, methods, etc.).
Hypermedia as the engine of application state (HATEOAS): Responses include links to related actions and resources.
The last principle—HATEOAS—is often ignored in practice, but it's what makes REST truly "RESTful" versus just "HTTP + JSON."
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// REST response with HATEOAS links// Client discovers available actions from the response itself { "id": 12345, "email": "user@example.com", "name": "Jane Smith", "status": "active", "created_at": "2024-01-15T10:30:00Z", "links": [ { "rel": "self", "href": "/users/12345", "method": "GET" }, { "rel": "update", "href": "/users/12345", "method": "PATCH" }, { "rel": "delete", "href": "/users/12345", "method": "DELETE" }, { "rel": "orders", "href": "/users/12345/orders", "method": "GET" }, { "rel": "create_order", "href": "/users/12345/orders", "method": "POST" }, { "rel": "deactivate", "href": "/users/12345/deactivate", "method": "POST" } ], "embedded": { "recent_orders": [ { "id": "ord-001", "total": 99.99, "links": [ { "rel": "self", "href": "/orders/ord-001" } ] } ] }}Most APIs labeled "RESTful" are actually "REST-ish"—they use HTTP verbs and JSON but ignore HATEOAS and treat APIs as RPC-over-HTTP. This isn't necessarily wrong; sometimes pragmatic simplicity beats architectural purity. But understanding true REST helps you appreciate what you're trading away.
Practical REST API design involves mapping domain operations to HTTP methods, resources, and status codes. Let's look at real-world patterns.
HTTP Methods and Their Semantics
HTTP methods (verbs) have defined semantics that REST leverages:
Safety means the request doesn't modify state. Idempotence means making the same request multiple times has the same effect as making it once.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
# RESTful API implementation with Flaskfrom flask import Flask, request, jsonify, url_forfrom werkzeug.exceptions import NotFound, BadRequest app = Flask(__name__) # In-memory database for demonstrationusers_db = {}orders_db = {} # =================== RESOURCE: Users =================== @app.route('/users', methods=['GET'])def list_users(): """ GET /users - List all users (with pagination) Query params: ?page=1&limit=20&status=active """ page = request.args.get('page', 1, type=int) limit = request.args.get('limit', 20, type=int) status_filter = request.args.get('status') users = list(users_db.values()) if status_filter: users = [u for u in users if u['status'] == status_filter] # Pagination start = (page - 1) * limit end = start + limit paginated_users = users[start:end] return jsonify({ 'data': paginated_users, 'meta': { 'page': page, 'limit': limit, 'total': len(users), }, 'links': { 'self': url_for('list_users', page=page, limit=limit), 'next': url_for('list_users', page=page+1, limit=limit) if end < len(users) else None, 'prev': url_for('list_users', page=page-1, limit=limit) if page > 1 else None, } }) @app.route('/users/<int:user_id>', methods=['GET'])def get_user(user_id): """ GET /users/:id - Retrieve a single user Returns: 200 OK with user data, or 404 Not Found """ user = users_db.get(user_id) if not user: raise NotFound(f"User {user_id} not found") return jsonify({ **user, 'links': _user_links(user_id), }) @app.route('/users', methods=['POST'])def create_user(): """ POST /users - Create a new user Returns: 201 Created with Location header """ data = request.get_json() # Validation if not data.get('email'): raise BadRequest("Email is required") user_id = len(users_db) + 1 user = { 'id': user_id, 'email': data['email'], 'name': data.get('name', ''), 'status': 'active', } users_db[user_id] = user response = jsonify({ **user, 'links': _user_links(user_id), }) response.status_code = 201 response.headers['Location'] = url_for('get_user', user_id=user_id) return response @app.route('/users/<int:user_id>', methods=['PUT'])def replace_user(user_id): """ PUT /users/:id - Replace user entirely Idempotent: same request always produces same result """ if user_id not in users_db: raise NotFound(f"User {user_id} not found") data = request.get_json() # PUT replaces the entire resource users_db[user_id] = { 'id': user_id, 'email': data.get('email', ''), 'name': data.get('name', ''), 'status': data.get('status', 'active'), } return jsonify(users_db[user_id]) @app.route('/users/<int:user_id>', methods=['PATCH'])def update_user(user_id): """ PATCH /users/:id - Partial update Only modifies provided fields """ if user_id not in users_db: raise NotFound(f"User {user_id} not found") data = request.get_json() user = users_db[user_id] # PATCH merges with existing data for key in ['email', 'name', 'status']: if key in data: user[key] = data[key] return jsonify(user) @app.route('/users/<int:user_id>', methods=['DELETE'])def delete_user(user_id): """ DELETE /users/:id - Remove user Returns: 204 No Content on success Idempotent: deleting non-existent user returns 204 too """ users_db.pop(user_id, None) return '', 204 # No Content # =================== NESTED RESOURCE: Orders =================== @app.route('/users/<int:user_id>/orders', methods=['GET'])def list_user_orders(user_id): """ GET /users/:id/orders - List orders for a user Demonstrates nested resources """ if user_id not in users_db: raise NotFound(f"User {user_id} not found") user_orders = [o for o in orders_db.values() if o['user_id'] == user_id] return jsonify({ 'data': user_orders, 'links': { 'self': url_for('list_user_orders', user_id=user_id), 'user': url_for('get_user', user_id=user_id), } }) @app.route('/users/<int:user_id>/orders', methods=['POST'])def create_order(user_id): """ POST /users/:id/orders - Create order for user """ if user_id not in users_db: raise NotFound(f"User {user_id} not found") data = request.get_json() order_id = len(orders_db) + 1 order = { 'id': order_id, 'user_id': user_id, 'items': data.get('items', []), 'total': data.get('total', 0), 'status': 'pending', } orders_db[order_id] = order response = jsonify(order) response.status_code = 201 response.headers['Location'] = url_for('get_order', order_id=order_id) return response def _user_links(user_id): """Generate HATEOAS links for a user""" return { 'self': url_for('get_user', user_id=user_id), 'update': url_for('update_user', user_id=user_id), 'delete': url_for('delete_user', user_id=user_id), 'orders': url_for('list_user_orders', user_id=user_id), } # =================== HTTP STATUS CODES ==================="""Status codes communicate result semantics: 2xx Success: 200 OK - Request succeeded 201 Created - Resource created (POST) 204 No Content - Success, no body (DELETE) 4xx Client Error: 400 Bad Request - Malformed request 401 Unauthorized - Authentication required 403 Forbidden - Authenticated but not authorized 404 Not Found - Resource doesn't exist 409 Conflict - State conflict (e.g., duplicate email) 422 Unprocessable - Valid syntax, semantic errors 5xx Server Error: 500 Internal Error - Server-side failure 502 Bad Gateway - Upstream failure 503 Service Unavailable - Temporarily overloaded"""REST APIs evolve. Common versioning approaches: (1) URL versioning: /v1/users, /v2/users. Simple but pollutes URLs. (2) Header versioning: Accept: application/vnd.api+json; version=2. Cleaner URLs but harder to test. (3) Query parameter: /users?version=2. Easy to use but not idiomatic. Most teams choose URL versioning for its simplicity and curl-friendliness.
gRPC (gRPC Remote Procedure Calls) was developed by Google and released in 2015. It's designed from the ground up for efficient, type-safe, polyglot service communication.
gRPC's Core Design Principles
Contract-First Development: Define the API in Protocol Buffer files. Generate client and server code.
Binary Efficiency: Protocol Buffers are 3-10x smaller and faster than JSON.
HTTP/2 Native: Full HTTP/2 benefits—multiplexing, bidirectional streaming, header compression.
Multi-Language by Design: IDL generates consistent, idiomatic code for 10+ languages.
Streaming First-Class: Client streaming, server streaming, and bidirectional streaming are native.
Deadline Propagation: Timeouts flow across service calls, preventing cascading delays.
The gRPC Request Flow
Unlike REST's request-response-per-connection model, gRPC uses HTTP/2's persistent, multiplexed connections:
Hundreds of concurrent RPCs can share a single TCP connection. This eliminates connection setup overhead and enables efficient multiplexing.
Communication Patterns
gRPC supports four communication patterns:
| Pattern | Request | Response | Use Case |
|---|---|---|---|
| Unary | Single | Single | Traditional request-response |
| Server streaming | Single | Stream | Download, real-time updates |
| Client streaming | Stream | Single | Upload, aggregation |
| Bidirectional streaming | Stream | Stream | Chat, real-time collaboration |
12345678910111213141516171819202122232425262728293031323334353637383940
// gRPC streaming patterns examplesyntax = "proto3"; package streaming; message FileChunk { bytes data = 1; int64 offset = 2;} message UploadStatus { int64 bytes_received = 1; string file_id = 2;} message FileRequest { string file_id = 1;} message ChatMessage { string user_id = 1; string content = 2; int64 timestamp = 3;} service FileService { // Server streaming: Download a file in chunks // Client sends one request, receives stream of chunks rpc DownloadFile(FileRequest) returns (stream FileChunk); // Client streaming: Upload a file in chunks // Client streams chunks, receives single confirmation rpc UploadFile(stream FileChunk) returns (UploadStatus);} service ChatService { // Bidirectional streaming: Real-time chat // Both sides send messages as they occur rpc Chat(stream ChatMessage) returns (stream ChatMessage);}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
// gRPC streaming implementation in Gopackage main import ( "io" pb "example/streaming") type FileServer struct { pb.UnimplementedFileServiceServer} // Server streaming: Download file in chunksfunc (s *FileServer) DownloadFile(req *pb.FileRequest, stream pb.FileService_DownloadFileServer) error { file, err := os.Open(getFilePath(req.FileId)) if err != nil { return status.Errorf(codes.NotFound, "file not found: %v", err) } defer file.Close() buffer := make([]byte, 64*1024) // 64KB chunks offset := int64(0) for { n, err := file.Read(buffer) if err == io.EOF { break } if err != nil { return status.Errorf(codes.Internal, "read error: %v", err) } // Send chunk to client if err := stream.Send(&pb.FileChunk{ Data: buffer[:n], Offset: offset, }); err != nil { return err } offset += int64(n) } return nil} // Client streaming: Receive file uploadfunc (s *FileServer) UploadFile(stream pb.FileService_UploadFileServer) error { fileId := generateFileId() file, err := os.Create(getFilePath(fileId)) if err != nil { return status.Errorf(codes.Internal, "create error: %v", err) } defer file.Close() var totalBytes int64 for { // Receive chunk from client chunk, err := stream.Recv() if err == io.EOF { // Client finished streaming return stream.SendAndClose(&pb.UploadStatus{ BytesReceived: totalBytes, FileId: fileId, }) } if err != nil { return err } // Write chunk to file n, err := file.Write(chunk.Data) if err != nil { return status.Errorf(codes.Internal, "write error: %v", err) } totalBytes += int64(n) }} type ChatServer struct { pb.UnimplementedChatServiceServer} // Bidirectional streaming: Chatfunc (s *ChatServer) Chat(stream pb.ChatService_ChatServer) error { // In a real implementation, this would broadcast to other connected users for { msg, err := stream.Recv() if err == io.EOF { return nil } if err != nil { return err } // Echo back (in reality, broadcast to others) response := &pb.ChatMessage{ UserId: "server", Content: "Echo: " + msg.Content, Timestamp: time.Now().UnixNano(), } if err := stream.Send(response); err != nil { return err } }}gRPC's performance advantage comes largely from HTTP/2: Multiplexing eliminates head-of-line blocking. Header compression (HPACK) reduces overhead for repeated headers. Server push can preemptively send expected data. Binary framing is more efficient to parse than HTTP/1.1's text format. A single gRPC connection can handle hundreds of concurrent requests without the overhead of connection pools.
Let's directly compare REST and gRPC across key dimensions. Neither is universally better—the right choice depends on your constraints and priorities.
| Dimension | REST | gRPC |
|---|---|---|
| Protocol | HTTP/1.1 or HTTP/2 | HTTP/2 only |
| Data format | JSON (text, human-readable) | Protocol Buffers (binary) |
| Contract | OpenAPI/Swagger (optional) | Protobuf (required) |
| Code generation | Optional, varies by tool | Native, consistent |
| Browser support | Native | Requires grpc-web proxy |
| Streaming | SSE, WebSockets (separate) | Native bidirectional |
| Caching | HTTP caching, CDNs | Not easily cacheable |
| Debugging | curl, browser, Postman | grpcurl, specialized tools |
| Payload size | Larger (JSON overhead) | 3-10x smaller |
| Latency | Higher (text parsing) | Lower (binary, HTTP/2) |
| Learning curve | Lower (familiar patterns) | Higher (protobuf, tooling) |
Performance: The Numbers
Benchmarks consistently show gRPC outperforming REST/JSON:
However, for many applications, REST performance is sufficient. If your bottleneck is database queries or external services, faster serialization won't matter.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
# Same operation implemented with REST and gRPC # =================== REST IMPLEMENTATION =================== import requestsimport json def rest_get_user(user_id): """REST: Get user profile""" response = requests.get( f'https://api.example.com/v1/users/{user_id}', headers={'Authorization': 'Bearer token123'} ) response.raise_for_status() return response.json() # Parsed JSON dict def rest_create_order(user_id, items): """REST: Create order""" response = requests.post( f'https://api.example.com/v1/users/{user_id}/orders', headers={ 'Authorization': 'Bearer token123', 'Content-Type': 'application/json', }, json={ 'items': items, 'metadata': {'source': 'api'} } ) response.raise_for_status() return response.json() # =================== gRPC IMPLEMENTATION =================== import grpcimport user_service_pb2import user_service_pb2_grpcimport order_service_pb2import order_service_pb2_grpc def grpc_get_user(channel, user_id): """gRPC: Get user profile""" stub = user_service_pb2_grpc.UserServiceStub(channel) request = user_service_pb2.GetUserRequest(user_id=user_id) # Call returns typed protobuf message, not dict response = stub.GetUser( request, metadata=[('authorization', 'Bearer token123')], timeout=5.0, ) return response # user_service_pb2.User object def grpc_create_order(channel, user_id, items): """gRPC: Create order""" stub = order_service_pb2_grpc.OrderServiceStub(channel) # Build typed request request = order_service_pb2.CreateOrderRequest( user_id=user_id, items=[ order_service_pb2.OrderItem( product_id=item['product_id'], quantity=item['quantity'], ) for item in items ], metadata={'source': 'api'}, ) response = stub.CreateOrder(request, timeout=10.0) return response # =================== KEY DIFFERENCES =================== def compare_error_handling(): """Error handling differences""" # REST: HTTP status codes + parsed error body try: rest_get_user('invalid') except requests.HTTPError as e: print(f"Status: {e.response.status_code}") print(f"Body: {e.response.json()}") # May not be JSON # gRPC: Typed status codes + metadata try: grpc_get_user(channel, 'invalid') except grpc.RpcError as e: print(f"Code: {e.code()}") # grpc.StatusCode.NOT_FOUND print(f"Details: {e.details()}") print(f"Metadata: {e.trailing_metadata()}") def compare_type_safety(): """Type safety differences""" # REST: Runtime errors if structure changes user = rest_get_user('123') # If API changes 'email' to 'email_address', this crashes at runtime: email = user['email'] # KeyError at runtime! # gRPC: Compile-time checking (in typed languages) user = grpc_get_user(channel, '123') # If proto changes, generated code changes, IDE shows error: email = user.email # Caught at compile/lint time def compare_payload_size(): """Payload size comparison""" import sys # REST JSON payload json_order = { "order_id": "ord-12345", "user_id": "user-67890", "items": [ {"product_id": "prod-001", "quantity": 2, "price": 29.99}, {"product_id": "prod-002", "quantity": 1, "price": 49.99}, ], "total": 109.97, "status": "pending", "created_at": "2024-01-15T10:30:00Z", } json_bytes = json.dumps(json_order).encode('utf-8') print(f"JSON size: {len(json_bytes)} bytes") # ~280 bytes # gRPC protobuf payload (equivalent data) proto_order = order_pb2.Order( order_id="ord-12345", user_id="user-67890", items=[ order_pb2.OrderItem(product_id="prod-001", quantity=2, price=29.99), order_pb2.OrderItem(product_id="prod-002", quantity=1, price=49.99), ], total=109.97, status=order_pb2.OrderStatus.PENDING, created_at_seconds=1705316400, ) proto_bytes = proto_order.SerializeToString() print(f"Protobuf size: {len(proto_bytes)} bytes") # ~85 bytes # Protobuf is ~3.3x smaller for this exampleMany organizations use both: REST at the edge for public APIs, browser clients, and third-party integrations, with gRPC internally for microservice communication. An API gateway translates between the two, transcoding requests. This gives you REST's accessibility and gRPC's efficiency where each matters most.
Both REST and gRPC have evolved patterns for handling common distributed system challenges.
Pagination
Both paradigms need to handle large result sets:
123456789101112131415161718192021222324
// gRPC pagination patterns message PageRequest { // Cursor-based (preferred for consistency) string page_token = 1; // Opaque cursor from previous response int32 page_size = 2; // Max results per page} message ListUsersRequest { PageRequest pagination = 1; string status_filter = 2;} message ListUsersResponse { repeated User users = 1; string next_page_token = 2; // Empty if no more pages int32 total_count = 3; // Optional total across all pages} // Usage:// 1. First request: { pagination: { page_size: 20 } }// 2. Response includes next_page_token: "eyJ..."// 3. Next request: { pagination: { page_token: "eyJ...", page_size: 20 } }// 4. Repeat until next_page_token is emptyError Handling
Both need structured error responses beyond simple status codes:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
# Error handling patterns # =================== REST ERROR RESPONSES ===================# Follow RFC 7807 Problem Details def rest_error_response(): """Standard REST error format""" return { "type": "https://api.example.com/errors/validation", "title": "Validation Error", "status": 400, "detail": "One or more fields failed validation", "instance": "/users/123", "errors": [ { "field": "email", "code": "invalid_format", "message": "Email must be a valid email address" }, { "field": "age", "code": "out_of_range", "message": "Age must be between 0 and 150" } ], "trace_id": "abc-123-def" } # =================== gRPC ERROR HANDLING ===================import grpcfrom google.rpc import error_details_pb2, status_pb2from google.protobuf import any_pb2 def grpc_rich_error(context): """ gRPC rich error details. Standard error_details protos provide structured info. """ # Create rich status with details status = status_pb2.Status( code=grpc.StatusCode.INVALID_ARGUMENT.value[0], message="Validation failed", ) # Add field violations bad_request = error_details_pb2.BadRequest( field_violations=[ error_details_pb2.BadRequest.FieldViolation( field="email", description="Email must be valid" ), error_details_pb2.BadRequest.FieldViolation( field="age", description="Age must be between 0 and 150" ), ] ) # Add debug info debug_info = error_details_pb2.DebugInfo( stack_entries=["..."], detail="Validation failed in UserValidator" ) # Pack into Any for transport detail1 = any_pb2.Any() detail1.Pack(bad_request) status.details.append(detail1) detail2 = any_pb2.Any() detail2.Pack(debug_info) status.details.append(detail2) # Set the error on context context.abort_with_status(rpc_status.to_status(status)) # Client-side error extractiondef grpc_handle_error(rpc_error): """Extract rich error details from gRPC error""" status = rpc_status.from_call(rpc_error) for detail in status.details: if detail.Is(error_details_pb2.BadRequest.DESCRIPTOR): bad_request = error_details_pb2.BadRequest() detail.Unpack(bad_request) for violation in bad_request.field_violations: print(f"Field {violation.field}: {violation.description}")Health Checking
Both ecosystems have standardized health check patterns:
12345678910111213141516171819202122232425262728293031
// gRPC standard health checking protocol// From: grpc.health.v1 syntax = "proto3"; package grpc.health.v1; message HealthCheckRequest { // Service to check (empty = server overall health) string service = 1;} message HealthCheckResponse { enum ServingStatus { UNKNOWN = 0; SERVING = 1; NOT_SERVING = 2; SERVICE_UNKNOWN = 3; // Service name not recognized } ServingStatus status = 1;} service Health { // Check health of a service rpc Check(HealthCheckRequest) returns (HealthCheckResponse); // Watch for health changes (streaming) rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);} // gRPC clients and load balancers understand this standard protocolBoth specification formats need to evolve without breaking clients. For OpenAPI: use semantic versioning, deprecate before removing, ensure additive changes only. For Protobuf: never reuse field numbers, use 'reserved' for removed fields, only add new optional fields. Both support extensions for backward-compatible additions.
The API Gateway pattern is central to many modern architectures, especially when mixing REST and gRPC.
What an API Gateway Does
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
# Envoy configuration for REST to gRPC transcoding# Client sends REST, Envoy converts to gRPC for backend static_resources: listeners: - address: socket_address: address: 0.0.0.0 port_value: 8080 filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: grpc_json route_config: name: local_route virtual_hosts: - name: backend domains: ["*"] routes: - match: prefix: "/api/v1/users" route: cluster: user_service http_filters: # gRPC-JSON transcoder - name: envoy.filters.http.grpc_json_transcoder typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder proto_descriptor: "/etc/envoy/proto.pb" services: ["user.UserService"] print_options: add_whitespace: true always_print_primitive_fields: true - name: envoy.filters.http.router clusters: - name: user_service type: STRICT_DNS lb_policy: ROUND_ROBIN http2_protocol_options: {} # Enable HTTP/2 for gRPC load_assignment: cluster_name: user_service endpoints: - lb_endpoints: - endpoint: address: socket_address: address: user-service port_value: 50051 # Proto file needs HTTP annotations for REST mapping:# # service UserService {# rpc GetUser(GetUserRequest) returns (User) {# option (google.api.http) = {# get: "/api/v1/users/{user_id}"# };# }# }Popular API Gateways
Browsers can't make native gRPC calls (no HTTP/2 trailers, limited control over headers). gRPC-Web is a protocol that works over HTTP/1.1 and HTTP/2, proxied through Envoy or another gateway that converts to native gRPC. This enables browser access to gRPC services without changing the backend.
REST and gRPC represent different points on the trade-off spectrum between accessibility and efficiency. Understanding both enables you to make informed architectural decisions.
What's Next:
We'll dive into Sockets—the fundamental operating system primitive upon which both REST and gRPC are built. Understanding sockets reveals how high-level communication abstractions work at the system level.
You now understand the design philosophies, mechanics, and trade-offs of both REST and gRPC. You can make informed decisions about which to use—or how to use both—based on your specific requirements for external accessibility, internal performance, and team capabilities.