Schema-driven RPC development, redefined for Go.
Hyperway bridges code-first agility with schema-first discipline. Your Go structs become the single source of truth, dynamically generating Protobuf schemas at runtime. Serve production-ready gRPC, Connect, gRPC-Web, and JSON-RPC 2.0 APIs from a single codebase, with automatic OpenAPI documentation, while maintaining the ability to export standard .proto files to share your schema-driven API with any team, any language.
Traditional gRPC/Connect development follows a schema-first approach:
- Writing
.protofiles - Running
protocwith various plugins - Managing generated code
- Rebuilding when schemas change
While this approach works well for many use cases, it can be cumbersome for rapid prototyping, small services, or teams that prefer working directly with Go types.
The traditional approach offers important advantages:
- Language-neutral contracts -
.protofiles serve as universal API documentation - Mature tooling ecosystem - Linters, breaking change detection, versioning tools
- Clear team boundaries - Explicit contracts for cross-team collaboration
- Established workflows - Well-understood CI/CD patterns
Hyperway preserves these benefits while accelerating development:
- Define your API using Go structs - your types are the schema
- Run your service with automatic schema generation
- Export
.protofiles whenever needed for cross-team collaboration - Use all existing proto tooling with your exported schemas
This hybrid approach maintains the discipline of schema-first development while removing friction from the development cycle. Teams can work rapidly in Go while still providing standard .proto files for tooling, documentation, and cross-language support.
Hyperway implements multiple RPC protocols with dynamic capabilities:
- Generates Protobuf schemas from your Go structs at runtime
- Supports gRPC (Protobuf), Connect RPC (both Protobuf and JSON), gRPC-Web, and JSON-RPC 2.0
- Automatically generates OpenAPI 3.0 documentation at
/openapi.json - Maintains wire compatibility with standard clients for all protocols
- Supports all RPC types: unary, server-streaming, client-streaming, and bidirectional streaming
- Handles both HTTP/1.1 and HTTP/2 (with h2c support)
Hyperway is designed with performance in mind and offers competitive performance compared to connect-go:
- Unary RPCs: Comparable performance across protocols
- Server Streaming: Improved performance and memory efficiency
- Client Streaming: Competitive performance
- Bidirectional Streaming: Efficient implementation
- Memory Usage: Reduced memory consumption for streaming operations
- Dynamic schema generation with caching
- Efficient message parsing using hyperpb
- Buffer pooling to reduce GC pressure
- Optimized streaming with configurable flushing
For detailed benchmarks and performance characteristics, see the protocol-benchmarks directory.
- π Schema-First: Go types as your schema definition language
- π€ Proto Export: Generate standard
.protofiles with language-specific options - β‘ High Performance: Uses hyperpb for efficient dynamic protobuf parsing
- π Multi-Protocol: Supports gRPC, Connect RPC, gRPC-Web, and JSON-RPC 2.0 on the same server
- π‘οΈ Type-Safe: Full Go type safety with runtime schema generation
- π€ Protocol Compatible: Works with any gRPC, Connect, or gRPC-Web client
- β Built-in Validation: Struct tags for automatic input validation
- π gRPC Reflection: Service discovery with dynamic schemas
- π OpenAPI Generation: Automatic API documentation
- π Browser Support: Native gRPC-Web support without proxy
- ποΈ Compression: Multi-algorithm support (gzip, brotli, zstd) for all protocols
- π All Streaming Types: Support for server, client, and bidirectional streaming RPCs
- β° Well-Known Types: Support for common Google Well-Known Types (Timestamp, Duration, Empty, Any, Struct, Value, ListValue, FieldMask)
- π Custom Interceptors: Middleware for logging, auth, metrics, etc.
- π¦ Proto3 Optional: Full support for optional fields
- π― Protobuf Editions: Support for Edition 2023 with features configuration
- π Message Size Limits: Configurable max send/receive message sizes
# Library
go get github.com/i2y/hyperway
# CLI tool
go install github.com/i2y/hyperway/cmd/hyperway@latestpackage main
import (
"context"
"log"
"net/http"
"github.com/i2y/hyperway/rpc"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
// Define your API using Go structs
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=3"`
Email string `json:"email" validate:"required,email"`
}
type CreateUserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
}
// Write your business logic
func createUser(ctx context.Context, req *CreateUserRequest) (*CreateUserResponse, error) {
// Your business logic here
return &CreateUserResponse{
ID: "user-123",
Name: req.Name,
}, nil
}
func main() {
// Create a service
svc := rpc.NewService("UserService",
rpc.WithPackage("user.v1"),
rpc.WithValidation(true),
rpc.WithMaxReceiveMessageSize(10 * 1024 * 1024), // 10MB max receive
rpc.WithMaxSendMessageSize(10 * 1024 * 1024), // 10MB max send
)
// Register your handlers - function name is automatically extracted
if err := rpc.Register(svc, createUser); err != nil {
log.Fatal(err)
}
// Or use explicit naming if preferred
// rpc.RegisterAs(svc, "CreateUser", createUser)
// Create handler - returns a standard http.Handler
handler, _ := rpc.NewHandler(svc)
// Wrap with h2c to support both HTTP/1.1 and HTTP/2 (required for gRPC)
h2s := &http2.Server{}
h2Handler := h2c.NewHandler(handler, h2s)
// Serve (now supports all protocols including gRPC over HTTP/2)
log.Fatal(http.ListenAndServe(":8080", h2Handler))
}By default, Hyperway enables Connect, gRPC, and gRPC-Web protocols. You can customize this using several approaches:
// REST-like APIs (Connect + JSON-RPC)
svc := rpc.NewService("UserService",
rpc.WithPreset(rpc.PresetREST),
)
// gRPC ecosystem (gRPC + gRPC-Web)
svc := rpc.NewService("UserService",
rpc.WithPreset(rpc.PresetGRPC),
)
// All protocols enabled
svc := rpc.NewService("UserService",
rpc.WithPreset(rpc.PresetAll),
)
// Minimal (Connect only)
svc := rpc.NewService("UserService",
rpc.WithPreset(rpc.PresetMinimal),
)// Enable specific protocols
svc := rpc.NewService("UserService",
rpc.WithConnect(true, true), // Allow both JSON and Proto
rpc.WithGRPC(true), // Enable with reflection
rpc.WithJSONRPC("/api/jsonrpc", 10), // Path and batch limit
)
// Disable specific protocols while keeping others
svc := rpc.NewService("UserService",
rpc.DisableGRPCWeb(), // Disable only gRPC-Web
)// Use the fluent builder for complex configurations
config := rpc.ConfigureProtocols().
Connect(true, true).
GRPC(true).
JSONRPC("/api/jsonrpc", 100).
Build()
svc := rpc.NewService("UserService", config)Your service automatically supports multiple protocols and provides OpenAPI documentation:
curl -X POST http://localhost:8080/user.v1.UserService/CreateUser \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com"}'# Note: Requires JSON-RPC to be enabled (see Protocol Configuration above)
curl -X POST http://localhost:8080/api/jsonrpc \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "UserService.CreateUser",
"params": {"name":"Alice","email":"alice@example.com"},
"id": 1
}'grpcurl -plaintext -d '{"name":"Bob","email":"bob@example.com"}' \
localhost:8080 user.v1.UserService/CreateUsercurl -X POST http://localhost:8080/user.v1.UserService/CreateUser \
-H "Content-Type: application/json" \
-H "Connect-Protocol-Version: 1" \
-d '{"name":"Charlie","email":"charlie@example.com"}'# Using buf curl for Connect protocol testing
buf curl --protocol connect \
--http2-prior-knowledge \
--data '{"name":"David","email":"david@example.com"}' \
http://localhost:8080/user.v1.UserService/CreateUser# Get OpenAPI 3.0 specification
curl http://localhost:8080/openapi.json
# View in Swagger UI or any OpenAPI viewer
# The spec includes all your RPC methods with request/response schemasHyperway redefines schema-driven development for the Go ecosystem:
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}Your Go types ARE the schema - type-safe, validated, and version-controlled with your code.
Hyperway automatically generates Protobuf schemas from your types at runtime, maintaining full wire compatibility with standard gRPC/Connect clients.
# Generate standard .proto files from your running service
hyperway proto export --endpoint localhost:8080 --output ./proto
# Export with language-specific options (no manual editing needed!)
hyperway proto export --endpoint localhost:8080 \
--go-package "github.com/example/api;apiv1" \
--java-package "com.example.api"Now share your schema-driven API with any team:
- Client SDK generation in any language
- API documentation and contracts
- Language-specific options are automatically added
- Schema registries (BSR, private repos)
- Standard protobuf tooling compatibility
This hybrid approach delivers the discipline of schema-first design with the agility of Go-native development.
# Export proto files from a running service
hyperway proto export --endpoint http://localhost:8080 --output ./proto
# Export with language-specific options (no manual editing needed!)
hyperway proto export --endpoint http://localhost:8080 \
--go-package "github.com/example/api;apiv1" \
--java-package "com.example.api" \
--csharp-namespace "Example.Api"
# Export as ZIP archive with options
hyperway proto export --endpoint http://localhost:8080 \
--format zip --output api.zip \
--go-package "github.com/example/api;apiv1"
# See all available language options
hyperway proto export --helpHyperway supports all Go types you need:
type Order struct {
ID string `json:"id"`
Items []OrderItem `json:"items"`
Metadata map[string]string `json:"metadata"`
Customer *Customer `json:"customer,omitempty"`
Status OrderStatus `json:"status"`
CreatedAt time.Time `json:"created_at"`
}Hyperway supports the most commonly used Google Well-Known Types:
import (
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
type UpdateRequest struct {
// Dynamic configuration using Struct
Config *structpb.Struct `json:"config"`
// Partial updates using FieldMask
UpdateMask *fieldmaskpb.FieldMask `json:"update_mask"`
// Mixed-type values
Settings map[string]*structpb.Value `json:"settings"`
}Use struct tags for automatic validation:
type RegisterRequest struct {
Username string `json:"username" validate:"required,alphanum,min=3,max=20"`
Password string `json:"password" validate:"required,min=8,containsany=!@#$%"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"required,min=13,max=120"`
}Here's a more complete example showing various features:
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"github.com/i2y/hyperway/rpc"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
// Domain models with validation and well-known types
type CreatePostRequest struct {
Title string `json:"title" validate:"required,min=5,max=200"`
Content string `json:"content" validate:"required,min=10"`
AuthorID string `json:"author_id" validate:"required,uuid"`
Tags []string `json:"tags" validate:"max=10,dive,min=2,max=20"`
Published bool `json:"published"`
Metadata map[string]string `json:"metadata,omitempty"`
}
type Post struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
AuthorID string `json:"author_id"`
Tags []string `json:"tags"`
Published bool `json:"published"`
PublishedAt *time.Time `json:"published_at,omitempty"` // Optional timestamp
CreatedAt time.Time `json:"created_at"` // Required timestamp
UpdatedAt time.Time `json:"updated_at"`
TTL *time.Duration `json:"ttl,omitempty"` // Optional duration
Metadata map[string]string `json:"metadata"`
}
// Service implementation
type BlogService struct {
// your database, cache, etc.
}
func (s *BlogService) CreatePost(ctx context.Context, req *CreatePostRequest) (*Post, error) {
// Business logic here
now := time.Now()
post := &Post{
ID: generateID(),
Title: req.Title,
Content: req.Content,
AuthorID: req.AuthorID,
Tags: req.Tags,
Published: req.Published,
CreatedAt: now,
UpdatedAt: now,
Metadata: req.Metadata,
}
if req.Published {
post.PublishedAt = &now
ttl := 30 * 24 * time.Hour // 30 days
post.TTL = &ttl
}
// Save to database...
return post, nil
}
func main() {
// Create blog service
blogService := &BlogService{}
// Create RPC service with interceptors
svc := rpc.NewService("BlogService",
rpc.WithPackage("blog.v1"),
rpc.WithValidation(true),
rpc.WithReflection(true),
rpc.WithInterceptor(&rpc.RecoveryInterceptor{}),
rpc.WithInterceptor(&rpc.TimeoutInterceptor{Timeout: 30*time.Second}),
)
// Register methods - no need to specify types!
if err := rpc.Register(svc, blogService.CreatePost); err != nil {
log.Fatal(err)
}
// Create handler and serve
handler, err := rpc.NewHandler(svc)
if err != nil {
log.Fatal(err)
}
// Wrap with h2c for HTTP/2 support (required for gRPC)
h2s := &http2.Server{}
h2Handler := h2c.NewHandler(handler, h2s)
log.Println("Blog service running on :8080")
log.Println("- Connect RPC: POST http://localhost:8080/blog.v1.BlogService/CreatePost")
log.Println("- gRPC: localhost:8080 (with reflection)")
log.Fatal(http.ListenAndServe(":8080", h2Handler))
}// Create multiple services
userSvc := rpc.NewService("UserService", rpc.WithPackage("api.v1"))
authSvc := rpc.NewService("AuthService", rpc.WithPackage("api.v1"))
adminSvc := rpc.NewService("AdminService", rpc.WithPackage("api.v1"))
// Register handlers
// Automatic function name extraction
rpc.Register(userSvc, createUser) // Registers as "CreateUser"
rpc.Register(userSvc, getUser) // Registers as "GetUser"
rpc.Register(authSvc, login) // Registers as "Login"
rpc.Register(adminSvc, deleteUser) // Registers as "DeleteUser"
// Serve all services on one port
handler, _ := rpc.NewHandler(userSvc, authSvc, adminSvc)Hyperway provides a type-safe registration API with automatic function name extraction:
// Automatic name extraction from function names
rpc.Register(svc, createUser) // Registers as "CreateUser"
rpc.Register(svc, getUserByID) // Registers as "GetUserByID"
rpc.Register(svc, service.UpdateProfile) // Registers as "UpdateProfile"
// Explicit naming when needed
rpc.RegisterAs(svc, "CustomName", myHandler)
// Streaming methods also support automatic naming
rpc.RegisterServerStream(svc, watchEvents) // Registers as "WatchEvents"
rpc.RegisterClientStream(svc, uploadFiles) // Registers as "UploadFiles"
rpc.RegisterBidiStream(svc, chatHandler) // Registers as "ChatHandler"The registration functions use Go generics to maintain full type safety at compile time, preventing runtime type errors while keeping the API clean and simple.
For services with many methods, Hyperway provides batch registration capabilities:
// Define methods with explicit types
svc.RegisterAll(
rpc.Unary("CreateUser", createUser),
rpc.Unary("GetUser", getUser),
rpc.ServerStreamDef("WatchUsers", watchUsers),
rpc.ClientStreamDef("ImportUsers", importUsers),
rpc.BidiStreamDef("UserChat", userChat),
)// Group related methods
userMethods := rpc.Group().
Add(rpc.Unary("Create", userService.Create)).
Add(rpc.Unary("Get", userService.Get)).
Add(rpc.Unary("Update", userService.Update)).
Add(rpc.Unary("Delete", userService.Delete))
// Register the group
userMethods.Register(svc)// Implement ServiceRegistrar interface
type UserService struct {
db Database
}
func (s *UserService) RegisterMethods(svc *rpc.Service) error {
return svc.RegisterAll(
rpc.Unary("CreateUser", s.CreateUser),
rpc.Unary("GetUser", s.GetUser),
rpc.ServerStreamDef("WatchUsers", s.WatchUsers),
)
}
// Register all methods from the service
userService := &UserService{db: db}
svc.RegisterService(userService)// Server sends multiple responses to client
func (s *Service) WatchEvents(ctx context.Context, req *WatchRequest, stream rpc.ServerStream[*Event]) error {
for i := 0; i < 10; i++ {
event := &Event{
ID: fmt.Sprintf("event-%d", i),
Message: fmt.Sprintf("Event %d", i),
}
if err := stream.Send(event); err != nil {
return err
}
time.Sleep(100 * time.Millisecond)
}
return nil
}
// Register: rpc.RegisterServerStream(svc, service.WatchEvents) // Auto-registers as "WatchEvents"// Client sends multiple requests, server sends single response
func (s *Service) UploadFiles(ctx context.Context, stream rpc.ClientStream[*FileChunk]) (*UploadResult, error) {
var totalSize int64
for {
chunk, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
totalSize += int64(len(chunk.Data))
}
return &UploadResult{TotalSize: totalSize}, nil
}
// Register: rpc.RegisterClientStream(svc, service.UploadFiles) // Auto-registers as "UploadFiles"// Both client and server send multiple messages
func (s *Service) Chat(ctx context.Context, stream rpc.BidiStream[*ChatMessage, *ChatResponse]) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
response := &ChatResponse{
Message: fmt.Sprintf("Echo: %s", msg.Text),
}
if err := stream.Send(response); err != nil {
return err
}
}
}
// Register: rpc.RegisterBidiStream(svc, service.Chat) // Auto-registers as "Chat"Hyperway provides a fluent error builder API for creating rich, detailed errors:
// Simple error with code and message
return rpc.InvalidArgument("email is required").Build()
// Error with additional details
return rpc.NotFound("user not found").
Detail("user_id", userID).
Detail("searched_in", "primary_db").
Build()
// Error with formatted message
return rpc.Internal("database error").
Messagef("failed to connect to %s:%d", host, port).
Detail("retry_after", "5s").
Build()
// Custom error building
return rpc.NewErrorBuilder().
Code(rpc.CodeResourceExhausted).
Message("quota exceeded").
Detail("limit", 1000).
Detail("current", 1050).
Build()// All common error types have convenience constructors
rpc.InvalidArgument("message") // Invalid input
rpc.NotFound("message") // Resource not found
rpc.AlreadyExists("message") // Resource already exists
rpc.PermissionDenied("message") // Insufficient permissions
rpc.Unauthenticated("message") // Authentication required
rpc.ResourceExhausted("message") // Quota/limit exceeded
rpc.FailedPrecondition("message") // Operation prerequisites not met
rpc.Aborted("message") // Operation aborted
rpc.OutOfRange("message") // Value out of range
rpc.Unimplemented("message") // Feature not implemented
rpc.Internal("message") // Internal server error
rpc.Unavailable("message") // Service unavailable
rpc.DataLoss("message") // Data loss or corruption
rpc.DeadlineExceeded("message") // Operation timeoutfunc validateUser(user *User) error {
collector := rpc.NewErrorCollector()
if user.Email == "" {
collector.Add("email", "email is required")
} else if !isValidEmail(user.Email) {
collector.Add("email", "invalid email format")
}
if user.Age < 18 {
collector.Addf("age", "must be at least %d years old", 18)
}
if collector.HasErrors() {
return collector.AsError() // Returns error with all validation issues
}
return nil
}Hyperway provides a fluent API for working with request/response headers and trailers:
func myHandler(ctx context.Context, req *Request) (*Response, error) {
// Fluent API for headers and trailers
rpc.Context(ctx).
SetHeader("X-Request-ID", requestID).
SetHeader("X-Version", "1.0.0").
SetTrailer("X-Processing-Time", processingTime)
// Get request headers
helper := rpc.Context(ctx)
clientID := helper.GetHeader("X-Client-ID")
authToken := helper.GetHeader("Authorization")
// Get request metadata
userAgent := helper.GetMetadata("user-agent")
return &Response{...}, nil
}func streamingHandler(ctx context.Context, req *Request, stream rpc.ServerStream[Response]) error {
helper := rpc.Context(ctx)
// Set response headers (must be before first Send)
helper.SetHeaders(map[string]string{
"X-Stream-ID": streamID,
"X-Total-Items": strconv.Itoa(totalItems),
})
// Stream responses...
for _, item := range items {
if err := stream.Send(item); err != nil {
return err
}
}
// Set trailers (after all sends)
helper.SetTrailers(map[string]string{
"X-Items-Sent": strconv.Itoa(sentCount),
"X-Duration": duration.String(),
})
return nil
}func protectedHandler(ctx context.Context, req *Request) (*Response, error) {
// Validate required headers
err := rpc.Context(ctx).
RequireHeader("Authorization", "auth token required").
RequireHeader("X-API-Key", "API key required").
Validate()
if err != nil {
return nil, err
}
// Process request...
return &Response{...}, nil
}package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"time"
"github.com/i2y/hyperway/rpc"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
// Request/Response types
type WatchRequest struct {
Filter string `json:"filter"`
}
type Event struct {
ID string `json:"id"`
Message string `json:"message"`
Time time.Time `json:"time"`
}
type FileChunk struct {
Name string `json:"name"`
Data []byte `json:"data"`
}
type UploadResult struct {
TotalSize int64 `json:"total_size"`
}
type ChatMessage struct {
Text string `json:"text"`
}
type ChatResponse struct {
Message string `json:"message"`
}
// Service implementation
type StreamService struct{}
// Server streaming
func (s *StreamService) WatchEvents(ctx context.Context, req *WatchRequest, stream rpc.ServerStream[*Event]) error {
for i := 0; i < 5; i++ {
event := &Event{
ID: fmt.Sprintf("event-%d", i),
Message: fmt.Sprintf("Filtered by: %s", req.Filter),
Time: time.Now(),
}
if err := stream.Send(event); err != nil {
return err
}
time.Sleep(100 * time.Millisecond)
}
return nil
}
// Client streaming
func (s *StreamService) UploadFiles(ctx context.Context, stream rpc.ClientStream[*FileChunk]) (*UploadResult, error) {
var totalSize int64
for {
chunk, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
totalSize += int64(len(chunk.Data))
}
return &UploadResult{TotalSize: totalSize}, nil
}
// Bidirectional streaming
func (s *StreamService) Chat(ctx context.Context, stream rpc.BidiStream[*ChatMessage, *ChatResponse]) error {
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
response := &ChatResponse{
Message: fmt.Sprintf("Server says: %s", msg.Text),
}
if err := stream.Send(response); err != nil {
return err
}
}
}
func main() {
service := &StreamService{}
// Create service
svc := rpc.NewService("StreamService",
rpc.WithPackage("stream.v1"),
rpc.WithReflection(true),
)
// Register all streaming methods
// Automatic function name extraction for streaming methods
rpc.MustRegisterServerStream(svc, service.WatchEvents) // Registers as "WatchEvents"
rpc.MustRegisterClientStream(svc, service.UploadFiles) // Registers as "UploadFiles"
rpc.MustRegisterBidiStream(svc, service.Chat) // Registers as "Chat"
// Create handler and serve
handler, err := rpc.NewHandler(svc)
if err != nil {
log.Fatal(err)
}
// Wrap with h2c for HTTP/2 support (required for gRPC)
h2s := &http2.Server{}
h2Handler := h2c.NewHandler(handler, h2s)
log.Println("Streaming service running on :8080")
log.Fatal(http.ListenAndServe(":8080", h2Handler))
}For more control, you can use the builder pattern:
// Use the builder pattern for additional options
rpc.MustRegisterMethod(svc,
rpc.NewMethod("CreateUser", createUser).
Validate(true).
WithInterceptors(customInterceptor),
)// Add logging, auth, rate limiting, etc.
svc := rpc.NewService("MyService",
rpc.WithInterceptor(&rpc.LoggingInterceptor{}),
rpc.WithInterceptor(&rpc.RecoveryInterceptor{}),
)Hyperway's handler implements the standard http.Handler interface, making it fully compatible with Go's HTTP ecosystem. This means you can:
- Use any standard net/http middleware
- Combine it with other HTTP handlers
- Integrate with existing HTTP routers and frameworks
- Pass context values from middleware to RPC handlers
HTTP middleware can add values to the request context, and these values will be accessible in your RPC handlers:
// Middleware that adds context values
func contextMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Add request ID
ctx := context.WithValue(r.Context(), "request-id", generateRequestID())
// Add user info from auth header
if userID := extractUserID(r.Header.Get("Authorization")); userID != "" {
ctx = context.WithValue(ctx, "user-id", userID)
}
// Pass the enriched context to the next handler
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// RPC handler can access context values
func createOrder(ctx context.Context, req *CreateOrderRequest) (*CreateOrderResponse, error) {
// Get values from context
requestID, _ := ctx.Value("request-id").(string)
userID, _ := ctx.Value("user-id").(string)
log.Printf("Processing order for user %s (request: %s)", userID, requestID)
// Your business logic here...
return &CreateOrderResponse{
OrderID: generateOrderID(),
RequestID: requestID,
}, nil
}
// Logging middleware with request tracking
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
requestID := generateRequestID()
// Add request ID to context for correlation
ctx := context.WithValue(r.Context(), "request-id", requestID)
log.Printf("[%s] Started %s %s", requestID, r.Method, r.URL.Path)
// Wrap ResponseWriter to capture status
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r.WithContext(ctx))
log.Printf("[%s] Completed with %d in %v",
requestID, wrapped.statusCode, time.Since(start))
})
}
// Auth middleware with context enrichment
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Validate token and extract user info
userInfo, err := validateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Add user info to context
ctx := context.WithValue(r.Context(), "user", userInfo)
ctx = context.WithValue(ctx, "user-id", userInfo.ID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}Here's a complete example showing middleware composition and context propagation:
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"github.com/i2y/hyperway/rpc"
"github.com/google/uuid"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
// Request/Response types
type CreateOrderRequest struct {
ProductID string `json:"product_id" validate:"required"`
Quantity int `json:"quantity" validate:"required,min=1"`
}
type CreateOrderResponse struct {
OrderID string `json:"order_id"`
UserID string `json:"user_id"`
RequestID string `json:"request_id"`
}
// Business logic that uses context values
func createOrder(ctx context.Context, req *CreateOrderRequest) (*CreateOrderResponse, error) {
// Access context values set by middleware
requestID, _ := ctx.Value("request-id").(string)
userID, _ := ctx.Value("user-id").(string)
log.Printf("[%s] Creating order for user %s: %d x %s",
requestID, userID, req.Quantity, req.ProductID)
// Create order...
orderID := fmt.Sprintf("order-%s", uuid.New().String()[:8])
return &CreateOrderResponse{
OrderID: orderID,
UserID: userID,
RequestID: requestID,
}, nil
}
func main() {
// Create service
svc := rpc.NewService("OrderService",
rpc.WithPackage("shop.v1"),
rpc.WithValidation(true),
)
// Register handlers
rpc.Register(svc, createOrder) // Automatically registers as "CreateOrder"
// Create handler
handler, _ := rpc.NewHandler(svc)
// Create a standard mux
mux := http.NewServeMux()
// Chain middleware: auth -> logging -> context -> handler
// Context values flow through to RPC handlers
mux.Handle("/",
authMiddleware(
loggingMiddleware(
contextMiddleware(handler))))
// Add health check endpoint
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, `{"status":"healthy"}`)
})
// Serve static files
mux.Handle("/static/", http.StripPrefix("/static/",
http.FileServer(http.Dir("./static"))))
// Add metrics endpoint
mux.Handle("/metrics", promhttp.Handler())
// Use with popular routers (e.g., gorilla/mux, chi)
// router := chi.NewRouter()
// router.Use(middleware.RequestID)
// router.Use(middleware.Logger)
// router.Mount("/api", handler)
// Wrap with h2c for HTTP/2 support
h2s := &http2.Server{}
h2Handler := h2c.NewHandler(mux, h2s)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", h2Handler))
}This flexibility allows you to:
- Pass request-scoped data (request ID, user info, trace ID) from middleware to handlers
- Add authentication, rate limiting, or CORS handling at the HTTP layer
- Serve your RPC API alongside REST endpoints on the same server
- Integrate with observability tools (Prometheus, OpenTelemetry)
- Use popular Go web frameworks and routers (chi, gin, echo, gorilla/mux)
- Implement custom request/response processing with full HTTP control
The key insight is that Hyperway's handler is just a standard http.Handler, so any context values set via r.WithContext() in your middleware will be available in your RPC handlers via the ctx parameter.
Hyperway implements a schema-driven architecture where:
- Go Types as Schema Source: Your structs define the contract, enforced at compile time
- Runtime Schema Generation: Dynamic Protobuf generation maintains wire compatibility
- Single Source of Truth: No schema duplication between
.protofiles and Go code
- High-Performance Parsing: Leverages hyperpb for optimized message handling
- Multi-Protocol Gateway: Unified implementation of gRPC, Connect, and gRPC-Web
- Standard http.Handler Interface: Seamless integration with Go's HTTP ecosystem
- Extensible Middleware: Interceptors for cross-cutting concerns
- Type-Safe by Design: Compile-time type checking with runtime protocol compliance
Traditional Proto-First:
- Edit
.protofile - Run code generation
- Update implementation
- Handle generated code inconsistencies
Hyperway:
- Edit Go struct
- Run service
- (Optional) Export
.protowhen sharing
Export .proto files when you need:
- Cross-language clients - Generate SDKs for other languages
- API documentation - Share contracts with external teams
- Breaking change detection - Use with buf or similar tools
- Schema registries - Upload to BSR or internal registries
# Development phase: Iterate rapidly with Go types
# Just write code, test, and refine
# Collaboration phase: Export schemas for wider use
hyperway proto export --endpoint localhost:8080 --output ./proto
# Now you have both:
# - Fast iteration for ongoing development
# - Standard .proto files for tooling and cross-team collaborationβ Perfect for:
- Teams embracing schema-driven development with Go
- Microservices requiring both type safety and rapid iteration
- Projects that value schema-first principles without manual schema maintenance
- Services that need multi-protocol support (gRPC + Connect RPC)
- Applications using all RPC types (unary, server/client/bidirectional streaming)
- Systems requiring automatic validation and type safety
- Organizations wanting to share schemas across polyglot teams
β Current Limitations:
- Go-only service definitions - Use exported protos for other languages
- Limited buf curl compatibility - Some Well-Known Types (Struct, FieldMask) have JSON parsing issues with buf curl
- Map of Well-Known Types -
map[string]*structpb.Valuecauses runtime panics (implementation limitation) - gRPC streaming compatibility - All streaming types (server/client/bidirectional) work with standard gRPC and Connect clients
Hyperway supports all RPC types (unary, server-streaming, client-streaming, and bidirectional streaming) with:
- β Comprehensive test coverage
- β Performance optimizations
- β Memory-efficient implementation
- β Thread-safe design
- β Clean static analysis
- β Configurable streaming behavior
- β
Proto Export - Generate standard
.protofiles from running services - β Full Compatibility - Exported protos work with buf, protoc, and all standard tools
- β Schema Registries - Compatible with BSR and corporate registries
- β Wire Compatibility - Works with any gRPC/Connect client
We welcome contributions! Please see our Contributing Guide for details.
# Clone the repository
git clone https://github.com/i2y/hyperway.git
cd hyperway
# Install dependencies
go mod download
# Run tests
make test
# Run linter
make lint
# Run benchmarks
make benchMIT License - see LICENSE file for details.
- Server-streaming RPC support
- Client-streaming RPC support
- Bidirectional streaming RPC support
- Streaming performance optimizations
- Protobuf Editions support (Edition 2023)
- Additional Well-Known Types (Struct, Value, ListValue, FieldMask)
- Buffer pooling and concurrency optimizations
- JSON-RPC 2.0 protocol support (v0.4.0)
- Multi-compression support: gzip, brotli, zstd (v0.5.0)
- Configurable message size limits (v0.5.0)
- Enhanced proto export with language-specific options (v0.5.0)
- Unified protocol configuration API (v0.6.0)
- Metrics and tracing integration (OpenTelemetry)
- WebSocket support for JSON-RPC
- Plugin system for custom protocols
A: Hyperway generates standard Protobuf schemas. Export them as .proto files and use any existing tooling - buf, protoc, linters, breaking change detection, etc. Your exported schemas are fully compatible with the entire Protobuf ecosystem.
A: Hyperway is currently under active development but may be suitable for production use depending on your requirements. It supports all RPC types (unary, server-streaming, client-streaming, and bidirectional streaming) with competitive performance. We recommend thoroughly testing it for your specific use case before production deployment. It's particularly well-suited for prototyping, internal tools, and services where rapid development is prioritized.
A: Export your schemas as .proto files and generate clients in any language. Hyperway maintains full wire compatibility with standard gRPC and Connect clients, so your services work seamlessly with clients written in any supported language.
// Create a new service
svc := rpc.NewService(name string, opts ...ServiceOption)
// Service options
rpc.WithPackage(pkg string) // Set protobuf package
rpc.WithValidation(enabled bool) // Enable validation
rpc.WithReflection(enabled bool) // Enable gRPC reflection
rpc.WithMaxReceiveMessageSize(bytes int) // Max receive size
rpc.WithMaxSendMessageSize(bytes int) // Max send size
rpc.WithInterceptors(interceptors ...Interceptor) // Add interceptors
rpc.WithServiceConfig(json string) // gRPC service config
rpc.WithEdition(edition string) // Protobuf edition// Automatic name extraction
rpc.Register(svc, handler) // Extract name from function
rpc.RegisterServerStream(svc, handler) // Server streaming
rpc.RegisterClientStream(svc, handler) // Client streaming
rpc.RegisterBidiStream(svc, handler) // Bidirectional streaming
// Explicit naming
rpc.RegisterAs(svc, name, handler) // Unary with name
rpc.RegisterServerStreamAs(svc, name, handler) // Server stream with name
rpc.RegisterClientStreamAs(svc, name, handler) // Client stream with name
rpc.RegisterBidiStreamAs(svc, name, handler) // Bidi stream with name
// Batch registration
svc.RegisterAll(methods ...MethodDefinition) // Register multiple
rpc.Unary(name, handler) // Unary method definition
rpc.ServerStreamDef(name, handler) // Server stream definition
rpc.ClientStreamDef(name, handler) // Client stream definition
rpc.BidiStreamDef(name, handler) // Bidi stream definition// Presets
rpc.WithPreset(rpc.PresetREST) // Connect + JSON-RPC
rpc.WithPreset(rpc.PresetGRPC) // gRPC + gRPC-Web
rpc.WithPreset(rpc.PresetAll) // All protocols
rpc.WithPreset(rpc.PresetMinimal) // Connect only
// Individual protocols
rpc.WithConnect(allowJSON, allowProto bool) // Enable Connect
rpc.WithGRPC(enableReflection bool) // Enable gRPC
rpc.WithGRPCWeb() // Enable gRPC-Web
rpc.WithJSONRPC(path string, batchLimit int) // Enable JSON-RPC
// Disable protocols
rpc.DisableConnect() // Disable Connect
rpc.DisableGRPC() // Disable gRPC
rpc.DisableGRPCWeb() // Disable gRPC-Web
rpc.DisableJSONRPC() // Disable JSON-RPC
// Fluent builder
rpc.ConfigureProtocols().
Connect(true, true).
GRPC(true).
Build()// Error builders
rpc.InvalidArgument(msg) // Create error builder
rpc.NotFound(msg) // Common error types
rpc.Internal(msg) // Internal error
rpc.NewErrorBuilder() // Custom builder
// Error builder methods
.Code(code) // Set error code
.Message(msg) // Set message
.Messagef(format, args...) // Formatted message
.Detail(key, value) // Add detail
.Details(map[string]any) // Add multiple details
.Build() // Build error
// Error collection
collector := rpc.NewErrorCollector()
collector.Add(field, message) // Add error
collector.Addf(field, format, args...) // Add formatted
collector.HasErrors() // Check if has errors
collector.AsError() // Convert to error// Context helper
helper := rpc.Context(ctx)
// Headers and trailers
helper.SetHeader(key, value) // Set response header
helper.SetHeaders(map[string]string) // Set multiple headers
helper.SetTrailer(key, value) // Set response trailer
helper.SetTrailers(map[string]string) // Set multiple trailers
// Get request data
helper.GetHeader(key) // Get request header
helper.GetMetadata(key) // Get request metadata
helper.RequireHeader(key, errorMsg) // Require header
helper.Validate() // Validate requirements// Create HTTP handler from services
handler, err := rpc.NewHandler(services ...*Service)
// Handler options
rpc.WithGatewayOptions(opts ...gateway.Option)- Connect-RPC - Protocol specification and wire format
- hyperpb - High-performance protobuf parsing with PGO
- go-playground/validator - Struct validation
- The Go community for inspiration and feedback