Skip to content

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

  1. Introduction
  2. Current gRPC Support
  3. Can gRPC and REST Share the Same Server?
  4. Understanding HTTP/2 Multiplexing
  5. Architecture
  6. Planned High-Level API
  7. 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 .proto files (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

  1. Single Port: Both protocols on the same listener
  2. Shared Infrastructure: Common middleware stack (rate limiting, auth, compression) for both protocols
  3. Simplified Deployment: One server process, one bind address
  4. 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-Type headers
  • 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:

spikard generate protobuf schema.proto --lang python --output ./generated

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.

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:

  1. Content-Type: application/grpc → Route to gRPC handler via service/method path
  2. Content-Type: application/json → Route to REST handler via URL pattern matching
  3. 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:

  1. Keep all existing REST endpoints active
  2. Add new gRPC endpoints for internal services
  3. 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:

GET /users/123
Authorization: Bearer <jwt-token>

gRPC Request:

Metadata: authorization: Bearer <jwt-token>

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: Authorization HTTP header
  • gRPC: authorization metadata field

Same validation, different transport.

Best Practices

1. Start with Internal Services

Begin your gRPC migration with internal microservice communication:

[Web Frontend] --REST--> [API Gateway] --gRPC--> [Backend Services]

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:

  1. Same Port: Both protocols run on the same server port via HTTP/2 multiplexing
  2. Automatic Routing: Content-Type headers route requests to the correct handler
  3. Shared Middleware: Authentication, rate limiting, and compression work for both
  4. Incremental Migration: Add gRPC endpoints gradually without breaking REST clients
  5. 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.

Edit this page on GitHub