gRPC Migration Guide¶
Target Audience: Developers exploring gRPC integration with Spikard Estimated Reading Time: 8 minutes Prerequisites: Familiarity with HTTP/REST APIs and basic Protocol Buffers Status: gRPC runtime support is implemented; high-level language bindings are planned
Table of Contents¶
- Introduction
- Current gRPC Support
- Can gRPC and REST Share the Same Server?
- Understanding HTTP/2 Multiplexing
- Architecture
- Planned High-Level API
- Performance Implications
Introduction¶
Spikard's gRPC support is built on the Tonic runtime and integrates with the same HTTP/2 server that powers REST endpoints. This guide explains the current architecture and planned APIs for adding gRPC services.
Current state: The low-level gRPC handler trait and routing infrastructure exist in spikard-http. High-level language binding APIs are in development.
Current gRPC Support¶
Spikard provides:
- Binary protocol support via Tonic gRPC runtime
- HTTP/2 multiplexing to run REST and gRPC on the same port
- Code generation from
.protofiles (spikard generate protobuf) - Type-safe handlers through language-specific bindings
Planned high-level APIs for Python, TypeScript, Rust, Ruby, PHP, and Elixir will simplify handler registration.
Can gRPC and REST Share the Same Server?¶
Yes! Spikard's HTTP/2 server supports both gRPC and REST on the same port via protocol detection.
How It Works¶
The routing layer automatically detects the protocol based on the request's Content-Type header:
- REST requests:
Content-Type: application/json→ REST handler - gRPC requests:
Content-Type: application/grpc*→ gRPC handler
Spikard's server examines incoming requests and dispatches them to the appropriate handler, enabling transparent protocol multiplexing.
Key Benefits¶
- Single Port: Both protocols on the same listener
- Shared Infrastructure: Common middleware stack (rate limiting, auth, compression) for both protocols
- Simplified Deployment: One server process, one bind address
- Gradual Migration: Incrementally add gRPC alongside existing REST endpoints
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
Architecture¶
Spikard's gRPC support is built in three layers:
Layer 1: Tonic Runtime¶
The spikard-http crate provides a Tonic-based gRPC server that handles:
- Protocol detection via
Content-Typeheaders - HTTP/2 multiplexing with REST endpoints
- Binary message serialization via protobuf
- gRPC status codes and metadata handling
Layer 2: Code Generation¶
The spikard-cli command provides:
This generates type-safe message structs and handler scaffolding for your target language.
Layer 3: Handler Traits¶
Language bindings implement the GrpcHandler trait:
Rust (from spikard-http):
pub trait GrpcHandler: Send + Sync {
fn call(&self, request: GrpcRequestData)
-> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>>;
}
Other languages (Python, TypeScript, Ruby, PHP, Elixir) implement equivalent FFI-compatible handler interfaces.
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
Planned High-Level API¶
The examples below show the intended API surface for handler registration, currently in development.
Single Shared Port (Recommended)¶
Run REST and gRPC on the same HTTP/2 server and port.
from spikard import App, ServerConfig
config = ServerConfig(
host="0.0.0.0",
port=8080
)
app = App().config(config)
# REST endpoint
async def health(request):
return {"status": "ok"}
app.get("/health", health)
# gRPC integration is currently available via the low-level
# GrpcHandler trait in the Rust runtime (spikard-http).
# High-level Python gRPC registration API is planned.
if __name__ == "__main__":
import asyncio
asyncio.run(app.run())
import { App, ServerConfig } from "@spikard/node";
const config = new ServerConfig({
host: "0.0.0.0",
port: 8080,
});
const app = new App().config(config);
// REST endpoint
app.get("/health", async (request) => {
return { status: "ok" };
});
// gRPC integration is currently available via the low-level
// GrpcHandler trait in the Rust runtime (spikard-http).
// High-level TypeScript gRPC registration API is planned.
if (require.main === module) {
app.run();
}
use spikard::{App, ServerConfig, handler_result_from_response, Response};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = ServerConfig::default()
.with_host("0.0.0.0")
.with_port(8080);
let mut app = App::new().config(config);
// REST endpoint
app.route(
spikard::get("/health"),
|_ctx| async {
let response = Response {
status_code: 200,
content: Some(serde_json::json!({"status": "ok"})),
headers: Default::default(),
};
handler_result_from_response(Ok(response))
},
)?;
// gRPC integration is currently available via the low-level
// GrpcHandler trait in spikard-http runtime.
// High-level Rust gRPC registration API is planned.
app.run().await?;
Ok(())
}
Benefits:
- Single bind address and port
- Shared middleware for both protocols
- No proxy complexity
- Simplified deployment
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.