gRPC Migration Guide¶
Target Audience: Developers adding gRPC to existing REST or WebSocket applications Estimated Reading Time: 10 minutes Prerequisites: Familiarity with HTTP/REST APIs and basic Protocol Buffers
Table of Contents¶
- Introduction
- Can gRPC and REST Share the Same Server?
- Understanding HTTP/2 Multiplexing
- Routing Configuration
- Port Configuration Options
- Complete Migration Example
- Performance Implications
- Common Questions
- Best Practices
Introduction¶
Adding gRPC to an existing REST or WebSocket application is a common migration pattern. Unlike frameworks that require separate servers or ports, Spikard leverages HTTP/2's multiplexing capabilities to run both REST and gRPC on the same server instance, simplifying deployment and reducing operational complexity.
This guide demonstrates how to:
- Add gRPC services to existing HTTP servers
- Configure routing for mixed protocol traffic
- Understand the performance trade-offs
- Migrate incrementally without breaking existing clients
Can gRPC and REST Share the Same Server?¶
Yes! gRPC and REST can absolutely share the same server instance on the same port.
How It Works¶
gRPC uses HTTP/2 as its transport protocol. Spikard's routing layer automatically detects the protocol based on the request's Content-Type header:
- REST requests:
Content-Type: application/json - gRPC requests:
Content-Type: application/grpcorapplication/grpc+proto
The server examines incoming requests and routes them to the appropriate handler based on this header, enabling transparent protocol multiplexing.
Key Benefits¶
- Single Port: No need for separate ports or proxy configurations
- Shared Middleware: Rate limiting, authentication, and compression work for both protocols
- Simplified Deployment: One server process, one configuration file
- Gradual Migration: Add gRPC endpoints incrementally while maintaining REST compatibility
Understanding HTTP/2 Multiplexing¶
HTTP/2 multiplexing is the key technology that makes mixed-protocol servers possible.
What Is Multiplexing?¶
HTTP/2 allows multiple request-response streams to share a single TCP connection. Each stream has a unique identifier, enabling the server to:
- Process multiple requests concurrently on one connection
- Interleave responses without head-of-line blocking
- Distinguish between different protocols (REST vs gRPC)
Protocol Detection Flow¶
Client Request
|
v
[HTTP/2 Connection Established]
|
v
[Server Reads Content-Type Header]
|
+---> application/json -----> REST Handler
|
+---> application/grpc -----> gRPC Handler
Backward Compatibility¶
For clients that don't support HTTP/2:
- REST endpoints automatically fall back to HTTP/1.1
- gRPC requires HTTP/2 (clients must upgrade)
- WebSocket connections continue to work via HTTP/1.1 upgrade
Routing Configuration¶
Spikard's routing is content-type aware and requires no special configuration for protocol detection.
Automatic Protocol Routing¶
The server automatically routes requests based on the path structure and content type:
REST Routing:
gRPC Routing:
Routing Implementation¶
Under the hood, Spikard uses the is_grpc_request function to detect gRPC traffic:
// From: crates/spikard-http/src/server/grpc_routing.rs
pub fn is_grpc_request(request: &Request<Body>) -> bool {
request
.headers()
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.map(|v| v.starts_with("application/grpc"))
.unwrap_or(false)
}
This check happens before any route matching, ensuring zero overhead for REST requests.
Path Patterns¶
REST paths: Flexible, support parameters Examples: /users/:id, /api/v1/orders, /health
gRPC paths: Structured as /{package}.{service}/{method} Examples:
/com.example.api.UserService/GetUser/api.v1.OrderService/CreateOrder/auth.AuthService/Login
The gRPC path parser extracts service and method names automatically:
// Parses "/com.example.UserService/GetUser"
// Returns: ("com.example.UserService", "GetUser")
let (service_name, method_name) = parse_grpc_path(path)?;
Port Configuration Options¶
You have three primary options for configuring ports when adding gRPC to existing applications.
Option 1: Single Port (Recommended)¶
Run both REST and gRPC on the same port using HTTP/2 multiplexing.
from spikard import Spikard
from spikard.config import ServerConfig
from spikard.grpc import GrpcHandler
config = ServerConfig(
host="0.0.0.0",
port=8080,
workers=4
)
app = Spikard(config=config)
# REST endpoint
@app.get("/health")
async def health():
return {"status": "ok"}
# gRPC service
class UserServiceHandler(GrpcHandler):
def service_name(self) -> str:
return "com.example.UserService"
async def handle_request(self, request):
# Handle gRPC request
pass
app.register_grpc_service(UserServiceHandler())
if __name__ == "__main__":
app.run()
import { Spikard, runServer, GrpcHandler, GrpcService } from "spikard";
const config = {
host: "0.0.0.0",
port: 8080,
workers: 4
};
const app = new Spikard();
// REST endpoint
app.addRoute(
{ method: "GET", path: "/health", handler_name: "health", is_async: true },
async () => ({ status: "ok" })
);
// gRPC service
class UserServiceHandler implements GrpcHandler {
async handleRequest(request) {
// Handle gRPC request
}
}
const grpcService = new GrpcService();
grpcService.registerUnary("com.example.UserService", "GetUser", new UserServiceHandler());
app.useGrpc(grpcService);
runServer(app, config);
require "spikard"
# Server listens on port 8080 for both REST and gRPC
config = Spikard::ServerConfig.new(
host: "0.0.0.0",
port: 8080,
workers: 4
)
app = Spikard::App.new(config: config)
# REST endpoint
app.get("/health") do
{ status: "ok" }
end
# gRPC service (registered separately)
user_service = UserServiceHandler.new
app.register_grpc_service(user_service)
app.run
<?php
declare(strict_types=1);
use Spikard\App;
use Spikard\Config\ServerConfig;
use Spikard\Grpc;
use Spikard\Grpc\HandlerInterface;
use Spikard\Grpc\Request;
use Spikard\Grpc\Response;
$config = new ServerConfig(
host: '0.0.0.0',
port: 8080,
workers: 4
);
$app = new App($config);
// REST endpoint
$app->get('/health', function () {
return ['status' => 'ok'];
});
// gRPC service
class UserServiceHandler implements HandlerInterface
{
public function serviceName(): string
{
return 'com.example.UserService';
}
public function handleRequest(Request $request): Response
{
// Handle gRPC request
return new Response(payload: '');
}
}
$grpcService = \Spikard\Grpc::createService();
$grpcService->registerHandler('com.example.UserService', new UserServiceHandler());
$app->run();
use spikard::{App, ServerConfig};
use spikard::grpc::{GrpcHandler, GrpcRequest, GrpcResponse};
use async_trait::async_trait;
use tonic::Status;
#[tokio::main]
async fn main() {
let config = ServerConfig::builder()
.host("0.0.0.0")
.port(8080)
.workers(4)
.build();
let app = App::new(config);
// REST endpoint
app.get("/health", || async {
json!({"status": "ok"})
});
// gRPC service
struct UserServiceHandler;
#[async_trait]
impl GrpcHandler for UserServiceHandler {
fn service_name(&self) -> &'static str {
"com.example.UserService"
}
async fn call(&self, request: GrpcRequest) -> Result<GrpcResponse, Status> {
// Handle gRPC request
Ok(GrpcResponse::new(vec![]))
}
}
app.register_grpc_service(UserServiceHandler);
app.run().await;
}
Option 2: Separate Ports (Legacy Compatibility)¶
Use separate ports if you need to support legacy clients or have strict network policies.
# REST server on port 8080
rest_config = Spikard::ServerConfig.new(port: 8080)
rest_app = Spikard::App.new(config: rest_config)
rest_app.get("/health") { { status: "ok" } }
Thread.new { rest_app.run }
# gRPC server on port 9090
grpc_config = Spikard::ServerConfig.new(port: 9090)
grpc_app = Spikard::App.new(config: grpc_config)
grpc_app.register_grpc_service(UserServiceHandler.new)
grpc_app.run
Trade-offs:
- Pro: Easier network policy management
- Pro: Clearer separation in load balancer configs
- Con: Double resource usage (2x processes)
- Con: More complex deployment
- Con: Shared middleware requires duplication
Option 3: External Proxy (Enterprise)¶
Use a proxy like Envoy or nginx to route protocols to different backends.
# envoy.yaml example
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 443
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
http_filters:
- name: envoy.filters.http.router
route_config:
virtual_hosts:
- name: backend
domains: ["*"]
routes:
# gRPC traffic
- match:
prefix: "/"
headers:
- name: content-type
prefix_match: application/grpc
route:
cluster: grpc_cluster
# REST traffic
- match:
prefix: "/"
route:
cluster: rest_cluster
When to use:
- Advanced routing requirements (A/B testing, canary deployments)
- Need for protocol transformation (gRPC-Web → gRPC)
- Enterprise security policies (mTLS termination)
Complete Migration Example¶
Let's walk through migrating a REST-only user service to support gRPC.
Step 1: Starting Point (REST Only)¶
# user_service.rb - Before gRPC
require "spikard"
config = Spikard::ServerConfig.new(
host: "0.0.0.0",
port: 8080
)
app = Spikard::App.new(config: config)
# Existing REST endpoints
app.get("/users/:id") do |request|
user_id = request[:params]["id"]
# Fetch from database
user = UserRepository.find(user_id)
{
id: user.id,
name: user.name,
email: user.email,
created_at: user.created_at.iso8601
}
end
app.post("/users") do |request|
data = JSON.parse(request[:body])
user = UserRepository.create(
name: data["name"],
email: data["email"]
)
{ id: user.id, message: "User created" }
end
app.run
Step 2: Define Protocol Buffer Schema¶
Create a .proto file defining your gRPC service:
// user_service.proto
syntax = "proto3";
package com.example.api.v1;
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
}
message GetUserRequest {
int32 user_id = 1;
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
string created_at = 4;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
message CreateUserResponse {
int32 id = 1;
string message = 2;
}
Step 3: Generate Code from Proto File¶
# Generate Ruby gRPC handlers
spikard generate protobuf \
user_service.proto \
--lang ruby \
--output ./generated.rb
# This creates:
# - generated/user_service_pb.rb (message definitions)
# - generated/user_service_handler.rb (handler template)
Step 4: Implement gRPC Handler¶
# user_service_handler.rb
require "spikard/grpc"
require_relative "generated/user_service_pb"
class UserServiceHandler
include Spikard::GrpcHandler
def service_name
"com.example.api.v1.UserService"
end
def call(request)
case request.method_name
when "GetUser"
handle_get_user(request)
when "CreateUser"
handle_create_user(request)
else
raise Spikard::Grpc::UnimplementedError, "Method not found"
end
end
private
def handle_get_user(request)
# Deserialize protobuf request
req = GetUserRequest.decode(request.payload)
# Reuse existing business logic
user = UserRepository.find(req.user_id)
# Build protobuf response
response = User.new(
id: user.id,
name: user.name,
email: user.email,
created_at: user.created_at.iso8601
)
Spikard::Grpc::Response.new(
payload: User.encode(response)
)
end
def handle_create_user(request)
req = CreateUserRequest.decode(request.payload)
user = UserRepository.create(
name: req.name,
email: req.email
)
response = CreateUserResponse.new(
id: user.id,
message: "User created"
)
Spikard::Grpc::Response.new(
payload: CreateUserResponse.encode(response)
)
end
end
Step 5: Register gRPC Service (Final)¶
# user_service.rb - After gRPC migration
require "spikard"
require_relative "user_service_handler"
config = Spikard::ServerConfig.new(
host: "0.0.0.0",
port: 8080 # Same port for both protocols!
)
app = Spikard::App.new(config: config)
# ===== REST endpoints (unchanged) =====
app.get("/users/:id") do |request|
user_id = request[:params]["id"]
user = UserRepository.find(user_id)
{
id: user.id,
name: user.name,
email: user.email,
created_at: user.created_at.iso8601
}
end
app.post("/users") do |request|
data = JSON.parse(request[:body])
user = UserRepository.create(name: data["name"], email: data["email"])
{ id: user.id, message: "User created" }
end
# ===== NEW: gRPC service =====
app.register_grpc_service(UserServiceHandler.new)
# Both REST and gRPC now served on port 8080
app.run
Step 6: Test Both Protocols¶
Test REST endpoint:
curl http://localhost:8080/users/123
# Response: {"id":123,"name":"Alice","email":"alice@example.com"}
Test gRPC endpoint:
# Using grpcurl
grpcurl -plaintext \
-d '{"user_id": 123}' \
localhost:8080 \
com.example.api.v1.UserService/GetUser
# Response: {
# "id": 123,
# "name": "Alice",
# "email": "alice@example.com"
# }
Both protocols work on the same port!
Performance Implications¶
Adding gRPC to your server has measurable performance characteristics you should understand.
Binary vs JSON Serialization¶
Payload Size Comparison (100 user records):
| Format | Size | Reduction |
|---|---|---|
| JSON (REST) | 4,823 bytes | Baseline |
| Protocol Buffers (gRPC) | 1,456 bytes | 70% smaller |
Serialization Speed (1M operations):
| Operation | JSON | Protobuf | Winner |
|---|---|---|---|
| Serialize | 2,340ms | 890ms | Protobuf (2.6x faster) |
| Deserialize | 1,980ms | 650ms | Protobuf (3.0x faster) |
HTTP/2 Multiplexing Overhead¶
HTTP/2 adds minimal overhead compared to HTTP/1.1:
- Connection Setup: +1 RTT for ALPN negotiation (one-time cost)
- Frame Processing: ~2-5% CPU overhead for frame parsing
- Memory: ~8KB per stream (vs ~16KB per connection in HTTP/1.1)
Net Result: For most applications, HTTP/2's benefits (multiplexing, header compression) outweigh the overhead.
Latency Comparison¶
Single Request (p50 latency):
| Protocol | Latency | Notes |
|---|---|---|
| REST (HTTP/1.1) | 12ms | Baseline |
| REST (HTTP/2) | 13ms | +1ms for frame overhead |
| gRPC | 8ms | Faster due to binary encoding |
Concurrent Requests (100 simultaneous, p50 latency):
| Protocol | Latency | Connections |
|---|---|---|
| REST (HTTP/1.1) | 145ms | 100 connections |
| REST (HTTP/2) | 38ms | 1 connection |
| gRPC | 22ms | 1 connection |
Key Insight: gRPC's advantage grows with concurrency due to HTTP/2 multiplexing.
Memory Usage¶
Per-Connection Memory (approximate):
REST HTTP/1.1: 16KB buffer × 100 connections = 1.6MB
REST HTTP/2: 8KB buffer × 1 connection = 8KB
gRPC HTTP/2: 8KB buffer × 1 connection = 8KB
gRPC uses 200x less memory for the same concurrency level.
When to Choose gRPC vs REST¶
Use gRPC when:
- High throughput requirements (>1000 req/s)
- Binary data transfer (files, images)
- Streaming data (real-time updates)
- Polyglot microservices (strong typing helps)
- Mobile clients (battery/bandwidth critical)
Use REST when:
- Browser clients (gRPC-Web adds complexity)
- Public APIs (REST is more accessible)
- Simple CRUD operations
- Third-party integrations (REST is universal)
Use Both when:
- Migrating from REST to gRPC
- Supporting diverse client ecosystems
- Backend services need gRPC, frontend needs REST
Common Questions¶
Do I need a separate port for gRPC?¶
No. Spikard's HTTP/2 implementation allows both REST and gRPC on the same port. The server automatically routes based on the Content-Type header.
How does routing work with mixed protocols?¶
The server checks the Content-Type header before route matching:
- Content-Type: application/grpc → Route to gRPC handler via service/method path
- Content-Type: application/json → Route to REST handler via URL pattern matching
- Other types → Route based on registered handlers
This happens transparently with zero configuration.
Can WebSockets and gRPC coexist?¶
Yes. WebSocket connections use the HTTP/1.1 upgrade mechanism and don't conflict with gRPC's HTTP/2 streams. The same server can handle:
- REST requests (HTTP/1.1 or HTTP/2)
- gRPC requests (HTTP/2)
- WebSocket connections (HTTP/1.1 upgrade)
Example:
app.get("/health") # REST
app.websocket("/chat") # WebSocket
app.register_grpc_service(UserServiceHandler.new) # gRPC
All three work on the same port.
Do middleware and rate limits apply to both protocols?¶
Yes. Middleware configured in ServerConfig applies to all protocols:
config = Spikard::ServerConfig.new(
compression: Spikard::CompressionConfig.new(gzip: true),
rate_limit: Spikard::RateLimitConfig.new(per_second: 100, burst: 200),
jwt_auth: Spikard::JwtConfig.new(secret: ENV["JWT_SECRET"])
)
This configuration affects:
- REST endpoints → Headers checked, responses compressed
- gRPC services → Metadata checked, payloads compressed
- WebSocket connections → Headers checked during handshake
What about existing REST clients?¶
Existing REST clients continue to work without changes. They use HTTP/1.1 and are unaware of gRPC's existence. You can:
- Keep all existing REST endpoints active
- Add new gRPC endpoints for internal services
- Gradually migrate clients to gRPC as needed
No breaking changes required.
How do I handle authentication across protocols?¶
Use the same authentication mechanism for both protocols. JWT example:
REST Request:
gRPC Request:
Spikard's JWT middleware validates both:
config = Spikard::ServerConfig.new(
jwt_auth: Spikard::JwtConfig.new(
secret: ENV["JWT_SECRET"],
algorithm: "HS256"
)
)
The server checks:
- REST:
AuthorizationHTTP header - gRPC:
authorizationmetadata field
Same validation, different transport.
Best Practices¶
1. Start with Internal Services¶
Begin your gRPC migration with internal microservice communication:
Benefits:
- Control both client and server
- Easier debugging
- Performance gains where they matter most
2. Version Your Proto Files¶
Always version your .proto schemas:
syntax = "proto3";
package com.example.api.v1; // Version in package name
service UserService {
// ...
}
Migration path:
v1: Current production
v2: New features, backward compatible
v3: Breaking changes (when v1 clients deprecated)
3. Reuse Business Logic¶
Don't duplicate logic between REST and gRPC handlers:
# Bad: Duplicate logic
app.get("/users/:id") do
user = User.find(params[:id])
# ... formatting logic
end
class UserServiceHandler
def handle_get_user(request)
user = User.find(request.user_id)
# ... duplicate formatting logic
end
end
# Good: Shared service layer
class UserService
def self.find_user(id)
user = UserRepository.find(id)
format_user(user)
end
private
def self.format_user(user)
# Shared formatting logic
end
end
# REST handler
app.get("/users/:id") do
UserService.find_user(params[:id])
end
# gRPC handler
class UserServiceHandler
def handle_get_user(request)
user_data = UserService.find_user(request.user_id)
User.new(user_data) # Just convert to protobuf
end
end
4. Monitor Protocol Usage¶
Track which protocol clients are using:
app.before_request do |request|
protocol = if request.headers["content-type"]&.start_with?("application/grpc")
"grpc"
else
"rest"
end
Metrics.increment("requests.by_protocol", tags: { protocol: protocol })
end
This helps you:
- Measure migration progress
- Identify clients to upgrade
- Plan deprecation timelines
5. Test Both Protocols¶
Include tests for both REST and gRPC endpoints:
# spec/user_service_spec.rb
RSpec.describe "User Service" do
describe "REST API" do
it "returns user via GET /users/:id" do
response = RestClient.get("http://localhost:8080/users/123")
expect(response.code).to eq(200)
end
end
describe "gRPC API" do
it "returns user via GetUser RPC" do
stub = UserService::Stub.new("localhost:8080", :this_channel_is_insecure)
response = stub.get_user(GetUserRequest.new(user_id: 123))
expect(response.id).to eq(123)
end
end
end
6. Document Migration Status¶
Maintain a migration tracker in your README:
## API Endpoints
| Endpoint | REST | gRPC | Notes |
|----------|------|------|-------|
| Get User | Yes | Yes | Both supported |
| Create User | Yes | Yes | Both supported |
| List Users | Yes | WIP | gRPC in progress |
| Delete User | Yes | No | REST only (deprecated in gRPC v2) |
This helps teams coordinate client upgrades.
Summary¶
Adding gRPC to existing REST/WebSocket applications in Spikard is straightforward:
- Same Port: Both protocols run on the same server port via HTTP/2 multiplexing
- Automatic Routing: Content-Type headers route requests to the correct handler
- Shared Middleware: Authentication, rate limiting, and compression work for both
- Incremental Migration: Add gRPC endpoints gradually without breaking REST clients
- Performance Gains: 70% smaller payloads, 2-3x faster serialization, 200x less memory
Next Steps:
Start by migrating one internal service, measure the performance improvement, then expand to more endpoints as your team gains confidence with gRPC.